Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions libs/code/deepagents_code/_env_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,18 @@
destinations are written today.
"""

LAUNCH_TERM_PROGRAM = "DEEPAGENTS_CODE_LAUNCH_TERM_PROGRAM"
"""Internal sentinel recording the `TERM_PROGRAM` present when `dcode` started.

Not user-facing. The resume hint echoes `TERM_PROGRAM` only when the launch
environment supplied it (an inline `TERM_PROGRAM=x dcode`, a terminal's own
export, or a shell alias), so the value set by a project or global `.env` file
*after* launch must not leak in. The app itself never sets `TERM_PROGRAM`, so
`cli_main` snapshotting the variable here at entry means a set sentinel always
marks an explicit launch value; the update re-exec inherits it unchanged,
which is correct because the relaunch runs the command the user typed.
"""

LEGACY_ENABLED_PROJECT_MCP_SERVERS = "DEEPAGENTS_CODE_ENABLED_PROJECT_MCP_SERVERS"
"""Removed project MCP allowlist env var retained for migration detection only.

Expand Down
12 changes: 9 additions & 3 deletions libs/code/deepagents_code/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8080,9 +8080,15 @@ def _refresh_cache_display(self) -> None:
inputs += self._inflight_turn_stats.input_tokens
reads += self._inflight_turn_stats.cache_read_tokens
writes += self._inflight_turn_stats.cache_write_tokens
cache_display = self._status_bar.query_one("#cache-display")
cache_display.visible = self._thread_has_completed_turn and writes > 0
self._status_bar.set_cache_tokens(reads, writes, input_tokens=inputs)
# The usage worker can fire while `/reload` has the status bar
# mid-compose or mid-teardown, before `#cache-display` is queryable.
# `set_cache_tokens` also touches the DOM, so skip both on that race;
# the next usage update (or `_reset_thread_usage` on the new thread)
# repaints.
with suppress(NoMatches):
cache_display = self._status_bar.query_one("#cache-display")
cache_display.visible = self._thread_has_completed_turn and writes > 0
self._status_bar.set_cache_tokens(reads, writes, input_tokens=inputs)

def _set_session_cost(
self,
Expand Down
4 changes: 4 additions & 0 deletions libs/code/deepagents_code/config_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1820,6 +1820,10 @@ def _credential_options() -> tuple[ConfigOption, ...]:
# Set by the self-update restart to carry the launched command name into
# the re-exec'd process; never user-configured.
_env_vars.INVOKED_AS,
# Launch-time snapshot of `TERM_PROGRAM` recorded by `cli_main` so the
# resume hint can distinguish an explicit launch value from a `.env`
# file that sets `TERM_PROGRAM` after launch; never user-configured.
_env_vars.LAUNCH_TERM_PROGRAM,
}
)
"""`_env_vars` constants intentionally excluded from the option catalog."""
Expand Down
60 changes: 56 additions & 4 deletions libs/code/deepagents_code/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
# Suppress Pydantic v1 compatibility warnings from langchain on Python 3.14+
warnings.filterwarnings("ignore", message=".*Pydantic V1.*", category=UserWarning)

