mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 01:35:28 -04:00
fix(code): restore welcome banner tips (#4528)
Restores rotating startup tips to the compact Deep Agents Code welcome banner after the banner rewrite removed the tip row. The fix keeps the compact box layout, selects one weighted tip per banner instance, and restores the `DEEPAGENTS_CODE_HIDE_SPLASH_TIPS` opt-out. ### Screenshots <img width="750" height="760" alt="Screenshot 2026-07-06 at 2 06 44 PM" src="https://github.com/user-attachments/assets/feadd944-dad0-4813-bf19-d4465b2b3843" /> --------- Co-authored-by: Mason Daugherty <mason@langchain.dev> Co-authored-by: Mason Daugherty <github@mdrxy.com>
This commit is contained in:
committed by
GitHub
parent
f183c1b217
commit
3f1e55eafb
+1
-1
@@ -99,7 +99,7 @@ The `deepagents-code --help` screen is hand-maintained in `ui.show_help()`, sepa
|
||||
|
||||
## Splash screen tips
|
||||
|
||||
When adding a user-facing CLI feature (new slash command, keybinding, workflow), add a corresponding tip to the `_TIPS` list in `deepagents_code/tui/widgets/welcome.py`. Tips are shown randomly on startup to help users discover features. Keep tips short and action-oriented (e.g., `"Press ctrl+x to compose prompts in your external editor"`).
|
||||
When adding a user-facing CLI feature (new slash command, keybinding, workflow), add a corresponding entry to the `_TIPS` dict in `deepagents_code/tui/widgets/startup_tip.py`, mapping the tip text to a relative selection weight (higher = shown more often). One tip is chosen at random above the input on startup to help users discover features. Keep tips short and action-oriented (e.g., `"Press ctrl+x to compose prompts in your external editor"`).
|
||||
|
||||
## Slash commands
|
||||
|
||||
|
||||
@@ -156,6 +156,9 @@ HIDE_GIT_BRANCH = "DEEPAGENTS_CODE_HIDE_GIT_BRANCH"
|
||||
HIDE_LANGSMITH_TRACING = "DEEPAGENTS_CODE_HIDE_LANGSMITH_TRACING"
|
||||
"""Hide LangSmith tracing project/thread info in the startup splash when enabled."""
|
||||
|
||||
HIDE_SPLASH_TIPS = "DEEPAGENTS_CODE_HIDE_SPLASH_TIPS"
|
||||
"""Hide the startup tip shown above the chat input when enabled."""
|
||||
|
||||
HIDE_SPLASH_VERSION = "DEEPAGENTS_CODE_HIDE_SPLASH_VERSION"
|
||||
"""Hide version and local-install details in the splash screen when enabled."""
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ from deepagents_code.tui.widgets.messages import (
|
||||
ToolGroupSummary,
|
||||
UserMessage,
|
||||
)
|
||||
from deepagents_code.tui.widgets.startup_tip import StartupTip, show_startup_tip
|
||||
from deepagents_code.tui.widgets.status import StatusBar
|
||||
from deepagents_code.tui.widgets.subagent_panel import SubagentPanel
|
||||
from deepagents_code.tui.widgets.welcome import WelcomeBanner
|
||||
@@ -2911,8 +2912,11 @@ class DeepAgentsApp(App):
|
||||
yield Container(id="messages")
|
||||
with Container(id="bottom-app-container"):
|
||||
# Live fan-out panel for subagents spawned from js_eval. Hidden
|
||||
# until the first spawn event; sits just above the input.
|
||||
# until the first spawn event; sits at the top of the bottom
|
||||
# container, above the startup tip and input.
|
||||
yield SubagentPanel(id="subagent-panel")
|
||||
if show_startup_tip():
|
||||
yield StartupTip(id="startup-tip")
|
||||
yield ChatInput(
|
||||
cwd=self._cwd,
|
||||
image_tracker=self._image_tracker,
|
||||
@@ -7353,6 +7357,11 @@ class DeepAgentsApp(App):
|
||||
`ALWAYS_IMMEDIATE` fast path for commands they classify as
|
||||
urgent.
|
||||
"""
|
||||
# Any submitted prompt (interactive or external) ends the startup
|
||||
# tip's lifetime, so dismiss it here at the shared entry point rather
|
||||
# than in a single handler.
|
||||
await self._dismiss_startup_tip()
|
||||
|
||||
from deepagents_code.command_registry import (
|
||||
ALWAYS_IMMEDIATE,
|
||||
HIDDEN_COMMANDS,
|
||||
@@ -7420,6 +7429,16 @@ class DeepAgentsApp(App):
|
||||
|
||||
await self._submit_input(value, mode)
|
||||
|
||||
async def _dismiss_startup_tip(self) -> None:
|
||||
"""Remove the startup tip once the first prompt is submitted.
|
||||
|
||||
Called from `_submit_input`, so every submission path (interactive
|
||||
and external) dismisses the tip. Subsequent calls are no-ops: the
|
||||
widget is already gone and `query_one` raises `NoMatches`.
|
||||
"""
|
||||
with suppress(NoMatches):
|
||||
await self.query_one("#startup-tip", StartupTip).remove()
|
||||
|
||||
async def on_external_input(self, event: ExternalInput) -> None:
|
||||
"""Route external prompt and command events through the app queue.
|
||||
|
||||
|
||||
@@ -861,6 +861,14 @@ _STATIC_OPTIONS: tuple[ConfigOption, ...] = (
|
||||
default=True,
|
||||
env_var=_env_vars.SHOW_LANGSMITH_REPLICA_TRACING,
|
||||
),
|
||||
ConfigOption(
|
||||
key="display.hide_splash_tips",
|
||||
group="Display",
|
||||
summary="Hide the startup tip shown above the chat input.",
|
||||
kind=OptionKind.BOOL,
|
||||
default=False,
|
||||
env_var=_env_vars.HIDE_SPLASH_TIPS,
|
||||
),
|
||||
ConfigOption(
|
||||
key="display.hide_splash_version",
|
||||
group="Display",
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Startup tip widget shown above the chat input."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
from textual.content import Content
|
||||
from textual.widgets import Static
|
||||
|
||||
from deepagents_code._env_vars import HIDE_SPLASH_TIPS, is_env_truthy
|
||||
|
||||
_TIPS: dict[str, int] = {
|
||||
"Use @ to reference files and / for commands": 3,
|
||||
"Try /threads to resume a previous conversation": 2,
|
||||
"Use /offload when your conversation gets long": 2,
|
||||
"Use /copy to copy the latest assistant message": 3,
|
||||
"Use /mcp to search your MCP servers and inspect tool parameters": 1,
|
||||
"Use /mcp login <server> to authenticate MCP OAuth servers without leaving the TUI": 1, # noqa: E501
|
||||
"Use /remember to save learnings from this conversation": 1,
|
||||
"Use /model to switch models mid-conversation": 2,
|
||||
"Use /effort high to change the current model's reasoning effort": 1,
|
||||
"Press ctrl+x to compose prompts in your external editor": 1,
|
||||
"Press ctrl+u to delete to the start of the line in the chat input": 1,
|
||||
"Use /skill:<name> to invoke a skill directly": 1,
|
||||
"Type /update to check for and install updates": 1,
|
||||
"Use /install <extra> to add optional dependencies (e.g. /install daytona)": 1,
|
||||
"Use /theme to customize the TUI's colors": 1,
|
||||
"Use /skill-creator to build reusable agent skills": 1,
|
||||
"Ask for a workflow to fan work out to subagents in parallel": 3,
|
||||
"Use /auto-update to toggle automatic updates": 1,
|
||||
"Use /timestamps to show or hide message timestamp footers": 1,
|
||||
"Use /agents to browse and switch between your available agents": 2,
|
||||
"In /agents, press Ctrl+S to set the highlighted agent as your default": 1,
|
||||
"Press Shift+Tab to toggle auto-approve mode": 2,
|
||||
"Use --startup-cmd to run a shell command before the first prompt": 1,
|
||||
"Use !! for incognito shell commands that stay out of model context": 1,
|
||||
"Deep Agents can explain its own features and look up its docs. Ask it how to use.": 3, # noqa: E501
|
||||
}
|
||||
"""Tips shown above the chat input. One is chosen at random per launch,
|
||||
weighted by these relative selection weights."""
|
||||
|
||||
# Fail fast at import if the registry is ever emptied or given a non-positive
|
||||
# weight: `random.choices` would otherwise raise a cryptic error at widget
|
||||
# construction. `_TIPS` is a hardcoded constant, so this never fires in
|
||||
# practice — it just guards future edits.
|
||||
if not _TIPS:
|
||||
msg = "_TIPS must not be empty"
|
||||
raise ValueError(msg)
|
||||
if any(weight <= 0 for weight in _TIPS.values()):
|
||||
msg = "_TIPS weights must be positive"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def _pick_tip() -> str:
|
||||
"""Pick one startup tip using the configured relative weights.
|
||||
|
||||
Returns:
|
||||
Tip text selected from `_TIPS`.
|
||||
"""
|
||||
tips = list(_TIPS.keys())
|
||||
weights = list(_TIPS.values())
|
||||
return random.choices(tips, weights=weights, k=1)[0] # noqa: S311
|
||||
|
||||
|
||||
def show_startup_tip() -> bool:
|
||||
"""Return whether startup tips should be shown.
|
||||
|
||||
Returns:
|
||||
`True` when startup tips are enabled for the current process.
|
||||
"""
|
||||
return not is_env_truthy(HIDE_SPLASH_TIPS)
|
||||
|
||||
|
||||
class StartupTip(Static):
|
||||
"""One startup tip displayed above the chat input."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
StartupTip {
|
||||
height: auto;
|
||||
color: $text-muted;
|
||||
text-style: dim italic;
|
||||
padding: 0 1;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, tip: str | None = None, **kwargs: Any) -> None:
|
||||
"""Initialize the startup tip widget.
|
||||
|
||||
Args:
|
||||
tip: Tip text to display. When omitted, one weighted tip is selected.
|
||||
**kwargs: Additional arguments passed to `Static`.
|
||||
"""
|
||||
self.tip: str = tip if tip is not None else _pick_tip()
|
||||
# Styling (dim italic, muted color) is owned by DEFAULT_CSS, which
|
||||
# applies to the whole widget — no need to restyle the spans here.
|
||||
super().__init__(Content.assemble("Tip: ", self.tip), **kwargs)
|
||||
@@ -43,35 +43,6 @@ _ANSI_THEMES: Final[frozenset[str]] = frozenset({"ansi-dark", "ansi-light"})
|
||||
"""Theme names whose color palette is determined by the terminal emulator
|
||||
rather than by the app, so link styles use bold instead of a parsed color."""
|
||||
|
||||
_TIPS: dict[str, int] = {
|
||||
"Use @ to reference files and / for commands": 3,
|
||||
"Try /threads to resume a previous conversation": 2,
|
||||
"Use /offload when your conversation gets long": 2,
|
||||
"Use /copy to copy the latest assistant message": 3,
|
||||
"Use /mcp to search your MCP servers and inspect tool parameters": 1,
|
||||
"Use /mcp login <server> to authenticate MCP OAuth servers without leaving the TUI": 1, # noqa: E501
|
||||
"Use /remember to save learnings from this conversation": 1,
|
||||
"Use /model to switch models mid-conversation": 2,
|
||||
"Use /effort high to change the current model's reasoning effort": 1,
|
||||
"Press ctrl+x to compose prompts in your external editor": 1,
|
||||
"Press ctrl+u to delete to the start of the line in the chat input": 1,
|
||||
"Use /skill:<name> to invoke a skill directly": 1,
|
||||
"Type /update to check for and install updates": 1,
|
||||
"Use /install <extra> to add optional dependencies (e.g. /install daytona)": 1,
|
||||
"Use /theme to customize the TUI's colors": 1,
|
||||
"Use /skill-creator to build reusable agent skills": 1,
|
||||
"Ask for a workflow to fan work out to subagents in parallel": 3,
|
||||
"Use /auto-update to toggle automatic updates": 1,
|
||||
"Use /timestamps to show or hide message timestamp footers": 1,
|
||||
"Use /agents to browse and switch between your available agents": 2,
|
||||
"In /agents, press Ctrl+S to set the highlighted agent as your default": 1,
|
||||
"Press Shift+Tab to toggle auto-approve mode": 2,
|
||||
"Use --startup-cmd to run a shell command before the first prompt": 1,
|
||||
"Use !! for incognito shell commands that stay out of model context": 1,
|
||||
"Deep Agents can explain its own features and look up its docs. Ask it how to use.": 3, # noqa: E501
|
||||
}
|
||||
"""Rotating tips shown in the welcome footer, with relative selection weights."""
|
||||
|
||||
_LANGSMITH_UTM_SOURCE: Final[str] = "deepagents-code"
|
||||
"""UTM source tag appended to LangSmith project URLs in the welcome banner."""
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ from deepagents_code.tui.widgets.messages import (
|
||||
SummarizationMessage,
|
||||
UserMessage,
|
||||
)
|
||||
from deepagents_code.tui.widgets.startup_tip import StartupTip
|
||||
|
||||
|
||||
async def _wait_for_branch(app: DeepAgentsApp, branch: str) -> None:
|
||||
@@ -3190,6 +3191,67 @@ class TestQueuedMessage:
|
||||
class TestMessageQueue:
|
||||
"""Test message queue behavior in DeepAgentsApp."""
|
||||
|
||||
async def test_startup_tip_mounts_above_input(self) -> None:
|
||||
"""The startup tip appears in the bottom container above the input."""
|
||||
app = DeepAgentsApp()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
|
||||
bottom = app.query_one("#bottom-app-container", Container)
|
||||
child_ids = [child.id for child in bottom.children]
|
||||
|
||||
assert "startup-tip" in child_ids
|
||||
assert child_ids.index("subagent-panel") < child_ids.index("startup-tip")
|
||||
assert child_ids.index("startup-tip") < child_ids.index("input-area")
|
||||
|
||||
async def test_startup_tip_respects_hide_env_var(
|
||||
self,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""The bottom startup tip is not mounted when tips are disabled."""
|
||||
from deepagents_code._env_vars import HIDE_SPLASH_TIPS
|
||||
|
||||
monkeypatch.setenv(HIDE_SPLASH_TIPS, "1")
|
||||
app = DeepAgentsApp()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
|
||||
assert not app.query(StartupTip)
|
||||
|
||||
async def test_startup_tip_removed_after_first_submission(self) -> None:
|
||||
"""The startup tip disappears once the first prompt is submitted."""
|
||||
app = DeepAgentsApp()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
assert len(app.query(StartupTip)) == 1
|
||||
# Force the queue path so the real submission flow runs (and
|
||||
# dismisses the tip) without kicking off actual agent work.
|
||||
app._agent_running = True
|
||||
|
||||
app.post_message(ChatInput.Submitted("hello", "normal"))
|
||||
await pilot.pause()
|
||||
|
||||
assert not app.query(StartupTip)
|
||||
assert app._pending_messages[0].text == "hello"
|
||||
|
||||
async def test_startup_tip_removed_on_external_input(self) -> None:
|
||||
"""External prompts dismiss the tip too, via the shared submit path."""
|
||||
app = DeepAgentsApp()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
assert len(app.query(StartupTip)) == 1
|
||||
app._agent_running = True
|
||||
|
||||
app.post_message(
|
||||
ExternalInput(
|
||||
ExternalEvent(kind="prompt", payload="task", source="test")
|
||||
)
|
||||
)
|
||||
await pilot.pause()
|
||||
|
||||
assert not app.query(StartupTip)
|
||||
assert app._pending_messages[0].text == "task"
|
||||
|
||||
async def test_message_queued_when_agent_running(self) -> None:
|
||||
"""Messages should be queued when agent is running."""
|
||||
app = DeepAgentsApp()
|
||||
@@ -19895,6 +19957,7 @@ class TestCanBypassQueue:
|
||||
|
||||
app._process_message = _process # ty: ignore
|
||||
app._mount_message = AsyncMock() # ty: ignore
|
||||
app._dismiss_startup_tip = AsyncMock() # ty: ignore
|
||||
|
||||
await app._submit_input("/install baseten", "command")
|
||||
|
||||
@@ -19913,6 +19976,7 @@ class TestCanBypassQueue:
|
||||
|
||||
app._process_message = _process # ty: ignore
|
||||
app._mount_message = AsyncMock() # ty: ignore
|
||||
app._dismiss_startup_tip = AsyncMock() # ty: ignore
|
||||
|
||||
await app._submit_input("/clear", "command")
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Unit tests for the startup tip widget."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from textual.content import Content
|
||||
|
||||
from deepagents_code._env_vars import HIDE_SPLASH_TIPS
|
||||
from deepagents_code.tui.widgets.startup_tip import (
|
||||
_TIPS,
|
||||
StartupTip,
|
||||
_pick_tip,
|
||||
show_startup_tip,
|
||||
)
|
||||
|
||||
_PICK_TIP = "deepagents_code.tui.widgets.startup_tip._pick_tip"
|
||||
_CHOICES = "deepagents_code.tui.widgets.startup_tip.random.choices"
|
||||
|
||||
|
||||
class TestStartupTip:
|
||||
"""Tests for the bottom startup tip widget."""
|
||||
|
||||
def test_returns_content(self) -> None:
|
||||
"""The widget renders Textual `Content`."""
|
||||
assert isinstance(StartupTip("Use /help").render(), Content)
|
||||
|
||||
def test_renders_tip_text(self) -> None:
|
||||
"""The widget labels and renders the selected tip."""
|
||||
rendered = StartupTip("Use /copy").render()
|
||||
assert isinstance(rendered, Content)
|
||||
assert rendered.plain == "Tip: Use /copy"
|
||||
|
||||
def test_selects_weighted_tip_when_omitted(self) -> None:
|
||||
"""A tip is selected when no explicit text is provided."""
|
||||
with patch(_PICK_TIP, return_value="Use /copy") as pick_tip:
|
||||
widget = StartupTip()
|
||||
|
||||
assert widget.tip == "Use /copy"
|
||||
rendered = widget.render()
|
||||
assert isinstance(rendered, Content)
|
||||
assert rendered.plain == "Tip: Use /copy"
|
||||
pick_tip.assert_called_once()
|
||||
|
||||
def test_pick_tip_returns_registered_tip(self) -> None:
|
||||
"""`_pick_tip` only ever returns a tip drawn from the registry."""
|
||||
for _ in range(100):
|
||||
assert _pick_tip() in _TIPS
|
||||
|
||||
def test_pick_tip_weights_by_registry_values(self) -> None:
|
||||
"""`_pick_tip` passes the registry's relative weights to the draw."""
|
||||
with patch(_CHOICES, return_value=["Use /copy"]) as choices:
|
||||
assert _pick_tip() == "Use /copy"
|
||||
|
||||
choices.assert_called_once()
|
||||
_, kwargs = choices.call_args
|
||||
assert kwargs["weights"] == list(_TIPS.values())
|
||||
|
||||
def test_show_startup_tip_defaults_to_true(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Startup tips are visible by default."""
|
||||
monkeypatch.delenv(HIDE_SPLASH_TIPS, raising=False)
|
||||
|
||||
assert show_startup_tip() is True
|
||||
|
||||
def test_hide_splash_tips_env_var_hides_tip(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""`HIDE_SPLASH_TIPS` disables the bottom tip widget."""
|
||||
monkeypatch.setenv(HIDE_SPLASH_TIPS, "1")
|
||||
|
||||
assert show_startup_tip() is False
|
||||
|
||||
def test_startup_cmd_tip_registered(self) -> None:
|
||||
"""The `--startup-cmd` flag keeps a discoverability tip."""
|
||||
assert any("--startup-cmd" in tip for tip in _TIPS)
|
||||
|
||||
def test_incognito_shell_tip_registered(self) -> None:
|
||||
"""The `!!` shell mode keeps a discoverability tip."""
|
||||
assert any("!!" in tip and "incognito" in tip.lower() for tip in _TIPS)
|
||||
|
||||
def test_copy_command_tip_registered(self) -> None:
|
||||
"""The `/copy` command keeps a discoverability tip."""
|
||||
assert any("/copy" in tip for tip in _TIPS)
|
||||
|
||||
def test_workflow_subagent_tip_registered(self) -> None:
|
||||
"""The workflow trigger phrase keeps an above-baseline weight."""
|
||||
tip = "Ask for a workflow to fan work out to subagents in parallel"
|
||||
assert _TIPS[tip] > 1
|
||||
@@ -621,7 +621,7 @@ class TestRemovedSections:
|
||||
def test_no_legacy_sections(self) -> None:
|
||||
"""None of the old splash footer sections appear."""
|
||||
plain = _make_banner()._build_banner().plain
|
||||
for absent in ("Ready to code", "Tip:"):
|
||||
for absent in ("Ready to code", "Tip:", "tip:"):
|
||||
assert absent not in plain
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user