from deepagents_code._env_vars import LAUNCH_TERM_PROGRAM
from deepagents_code._version import __version__

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -264,6 +265,36 @@ def _should_check_teardown_thread(
return bool(thread_id)


def _resume_term_program() -> str | None:
"""Return a `TERM_PROGRAM` value safe to echo inside the resume hint.

The value is read from `LAUNCH_TERM_PROGRAM` — the snapshot `cli_main`
takes at process entry — rather than live `TERM_PROGRAM`, so only a value
the launch environment supplied (inline prefix, terminal export, or shell
alias) is echoed back. A `TERM_PROGRAM` that appears later, from a project
or global `.env` file, never reaches the hint.

Returns:
The launch-time value when it is set and fully printable, else `None`.
A value carrying control characters is dropped rather than stripped:
stripping would both write raw escape sequences into teardown output
and name a terminal the environment never actually contained. Native
Windows shells also return `None`: VS Code and WezTerm set
`TERM_PROGRAM` on every platform, so its presence under `win32` does
not imply a POSIX shell, and the `VAR=value` prefix would be executed
as a command by `cmd.exe`/PowerShell. POSIX markers (`SHELL` from
git-bash/MSYS, `MSYSTEM`, `WSL_DISTRO_NAME`) restore the prefix there.
"""
raw = os.environ.get(LAUNCH_TERM_PROGRAM, "").strip()
if not raw or not raw.isprintable():
return None
if sys.platform == "win32" and not any(
os.environ.get(marker) for marker in ("SHELL", "MSYSTEM", "WSL_DISTRO_NAME")
):
return None
return raw


def _render_teardown_thread_hints(
console: "Console",
thread_id: str,
Expand All @@ -282,6 +313,8 @@ def _render_teardown_thread_hints(
thread_id: Thread whose checkpoints back the hints.
return_code: Process exit code; failed sessions add a resume safety caveat.
"""
import shlex

from rich.style import Style
from rich.text import Text

Expand Down Expand Up @@ -318,10 +351,21 @@ def _render_teardown_thread_hints(
console.print("[dim]Resume this thread with:[/dim]")
# Echo the command the user actually launched (a shim or the
# `deepagents-code` alias), not a hardcoded `dcode` they may not have.
hint = Text(invoked_name(), style="cyan")
hint.append(" -r ", style="cyan")
hint.append(str(thread_id), style="cyan")
console.print(hint)
resume_command = shlex.join([invoked_name(), "-r", str(thread_id)])
# A shell alias that exports `TERM_PROGRAM` (to select a theme, say) is
# invisible to `invoked_name`, since an alias does not change `argv[0]`, so
# the bare command would resume without it. Carry the launch-time value as
# an env prefix to keep the line pasteable as-is; the launch snapshot (not
# the live variable) is what keeps a `.env`-supplied `TERM_PROGRAM` out of
# the hint. The prefix uses POSIX syntax, so `_resume_term_program`
# withholds it on native Windows, where terminals (VS Code, WezTerm) set
# the variable even under `cmd.exe`/PowerShell and those shells cannot
# parse a `VAR=value` command prefix.
term_program = _resume_term_program()
if term_program is not None:
resume_command = f"TERM_PROGRAM={shlex.quote(term_program)} {resume_command}"
console.print(Text(resume_command, style="cyan"))

if return_code != 0:
console.print(
"[dim]Note: the session exited with a non-zero status. Attempting "
Expand Down Expand Up @@ -4196,6 +4240,14 @@ def cli_main() -> None:
if sys.platform == "darwin":
os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "0"

# Snapshot `TERM_PROGRAM` before settings bootstrap loads any `.env` file,
# so the resume hint echoes the variable only when the launch environment
# (inline prefix, terminal export, or shell alias) supplied it. The app
# itself never sets `TERM_PROGRAM`, and the update re-exec inherits this
# sentinel, so a set value here always marks an explicit launch value.
if "TERM_PROGRAM" in os.environ and LAUNCH_TERM_PROGRAM not in os.environ:
os.environ[LAUNCH_TERM_PROGRAM] = os.environ["TERM_PROGRAM"]

# Note: LANGSMITH_PROJECT override is handled lazily by config.py's
# _ensure_bootstrap() (triggered on first access of `settings`).
# This ensures agent traces use DEEPAGENTS_CODE_LANGSMITH_PROJECT while
Expand Down
11 changes: 11 additions & 0 deletions libs/code/tests/unit_tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2615,6 +2615,17 @@ def test_refresh_includes_matching_inflight_input_total(self) -> None:
input_tokens=1_500,
)

def test_refresh_swallows_uncomposed_status_bar(self) -> None:
"""A usage update racing `/reload` compose/teardown must not raise."""
app = DeepAgentsApp(thread_id="thread-123")
app._status_bar = MagicMock()
app._status_bar.query_one.side_effect = NoMatches(
"No nodes match '#cache-display'"
)

# Should not raise despite the status bar lacking `#cache-display`.
app._refresh_cache_display()


class TestThreadCachePrewarm:
"""Tests for startup thread-cache prewarming."""
Expand Down
Loading