claudechic 0.2.2__py3-none-any.whl → 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- claudechic/__init__.py +3 -1
- claudechic/__main__.py +12 -1
- claudechic/agent.py +60 -19
- claudechic/agent_manager.py +8 -2
- claudechic/analytics.py +62 -0
- claudechic/app.py +267 -158
- claudechic/commands.py +120 -6
- claudechic/config.py +80 -0
- claudechic/features/worktree/commands.py +70 -1
- claudechic/help_data.py +200 -0
- claudechic/messages.py +0 -17
- claudechic/processes.py +120 -0
- claudechic/profiling.py +18 -1
- claudechic/protocols.py +1 -1
- claudechic/remote.py +249 -0
- claudechic/sessions.py +60 -50
- claudechic/styles.tcss +19 -18
- claudechic/widgets/__init__.py +112 -41
- claudechic/widgets/base/__init__.py +20 -0
- claudechic/widgets/base/clickable.py +23 -0
- claudechic/widgets/base/copyable.py +55 -0
- claudechic/{cursor.py → widgets/base/cursor.py} +9 -28
- claudechic/widgets/base/tool_protocol.py +30 -0
- claudechic/widgets/content/__init__.py +41 -0
- claudechic/widgets/{diff.py → content/diff.py} +11 -65
- claudechic/widgets/{chat.py → content/message.py} +25 -76
- claudechic/widgets/{tools.py → content/tools.py} +12 -24
- claudechic/widgets/input/__init__.py +9 -0
- claudechic/widgets/layout/__init__.py +51 -0
- claudechic/widgets/{chat_view.py → layout/chat_view.py} +92 -43
- claudechic/widgets/{footer.py → layout/footer.py} +17 -7
- claudechic/widgets/{indicators.py → layout/indicators.py} +55 -7
- claudechic/widgets/layout/processes.py +68 -0
- claudechic/widgets/{agents.py → layout/sidebar.py} +163 -82
- claudechic/widgets/modals/__init__.py +9 -0
- claudechic/widgets/modals/process_modal.py +121 -0
- claudechic/widgets/{profile_modal.py → modals/profile.py} +2 -1
- claudechic/widgets/primitives/__init__.py +13 -0
- claudechic/widgets/{button.py → primitives/button.py} +1 -1
- claudechic/widgets/{collapsible.py → primitives/collapsible.py} +5 -1
- claudechic/widgets/{scroll.py → primitives/scroll.py} +2 -0
- claudechic/widgets/primitives/spinner.py +57 -0
- claudechic/widgets/prompts.py +146 -17
- claudechic/widgets/reports/__init__.py +10 -0
- claudechic-0.3.1.dist-info/METADATA +88 -0
- claudechic-0.3.1.dist-info/RECORD +71 -0
- {claudechic-0.2.2.dist-info → claudechic-0.3.1.dist-info}/WHEEL +1 -1
- claudechic-0.3.1.dist-info/licenses/LICENSE +21 -0
- claudechic/features/worktree/prompts.py +0 -101
- claudechic/widgets/model_prompt.py +0 -56
- claudechic-0.2.2.dist-info/METADATA +0 -58
- claudechic-0.2.2.dist-info/RECORD +0 -54
- /claudechic/widgets/{todo.py → content/todo.py} +0 -0
- /claudechic/widgets/{autocomplete.py → input/autocomplete.py} +0 -0
- /claudechic/widgets/{history_search.py → input/history_search.py} +0 -0
- /claudechic/widgets/{context_report.py → reports/context.py} +0 -0
- /claudechic/widgets/{usage.py → reports/usage.py} +0 -0
- {claudechic-0.2.2.dist-info → claudechic-0.3.1.dist-info}/entry_points.txt +0 -0
- {claudechic-0.2.2.dist-info → claudechic-0.3.1.dist-info}/top_level.txt +0 -0
claudechic/app.py
CHANGED
|
@@ -30,7 +30,6 @@ from claude_agent_sdk import (
|
|
|
30
30
|
ResultMessage,
|
|
31
31
|
)
|
|
32
32
|
from claudechic.messages import (
|
|
33
|
-
StreamChunk,
|
|
34
33
|
ResponseComplete,
|
|
35
34
|
SystemNotification,
|
|
36
35
|
ToolUseMessage,
|
|
@@ -48,6 +47,8 @@ from claudechic.features.worktree.commands import on_response_complete_finish
|
|
|
48
47
|
from claudechic.permissions import PermissionRequest, PermissionResponse
|
|
49
48
|
from claudechic.agent import Agent, ImageAttachment, ToolUse
|
|
50
49
|
from claudechic.agent_manager import AgentManager
|
|
50
|
+
from claudechic.analytics import capture
|
|
51
|
+
from claudechic.config import get_theme, set_theme, is_new_install
|
|
51
52
|
from claudechic.enums import AgentStatus, PermissionChoice, ToolName
|
|
52
53
|
from claudechic.mcp import set_app, create_chic_server
|
|
53
54
|
from claudechic.file_index import FileIndex
|
|
@@ -61,21 +62,23 @@ from claudechic.widgets import (
|
|
|
61
62
|
AgentToolWidget,
|
|
62
63
|
TodoWidget,
|
|
63
64
|
TodoPanel,
|
|
65
|
+
ProcessPanel,
|
|
64
66
|
SelectionPrompt,
|
|
65
67
|
QuestionPrompt,
|
|
66
68
|
SessionItem,
|
|
67
69
|
TextAreaAutoComplete,
|
|
68
70
|
HistorySearch,
|
|
69
|
-
|
|
71
|
+
AgentSection,
|
|
70
72
|
AgentItem,
|
|
71
73
|
WorktreeItem,
|
|
72
74
|
ChatView,
|
|
73
|
-
|
|
75
|
+
PlanItem,
|
|
76
|
+
PlanSection,
|
|
74
77
|
HamburgerButton,
|
|
75
78
|
EditPlanRequested,
|
|
76
79
|
)
|
|
77
|
-
from claudechic.widgets.footer import AutoEditLabel, ModelLabel, StatusFooter
|
|
78
|
-
from claudechic.widgets.
|
|
80
|
+
from claudechic.widgets.layout.footer import AutoEditLabel, ModelLabel, StatusFooter
|
|
81
|
+
from claudechic.widgets.prompts import ModelPrompt
|
|
79
82
|
from claudechic.errors import setup_logging # noqa: F401 - used at startup
|
|
80
83
|
from claudechic.profiling import profile
|
|
81
84
|
from claudechic.sampling import start_sampler
|
|
@@ -125,7 +128,6 @@ class ChatApp(App):
|
|
|
125
128
|
"shift+tab", "cycle_permission_mode", "Auto-edit", priority=True, show=False
|
|
126
129
|
),
|
|
127
130
|
Binding("escape", "escape", "Cancel", show=False),
|
|
128
|
-
Binding("ctrl+n", "new_agent", "New Agent", priority=True, show=False),
|
|
129
131
|
Binding("ctrl+r", "history_search", "History", priority=True, show=False),
|
|
130
132
|
# Agent switching: ctrl+1 through ctrl+9
|
|
131
133
|
*[
|
|
@@ -148,23 +150,32 @@ class ChatApp(App):
|
|
|
148
150
|
CENTERED_SIDEBAR_WIDTH = 140 # Above this, center chat while showing sidebar
|
|
149
151
|
|
|
150
152
|
def __init__(
|
|
151
|
-
self,
|
|
153
|
+
self,
|
|
154
|
+
resume_session_id: str | None = None,
|
|
155
|
+
initial_prompt: str | None = None,
|
|
156
|
+
remote_port: int = 0,
|
|
152
157
|
) -> None:
|
|
153
158
|
super().__init__()
|
|
159
|
+
self.scroll_sensitivity_y = 1.0 # Smoother scrolling (default is 2.0)
|
|
154
160
|
# AgentManager is the single source of truth for agents
|
|
155
161
|
self.agent_mgr: AgentManager | None = None
|
|
156
162
|
|
|
157
163
|
self._resume_on_start = resume_session_id
|
|
158
164
|
self._initial_prompt = initial_prompt
|
|
165
|
+
self._remote_port = remote_port
|
|
159
166
|
self._session_picker_active = False
|
|
160
167
|
# Event queues for testing
|
|
161
168
|
self.interactions: asyncio.Queue[PermissionRequest] = asyncio.Queue()
|
|
162
169
|
self.completions: asyncio.Queue[ResponseComplete] = asyncio.Queue()
|
|
170
|
+
# Permission UI serialization - only show one prompt at a time
|
|
171
|
+
self._permission_lock = asyncio.Lock()
|
|
163
172
|
# File index for fuzzy file search
|
|
164
173
|
self.file_index: FileIndex | None = None
|
|
165
174
|
# Cached widget references (initialized lazily)
|
|
166
|
-
self.
|
|
175
|
+
self._agent_section: AgentSection | None = None
|
|
176
|
+
self._plan_section: PlanSection | None = None
|
|
167
177
|
self._todo_panel: TodoPanel | None = None
|
|
178
|
+
self._process_panel: ProcessPanel | None = None
|
|
168
179
|
self._context_bar: ContextBar | None = None
|
|
169
180
|
self._right_sidebar: Vertical | None = None
|
|
170
181
|
self._input_container: Vertical | None = None
|
|
@@ -174,14 +185,15 @@ class ChatApp(App):
|
|
|
174
185
|
self._shell_process: asyncio.subprocess.Process | None = None
|
|
175
186
|
# Agent-to-UI mappings (Agent has no UI references)
|
|
176
187
|
self._chat_views: dict[str, ChatView] = {} # agent_id -> ChatView
|
|
188
|
+
self._agent_metadata: dict[
|
|
189
|
+
str, dict
|
|
190
|
+
] = {} # agent_id -> {created_at, same_directory}
|
|
177
191
|
self._active_prompts: dict[
|
|
178
192
|
str, Any
|
|
179
193
|
] = {} # agent_id -> SelectionPrompt/QuestionPrompt
|
|
180
194
|
# Sidebar overlay state (for narrow screens)
|
|
181
195
|
self._sidebar_overlay_open = False
|
|
182
196
|
self._hamburger_btn: HamburgerButton | None = None
|
|
183
|
-
# Selected model (None = SDK default)
|
|
184
|
-
self.selected_model: str | None = None
|
|
185
197
|
# Available models from SDK (populated in _update_slash_commands)
|
|
186
198
|
self._available_models: list[dict] = []
|
|
187
199
|
|
|
@@ -255,10 +267,16 @@ class ChatApp(App):
|
|
|
255
267
|
|
|
256
268
|
# Cached widget accessors (lazy init on first access)
|
|
257
269
|
@property
|
|
258
|
-
def
|
|
259
|
-
if self.
|
|
260
|
-
self.
|
|
261
|
-
return self.
|
|
270
|
+
def agent_section(self) -> AgentSection:
|
|
271
|
+
if self._agent_section is None:
|
|
272
|
+
self._agent_section = self.query_one("#agent-section", AgentSection)
|
|
273
|
+
return self._agent_section
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def plan_section(self) -> PlanSection:
|
|
277
|
+
if self._plan_section is None:
|
|
278
|
+
self._plan_section = self.query_one("#plan-section", PlanSection)
|
|
279
|
+
return self._plan_section
|
|
262
280
|
|
|
263
281
|
@property
|
|
264
282
|
def todo_panel(self) -> TodoPanel:
|
|
@@ -266,6 +284,12 @@ class ChatApp(App):
|
|
|
266
284
|
self._todo_panel = self.query_one("#todo-panel", TodoPanel)
|
|
267
285
|
return self._todo_panel
|
|
268
286
|
|
|
287
|
+
@property
|
|
288
|
+
def process_panel(self) -> ProcessPanel:
|
|
289
|
+
if self._process_panel is None:
|
|
290
|
+
self._process_panel = self.query_one("#process-panel", ProcessPanel)
|
|
291
|
+
return self._process_panel
|
|
292
|
+
|
|
269
293
|
@property
|
|
270
294
|
def context_bar(self) -> ContextBar:
|
|
271
295
|
if self._context_bar is None:
|
|
@@ -311,7 +335,7 @@ class ChatApp(App):
|
|
|
311
335
|
return
|
|
312
336
|
agent.status = status
|
|
313
337
|
try:
|
|
314
|
-
self.
|
|
338
|
+
self.agent_section.update_status(agent.id, status)
|
|
315
339
|
except Exception:
|
|
316
340
|
pass # Sidebar not mounted yet
|
|
317
341
|
|
|
@@ -434,7 +458,9 @@ class ChatApp(App):
|
|
|
434
458
|
"/compactish",
|
|
435
459
|
"/usage",
|
|
436
460
|
"/model",
|
|
461
|
+
"/processes",
|
|
437
462
|
"/welcome",
|
|
463
|
+
"/help",
|
|
438
464
|
]
|
|
439
465
|
|
|
440
466
|
def compose(self) -> ComposeResult:
|
|
@@ -443,8 +469,10 @@ class ChatApp(App):
|
|
|
443
469
|
yield ListView(id="session-picker", classes="hidden")
|
|
444
470
|
yield ChatView(id="chat-view")
|
|
445
471
|
with Vertical(id="right-sidebar", classes="hidden"):
|
|
446
|
-
yield
|
|
472
|
+
yield AgentSection(id="agent-section")
|
|
473
|
+
yield PlanSection(id="plan-section", classes="hidden")
|
|
447
474
|
yield TodoPanel(id="todo-panel")
|
|
475
|
+
yield ProcessPanel(id="process-panel", classes="hidden")
|
|
448
476
|
with Horizontal(id="input-wrapper"):
|
|
449
477
|
with Vertical(id="input-container"):
|
|
450
478
|
yield ImageAttachments(id="image-attachments", classes="hidden")
|
|
@@ -468,6 +496,7 @@ class ChatApp(App):
|
|
|
468
496
|
cwd: Path | None = None,
|
|
469
497
|
resume: str | None = None,
|
|
470
498
|
agent_name: str | None = None,
|
|
499
|
+
model: str | None = None,
|
|
471
500
|
) -> ClaudeAgentOptions:
|
|
472
501
|
"""Create SDK options with common settings.
|
|
473
502
|
|
|
@@ -480,22 +509,37 @@ class ChatApp(App):
|
|
|
480
509
|
setting_sources=["user", "project", "local"],
|
|
481
510
|
cwd=cwd,
|
|
482
511
|
resume=resume,
|
|
483
|
-
model=
|
|
512
|
+
model=model,
|
|
484
513
|
mcp_servers={"chic": create_chic_server(caller_name=agent_name)},
|
|
485
514
|
include_partial_messages=True,
|
|
486
515
|
stderr=self._handle_sdk_stderr,
|
|
487
516
|
)
|
|
488
517
|
|
|
489
518
|
async def on_mount(self) -> None:
|
|
519
|
+
# Track app start (and install if new user)
|
|
520
|
+
self._app_start_time = time.time()
|
|
521
|
+
if is_new_install():
|
|
522
|
+
self.run_worker(capture("app_installed"))
|
|
523
|
+
self.run_worker(capture("app_started", resumed=bool(self._resume_on_start)))
|
|
524
|
+
|
|
490
525
|
# Start CPU sampling profiler
|
|
491
526
|
start_sampler()
|
|
492
527
|
|
|
528
|
+
# Start background process polling
|
|
529
|
+
self.set_interval(2.0, self._poll_background_processes)
|
|
530
|
+
|
|
493
531
|
# Register app for MCP tools
|
|
494
532
|
set_app(self)
|
|
495
533
|
|
|
496
|
-
#
|
|
534
|
+
# Start remote control server if requested
|
|
535
|
+
if self._remote_port:
|
|
536
|
+
from claudechic.remote import start_server
|
|
537
|
+
|
|
538
|
+
await start_server(self, self._remote_port)
|
|
539
|
+
|
|
540
|
+
# Register and activate custom theme (use saved preference or default to chic)
|
|
497
541
|
self.register_theme(CHIC_THEME)
|
|
498
|
-
self.theme = "chic"
|
|
542
|
+
self.theme = get_theme() or "chic"
|
|
499
543
|
|
|
500
544
|
# Initialize AgentManager (new architecture)
|
|
501
545
|
self.agent_mgr = AgentManager(self._make_options)
|
|
@@ -518,6 +562,10 @@ class ChatApp(App):
|
|
|
518
562
|
# Connect SDK in background - UI renders while this happens
|
|
519
563
|
self._connect_initial_client()
|
|
520
564
|
|
|
565
|
+
def watch_theme(self, theme: str) -> None:
|
|
566
|
+
"""Save theme preference when changed."""
|
|
567
|
+
set_theme(theme)
|
|
568
|
+
|
|
521
569
|
@work(exclusive=True, group="connect")
|
|
522
570
|
async def _connect_initial_client(self) -> None:
|
|
523
571
|
"""Connect SDK for the initial agent."""
|
|
@@ -537,7 +585,7 @@ class ChatApp(App):
|
|
|
537
585
|
|
|
538
586
|
# Connect the agent to SDK
|
|
539
587
|
options = self._make_options(
|
|
540
|
-
cwd=agent.cwd, resume=resume, agent_name=agent.name
|
|
588
|
+
cwd=agent.cwd, resume=resume, agent_name=agent.name, model=agent.model
|
|
541
589
|
)
|
|
542
590
|
try:
|
|
543
591
|
await agent.connect(options, resume=resume)
|
|
@@ -575,27 +623,9 @@ class ChatApp(App):
|
|
|
575
623
|
models = info["models"]
|
|
576
624
|
if isinstance(models, list) and models:
|
|
577
625
|
self._available_models = models
|
|
578
|
-
#
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
# If user selected a model, find it
|
|
582
|
-
if (
|
|
583
|
-
self.selected_model
|
|
584
|
-
and m.get("value") == self.selected_model
|
|
585
|
-
):
|
|
586
|
-
active = m
|
|
587
|
-
break
|
|
588
|
-
# Otherwise use the one marked 'default' by SDK
|
|
589
|
-
if m.get("value") == "default":
|
|
590
|
-
active = m
|
|
591
|
-
# Extract short name from description like "Opus 4.5 · ..."
|
|
592
|
-
desc = active.get("description", "")
|
|
593
|
-
model_name = (
|
|
594
|
-
desc.split("·")[0].strip()
|
|
595
|
-
if "·" in desc
|
|
596
|
-
else active.get("displayName", "")
|
|
597
|
-
)
|
|
598
|
-
self.status_footer.model = model_name
|
|
626
|
+
# Update footer with current agent's model
|
|
627
|
+
agent = self._agent
|
|
628
|
+
self._update_footer_model(agent.model if agent else None)
|
|
599
629
|
except Exception as e:
|
|
600
630
|
log.warning(f"Failed to fetch SDK commands: {e}")
|
|
601
631
|
self.refresh_context()
|
|
@@ -606,6 +636,15 @@ class ChatApp(App):
|
|
|
606
636
|
if self.file_index:
|
|
607
637
|
await self.file_index.refresh()
|
|
608
638
|
|
|
639
|
+
def _poll_background_processes(self) -> None:
|
|
640
|
+
"""Poll for background processes and update the panel and footer."""
|
|
641
|
+
agent = self._agent
|
|
642
|
+
if not agent:
|
|
643
|
+
return
|
|
644
|
+
processes = agent.get_background_processes()
|
|
645
|
+
self.process_panel.update_processes(processes)
|
|
646
|
+
self.status_footer.update_processes(processes)
|
|
647
|
+
|
|
609
648
|
async def _load_and_display_history(
|
|
610
649
|
self, session_id: str, cwd: Path | None = None
|
|
611
650
|
) -> None:
|
|
@@ -619,7 +658,7 @@ class ChatApp(App):
|
|
|
619
658
|
|
|
620
659
|
# Set session_id and load history
|
|
621
660
|
agent.session_id = session_id
|
|
622
|
-
await agent.load_history(
|
|
661
|
+
await agent.load_history(cwd=cwd)
|
|
623
662
|
|
|
624
663
|
# Re-render ChatView from Agent.messages
|
|
625
664
|
chat_view = self._chat_views.get(agent.id)
|
|
@@ -726,14 +765,6 @@ class ChatApp(App):
|
|
|
726
765
|
except Exception:
|
|
727
766
|
pass # OK to fail during shutdown
|
|
728
767
|
|
|
729
|
-
@profile
|
|
730
|
-
def on_stream_chunk(self, event: StreamChunk) -> None:
|
|
731
|
-
chat_view = self._get_chat_view(event.agent_id)
|
|
732
|
-
if not chat_view:
|
|
733
|
-
return
|
|
734
|
-
|
|
735
|
-
chat_view.append_text(event.text, event.new_message, event.parent_tool_use_id)
|
|
736
|
-
|
|
737
768
|
@profile
|
|
738
769
|
def on_tool_use_message(self, event: ToolUseMessage) -> None:
|
|
739
770
|
agent = self._get_agent(event.agent_id)
|
|
@@ -741,21 +772,6 @@ class ChatApp(App):
|
|
|
741
772
|
if not agent or not chat_view:
|
|
742
773
|
return
|
|
743
774
|
|
|
744
|
-
# TodoWrite gets special handling - update sidebar panel and/or inline widget
|
|
745
|
-
if event.block.name == "TodoWrite":
|
|
746
|
-
todos = event.block.input.get("todos", [])
|
|
747
|
-
agent.todos = todos # Store on agent for switching
|
|
748
|
-
self.todo_panel.update_todos(todos)
|
|
749
|
-
self._position_right_sidebar()
|
|
750
|
-
# Also update inline widget if exists, or create if narrow
|
|
751
|
-
existing = self.query(TodoWidget)
|
|
752
|
-
if existing:
|
|
753
|
-
existing[0].update_todos(todos)
|
|
754
|
-
elif self.size.width < self.SIDEBAR_MIN_WIDTH:
|
|
755
|
-
chat_view.mount(TodoWidget(todos))
|
|
756
|
-
self._show_thinking(event.agent_id)
|
|
757
|
-
return
|
|
758
|
-
|
|
759
775
|
# Create ToolUse data object for ChatView
|
|
760
776
|
tool = ToolUse(
|
|
761
777
|
id=event.block.id, name=event.block.name, input=event.block.input
|
|
@@ -850,7 +866,7 @@ class ChatApp(App):
|
|
|
850
866
|
# Show sidebar when wide enough and we have multiple agents, worktrees, or todos
|
|
851
867
|
agent_count = len(self.agent_mgr) if self.agent_mgr else 0
|
|
852
868
|
has_content = (
|
|
853
|
-
agent_count > 1 or self.
|
|
869
|
+
agent_count > 1 or self.agent_section._worktrees or self.todo_panel.todos
|
|
854
870
|
)
|
|
855
871
|
width = self.size.width
|
|
856
872
|
main = self.query_one("#main", Horizontal)
|
|
@@ -949,7 +965,7 @@ class ChatApp(App):
|
|
|
949
965
|
|
|
950
966
|
# Use custom widget for context reports
|
|
951
967
|
if "## Context Usage" in event.content:
|
|
952
|
-
from claudechic.widgets.
|
|
968
|
+
from claudechic.widgets.reports.context import ContextReport
|
|
953
969
|
|
|
954
970
|
widget = ContextReport(event.content)
|
|
955
971
|
else:
|
|
@@ -1080,15 +1096,19 @@ class ChatApp(App):
|
|
|
1080
1096
|
|
|
1081
1097
|
async def _cleanup_and_exit(self) -> None:
|
|
1082
1098
|
"""Disconnect all agents and exit."""
|
|
1099
|
+
# Track app close with session duration
|
|
1100
|
+
duration = time.time() - getattr(self, "_app_start_time", time.time())
|
|
1101
|
+
await capture("app_closed", duration_seconds=int(duration))
|
|
1102
|
+
|
|
1083
1103
|
for agent in self.agents.values():
|
|
1084
1104
|
if agent.client:
|
|
1085
1105
|
try:
|
|
1086
|
-
|
|
1106
|
+
# disconnect() terminates the subprocess and waits for it to finish,
|
|
1107
|
+
# allowing it to flush session files before we exit
|
|
1108
|
+
await agent.client.disconnect()
|
|
1087
1109
|
except Exception:
|
|
1088
1110
|
pass # Best-effort cleanup during shutdown
|
|
1089
1111
|
agent.client = None
|
|
1090
|
-
# Brief delay to let SDK hooks complete before stream closes
|
|
1091
|
-
await asyncio.sleep(0.1)
|
|
1092
1112
|
# Suppress SDK stderr noise during exit (stream closed errors)
|
|
1093
1113
|
sys.stderr = open(os.devnull, "w")
|
|
1094
1114
|
self.exit()
|
|
@@ -1099,7 +1119,7 @@ class ChatApp(App):
|
|
|
1099
1119
|
"""Run a shell command async with PTY for color support."""
|
|
1100
1120
|
from claudechic.shell_runner import run_in_pty
|
|
1101
1121
|
from claudechic.widgets import ShellOutputWidget
|
|
1102
|
-
from claudechic.widgets.
|
|
1122
|
+
from claudechic.widgets.content.message import Spinner
|
|
1103
1123
|
|
|
1104
1124
|
chat_view = self._chat_view
|
|
1105
1125
|
if not chat_view:
|
|
@@ -1166,8 +1186,8 @@ class ChatApp(App):
|
|
|
1166
1186
|
picker = self.query_one("#session-picker", ListView)
|
|
1167
1187
|
picker.clear()
|
|
1168
1188
|
sessions = await get_recent_sessions(search=search)
|
|
1169
|
-
for session_id,
|
|
1170
|
-
picker.append(SessionItem(session_id,
|
|
1189
|
+
for session_id, title, mtime, msg_count in sessions:
|
|
1190
|
+
picker.append(SessionItem(session_id, title, mtime, msg_count))
|
|
1171
1191
|
# Select first item and focus for keyboard nav
|
|
1172
1192
|
if sessions:
|
|
1173
1193
|
self.call_after_refresh(
|
|
@@ -1195,7 +1215,12 @@ class ChatApp(App):
|
|
|
1195
1215
|
resume_id = sessions[0][0] if sessions else None
|
|
1196
1216
|
|
|
1197
1217
|
await self._replace_client(
|
|
1198
|
-
self._make_options(
|
|
1218
|
+
self._make_options(
|
|
1219
|
+
cwd=new_cwd,
|
|
1220
|
+
resume=resume_id,
|
|
1221
|
+
agent_name=agent.name,
|
|
1222
|
+
model=agent.model,
|
|
1223
|
+
)
|
|
1199
1224
|
)
|
|
1200
1225
|
|
|
1201
1226
|
# Clear ChatView state
|
|
@@ -1222,7 +1247,7 @@ class ChatApp(App):
|
|
|
1222
1247
|
agent.plan_path = plan_path
|
|
1223
1248
|
# Update sidebar if this is still the active agent
|
|
1224
1249
|
if self._agent and self._agent.id == agent.id:
|
|
1225
|
-
self.
|
|
1250
|
+
self.plan_section.set_plan(plan_path)
|
|
1226
1251
|
|
|
1227
1252
|
def action_escape(self) -> None:
|
|
1228
1253
|
"""Handle Escape: cancel picker, dismiss prompts, close overlay, or interrupt agent."""
|
|
@@ -1288,22 +1313,26 @@ class ChatApp(App):
|
|
|
1288
1313
|
event.branch, event.path, worktree=event.branch, auto_resume=True
|
|
1289
1314
|
)
|
|
1290
1315
|
|
|
1291
|
-
def
|
|
1292
|
-
"""Handle plan
|
|
1316
|
+
def on_plan_item_plan_requested(self, event: PlanItem.PlanRequested) -> None:
|
|
1317
|
+
"""Handle plan item click - open plan file in editor."""
|
|
1293
1318
|
editor = os.environ.get("EDITOR", "vi")
|
|
1294
1319
|
handle_command(self, f"/shell -i {editor} {event.plan_path}")
|
|
1295
1320
|
|
|
1296
|
-
def
|
|
1297
|
-
|
|
1321
|
+
def on_hamburger_button_sidebar_toggled(
|
|
1322
|
+
self, event: HamburgerButton.SidebarToggled
|
|
1323
|
+
) -> None:
|
|
1324
|
+
"""Handle hamburger button press - toggle sidebar overlay."""
|
|
1298
1325
|
self._sidebar_overlay_open = not self._sidebar_overlay_open
|
|
1299
1326
|
self._position_right_sidebar()
|
|
1300
1327
|
|
|
1301
1328
|
def on_auto_edit_label_toggled(self, event: AutoEditLabel.Toggled) -> None:
|
|
1302
|
-
"""Handle auto-edit label
|
|
1329
|
+
"""Handle auto-edit label press - toggle auto-edit mode."""
|
|
1303
1330
|
self.action_cycle_permission_mode()
|
|
1304
1331
|
|
|
1305
|
-
def
|
|
1306
|
-
|
|
1332
|
+
def on_model_label_model_change_requested(
|
|
1333
|
+
self, event: ModelLabel.ModelChangeRequested
|
|
1334
|
+
) -> None:
|
|
1335
|
+
"""Handle model label press - open model selector."""
|
|
1307
1336
|
self._handle_model_prompt()
|
|
1308
1337
|
|
|
1309
1338
|
def _close_sidebar_overlay(self) -> None:
|
|
@@ -1343,7 +1372,7 @@ class ChatApp(App):
|
|
|
1343
1372
|
continue # Skip main worktree
|
|
1344
1373
|
if wt.branch in agent_names:
|
|
1345
1374
|
continue # Already have an agent
|
|
1346
|
-
self.
|
|
1375
|
+
self.agent_section.add_worktree(wt.branch, wt.path)
|
|
1347
1376
|
|
|
1348
1377
|
def on_agent_item_close_requested(self, event: AgentItem.CloseRequested) -> None:
|
|
1349
1378
|
"""Handle close button click on agent item."""
|
|
@@ -1357,52 +1386,58 @@ class ChatApp(App):
|
|
|
1357
1386
|
if agent_id not in self.agents:
|
|
1358
1387
|
return
|
|
1359
1388
|
old_agent = self._agent
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
if
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
self.
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1389
|
+
|
|
1390
|
+
# Batch all class changes to trigger single CSS recalculation
|
|
1391
|
+
with self.batch_update():
|
|
1392
|
+
# Save current input and hide old agent's UI
|
|
1393
|
+
if old_agent:
|
|
1394
|
+
old_agent.pending_input = self.chat_input.text
|
|
1395
|
+
old_chat_view = self._chat_views.get(old_agent.id)
|
|
1396
|
+
if old_chat_view:
|
|
1397
|
+
old_chat_view.add_class("hidden")
|
|
1398
|
+
old_prompt = self._active_prompts.get(old_agent.id)
|
|
1399
|
+
if old_prompt:
|
|
1400
|
+
old_prompt.add_class("hidden")
|
|
1401
|
+
# Switch active agent (setter syncs to AgentManager)
|
|
1402
|
+
self.active_agent_id = agent_id
|
|
1403
|
+
agent = self._agent
|
|
1404
|
+
chat_view = self._chat_views.get(agent_id)
|
|
1405
|
+
if chat_view:
|
|
1406
|
+
chat_view.remove_class("hidden")
|
|
1407
|
+
# Restore new agent's input
|
|
1408
|
+
if agent:
|
|
1409
|
+
self.chat_input.text = agent.pending_input
|
|
1410
|
+
# Show new agent's prompt if it has one, otherwise show input
|
|
1411
|
+
active_prompt = self._active_prompts.get(agent_id)
|
|
1412
|
+
if active_prompt:
|
|
1413
|
+
active_prompt.remove_class("hidden")
|
|
1414
|
+
self.input_container.add_class("hidden")
|
|
1415
|
+
else:
|
|
1416
|
+
self.input_container.remove_class("hidden")
|
|
1417
|
+
# Update sidebar selection
|
|
1418
|
+
self.agent_section.set_active(agent_id)
|
|
1419
|
+
# Update footer
|
|
1420
|
+
self.status_footer.auto_edit = agent.auto_approve_edits if agent else False
|
|
1421
|
+
self._update_footer_model(agent.model if agent else None)
|
|
1422
|
+
# Update todo panel for new agent
|
|
1423
|
+
self.todo_panel.update_todos(agent.todos if agent else [])
|
|
1424
|
+
# Update context bar for new agent
|
|
1425
|
+
self.refresh_context()
|
|
1426
|
+
# Update plan button for new agent (use cached plan_path)
|
|
1427
|
+
self.plan_section.set_plan(agent.plan_path if agent else None)
|
|
1428
|
+
self._position_right_sidebar()
|
|
1429
|
+
|
|
1430
|
+
# These happen outside batch (async/focus)
|
|
1388
1431
|
asyncio.create_task(
|
|
1389
1432
|
self.status_footer.refresh_branch(str(agent.cwd) if agent else None)
|
|
1390
1433
|
)
|
|
1391
|
-
self.status_footer.auto_edit = agent.auto_approve_edits if agent else False
|
|
1392
|
-
# Update todo panel for new agent
|
|
1393
|
-
self.todo_panel.update_todos(agent.todos if agent else [])
|
|
1394
|
-
# Update context bar for new agent
|
|
1395
|
-
self.refresh_context()
|
|
1396
|
-
# Update plan button for new agent (use cached plan_path)
|
|
1397
|
-
self.agent_sidebar.set_plan(agent.plan_path if agent else None)
|
|
1398
|
-
self._position_right_sidebar()
|
|
1399
1434
|
self.chat_input.focus()
|
|
1400
1435
|
|
|
1401
1436
|
async def _reconnect_agent(self, agent: "Agent", session_id: str) -> None:
|
|
1402
1437
|
"""Disconnect and reconnect an agent to reload its session."""
|
|
1403
1438
|
await agent.disconnect()
|
|
1404
1439
|
options = self._make_options(
|
|
1405
|
-
cwd=agent.cwd, resume=session_id, agent_name=agent.name
|
|
1440
|
+
cwd=agent.cwd, resume=session_id, agent_name=agent.name, model=agent.model
|
|
1406
1441
|
)
|
|
1407
1442
|
await agent.connect(options, resume=session_id)
|
|
1408
1443
|
|
|
@@ -1416,7 +1451,9 @@ class ChatApp(App):
|
|
|
1416
1451
|
if chat_view:
|
|
1417
1452
|
chat_view.clear()
|
|
1418
1453
|
await agent.disconnect()
|
|
1419
|
-
options = self._make_options(
|
|
1454
|
+
options = self._make_options(
|
|
1455
|
+
cwd=agent.cwd, agent_name=agent.name, model=agent.model
|
|
1456
|
+
)
|
|
1420
1457
|
await agent.connect(options)
|
|
1421
1458
|
self.refresh_context()
|
|
1422
1459
|
self.notify("New session started")
|
|
@@ -1425,7 +1462,7 @@ class ChatApp(App):
|
|
|
1425
1462
|
async def _handle_usage_command(self) -> None:
|
|
1426
1463
|
"""Handle /usage command - show API usage limits."""
|
|
1427
1464
|
from claudechic.usage import fetch_usage
|
|
1428
|
-
from claudechic.widgets.usage import UsageReport
|
|
1465
|
+
from claudechic.widgets.reports.usage import UsageReport
|
|
1429
1466
|
|
|
1430
1467
|
chat_view = self._chat_view
|
|
1431
1468
|
if not chat_view:
|
|
@@ -1436,16 +1473,40 @@ class ChatApp(App):
|
|
|
1436
1473
|
chat_view.mount(widget)
|
|
1437
1474
|
chat_view.scroll_if_tailing()
|
|
1438
1475
|
|
|
1476
|
+
@work(group="model_switch", exclusive=True, exit_on_error=False)
|
|
1477
|
+
async def _set_agent_model(self, model: str) -> None:
|
|
1478
|
+
"""Set model for active agent and reconnect."""
|
|
1479
|
+
agent = self._agent
|
|
1480
|
+
if not agent:
|
|
1481
|
+
self.notify("No active agent", severity="warning")
|
|
1482
|
+
return
|
|
1483
|
+
if model == agent.model:
|
|
1484
|
+
return
|
|
1485
|
+
agent.model = model
|
|
1486
|
+
self._update_footer_model(model)
|
|
1487
|
+
if agent.client:
|
|
1488
|
+
self.notify(f"Switching to {model}...")
|
|
1489
|
+
await agent.disconnect()
|
|
1490
|
+
options = self._make_options(
|
|
1491
|
+
cwd=agent.cwd, agent_name=agent.name, model=model
|
|
1492
|
+
)
|
|
1493
|
+
await agent.connect(options)
|
|
1494
|
+
|
|
1439
1495
|
@work(group="model_prompt", exclusive=True, exit_on_error=False)
|
|
1440
1496
|
async def _handle_model_prompt(self) -> None:
|
|
1441
|
-
"""Show model selection prompt and handle result."""
|
|
1497
|
+
"""Show model selection prompt and handle result for active agent."""
|
|
1442
1498
|
from textual.containers import Center
|
|
1443
1499
|
|
|
1500
|
+
agent = self._agent
|
|
1501
|
+
if not agent:
|
|
1502
|
+
self.notify("No active agent", severity="warning")
|
|
1503
|
+
return
|
|
1504
|
+
|
|
1444
1505
|
if not self._available_models:
|
|
1445
1506
|
self.notify("No models available", severity="warning")
|
|
1446
1507
|
return
|
|
1447
1508
|
|
|
1448
|
-
prompt = ModelPrompt(self._available_models, current_value=
|
|
1509
|
+
prompt = ModelPrompt(self._available_models, current_value=agent.model)
|
|
1449
1510
|
container = Center(prompt, id="model-modal")
|
|
1450
1511
|
self.mount(container)
|
|
1451
1512
|
|
|
@@ -1454,17 +1515,8 @@ class ChatApp(App):
|
|
|
1454
1515
|
finally:
|
|
1455
1516
|
container.remove()
|
|
1456
1517
|
|
|
1457
|
-
if result and result !=
|
|
1458
|
-
self.
|
|
1459
|
-
# Reconnect active agent with new model
|
|
1460
|
-
agent = self._agent
|
|
1461
|
-
if agent and agent.client:
|
|
1462
|
-
self.notify(f"Switching to {result}...")
|
|
1463
|
-
await agent.disconnect()
|
|
1464
|
-
options = self._make_options(cwd=agent.cwd, agent_name=agent.name)
|
|
1465
|
-
await agent.connect(options)
|
|
1466
|
-
# Refresh model display
|
|
1467
|
-
await self._update_slash_commands()
|
|
1518
|
+
if result and result != agent.model:
|
|
1519
|
+
self._set_agent_model(result)
|
|
1468
1520
|
|
|
1469
1521
|
@work(group="new_agent", exclusive=True, exit_on_error=False)
|
|
1470
1522
|
async def _create_new_agent(
|
|
@@ -1474,6 +1526,7 @@ class ChatApp(App):
|
|
|
1474
1526
|
worktree: str | None = None,
|
|
1475
1527
|
auto_resume: bool = False,
|
|
1476
1528
|
switch_to: bool = True,
|
|
1529
|
+
model: str | None = None,
|
|
1477
1530
|
) -> None:
|
|
1478
1531
|
"""Create a new agent via AgentManager.
|
|
1479
1532
|
|
|
@@ -1483,6 +1536,7 @@ class ChatApp(App):
|
|
|
1483
1536
|
worktree: Git worktree branch name if applicable
|
|
1484
1537
|
auto_resume: Try to resume session with most messages in cwd
|
|
1485
1538
|
switch_to: Whether to switch to the new agent (default True)
|
|
1539
|
+
model: Model override (None = SDK default)
|
|
1486
1540
|
"""
|
|
1487
1541
|
if self.agent_mgr is None:
|
|
1488
1542
|
self.notify("Agent manager not initialized", severity="error")
|
|
@@ -1505,6 +1559,7 @@ class ChatApp(App):
|
|
|
1505
1559
|
worktree=worktree,
|
|
1506
1560
|
resume=resume_id,
|
|
1507
1561
|
switch_to=switch_to,
|
|
1562
|
+
model=model,
|
|
1508
1563
|
)
|
|
1509
1564
|
except Exception as e:
|
|
1510
1565
|
self.show_error(f"Failed to create agent '{name}'", e)
|
|
@@ -1636,6 +1691,14 @@ class ChatApp(App):
|
|
|
1636
1691
|
"""Handle new agent creation from AgentManager."""
|
|
1637
1692
|
log.info(f"New agent created: {agent.name} (id={agent.id})")
|
|
1638
1693
|
|
|
1694
|
+
# Track analytics (same_directory = agent cwd matches app starting cwd)
|
|
1695
|
+
same_directory = agent.cwd == Path.cwd()
|
|
1696
|
+
self._agent_metadata[agent.id] = {
|
|
1697
|
+
"created_at": time.time(),
|
|
1698
|
+
"same_directory": same_directory,
|
|
1699
|
+
}
|
|
1700
|
+
self.run_worker(capture("agent_created", same_directory=same_directory))
|
|
1701
|
+
|
|
1639
1702
|
try:
|
|
1640
1703
|
# Create chat view for the agent
|
|
1641
1704
|
is_first_agent = len(self.agent_mgr.agents) == 1 if self.agent_mgr else True
|
|
@@ -1648,7 +1711,8 @@ class ChatApp(App):
|
|
|
1648
1711
|
else:
|
|
1649
1712
|
# Additional agents get new chat views
|
|
1650
1713
|
chat_view = ChatView(
|
|
1651
|
-
id=f"chat-view-{agent.id
|
|
1714
|
+
id=f"chat-view-{agent.id.replace('/', '-')}",
|
|
1715
|
+
classes="chat-view hidden",
|
|
1652
1716
|
)
|
|
1653
1717
|
main = self.query_one("#main", Horizontal)
|
|
1654
1718
|
main.mount(chat_view, after=self.query_one("#session-picker"))
|
|
@@ -1659,7 +1723,7 @@ class ChatApp(App):
|
|
|
1659
1723
|
|
|
1660
1724
|
# Add to sidebar
|
|
1661
1725
|
try:
|
|
1662
|
-
self.
|
|
1726
|
+
self.agent_section.add_agent(agent.id, agent.name)
|
|
1663
1727
|
except Exception:
|
|
1664
1728
|
log.debug(f"Sidebar not mounted for agent {agent.id}")
|
|
1665
1729
|
|
|
@@ -1685,24 +1749,39 @@ class ChatApp(App):
|
|
|
1685
1749
|
|
|
1686
1750
|
# Update sidebar
|
|
1687
1751
|
try:
|
|
1688
|
-
self.
|
|
1752
|
+
self.agent_section.set_active(new_agent.id)
|
|
1689
1753
|
except Exception:
|
|
1690
1754
|
pass
|
|
1691
1755
|
|
|
1692
1756
|
# Update footer
|
|
1693
1757
|
self._update_footer_auto_edit()
|
|
1694
1758
|
self._update_footer_cwd(new_agent.cwd)
|
|
1759
|
+
self._update_footer_model(new_agent.model)
|
|
1695
1760
|
|
|
1696
1761
|
# Update todo panel
|
|
1697
1762
|
self.todo_panel.update_todos(new_agent.todos)
|
|
1698
1763
|
self.refresh_context()
|
|
1699
1764
|
self._position_right_sidebar()
|
|
1700
1765
|
|
|
1701
|
-
def on_agent_closed(self, agent_id: str) -> None:
|
|
1766
|
+
def on_agent_closed(self, agent_id: str, message_count: int = 0) -> None:
|
|
1702
1767
|
"""Handle agent closure from AgentManager."""
|
|
1703
1768
|
log.info(f"Agent closed: {agent_id}")
|
|
1769
|
+
|
|
1770
|
+
# Track analytics
|
|
1771
|
+
metadata = self._agent_metadata.pop(agent_id, {})
|
|
1772
|
+
duration = time.time() - metadata.get("created_at", time.time())
|
|
1773
|
+
same_directory = metadata.get("same_directory", True)
|
|
1774
|
+
self.run_worker(
|
|
1775
|
+
capture(
|
|
1776
|
+
"agent_closed",
|
|
1777
|
+
duration_seconds=int(duration),
|
|
1778
|
+
same_directory=same_directory,
|
|
1779
|
+
message_count=message_count,
|
|
1780
|
+
)
|
|
1781
|
+
)
|
|
1782
|
+
|
|
1704
1783
|
try:
|
|
1705
|
-
self.
|
|
1784
|
+
self.agent_section.remove_agent(agent_id)
|
|
1706
1785
|
except Exception:
|
|
1707
1786
|
pass
|
|
1708
1787
|
self._position_right_sidebar()
|
|
@@ -1710,7 +1789,7 @@ class ChatApp(App):
|
|
|
1710
1789
|
def on_status_changed(self, agent: Agent) -> None:
|
|
1711
1790
|
"""Handle agent status change."""
|
|
1712
1791
|
try:
|
|
1713
|
-
self.
|
|
1792
|
+
self.agent_section.update_status(agent.id, agent.status)
|
|
1714
1793
|
# Update hamburger color if any agent needs attention
|
|
1715
1794
|
self._update_hamburger_attention()
|
|
1716
1795
|
except Exception:
|
|
@@ -1764,23 +1843,21 @@ class ChatApp(App):
|
|
|
1764
1843
|
def on_todos_updated(self, agent: Agent) -> None:
|
|
1765
1844
|
"""Handle agent todos update."""
|
|
1766
1845
|
if self.agent_mgr and agent.id == self.agent_mgr.active_id:
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1846
|
+
self.todo_panel.update_todos(agent.todos)
|
|
1847
|
+
self._position_right_sidebar()
|
|
1848
|
+
# Add inline widget to chat stream
|
|
1849
|
+
chat_view = self._get_chat_view(agent.id)
|
|
1850
|
+
if chat_view:
|
|
1851
|
+
chat_view.mount(TodoWidget(agent.todos))
|
|
1852
|
+
chat_view.scroll_if_tailing()
|
|
1771
1853
|
|
|
1772
1854
|
def on_text_chunk(
|
|
1773
1855
|
self, agent: Agent, text: str, new_message: bool, parent_tool_use_id: str | None
|
|
1774
1856
|
) -> None:
|
|
1775
|
-
"""Handle text chunk from agent -
|
|
1776
|
-
self.
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
new_message=new_message,
|
|
1780
|
-
parent_tool_use_id=parent_tool_use_id,
|
|
1781
|
-
agent_id=agent.id,
|
|
1782
|
-
)
|
|
1783
|
-
)
|
|
1857
|
+
"""Handle text chunk from agent - update UI directly (bypasses message queue)."""
|
|
1858
|
+
chat_view = self._chat_views.get(agent.id)
|
|
1859
|
+
if chat_view:
|
|
1860
|
+
chat_view.append_text(text, new_message, parent_tool_use_id)
|
|
1784
1861
|
|
|
1785
1862
|
def on_tool_use(self, agent: Agent, tool: ToolUse) -> None:
|
|
1786
1863
|
"""Handle tool use from agent - post Textual Message for UI."""
|
|
@@ -1833,15 +1910,25 @@ class ChatApp(App):
|
|
|
1833
1910
|
|
|
1834
1911
|
This is called by Agent when it needs user input for a permission.
|
|
1835
1912
|
Returns a PermissionResponse with choice and optional alternative message.
|
|
1913
|
+
|
|
1914
|
+
Uses a lock to serialize permission prompts - only one shown at a time.
|
|
1836
1915
|
"""
|
|
1837
1916
|
# Put in interactions queue for testing
|
|
1838
1917
|
await self.interactions.put(request)
|
|
1839
1918
|
|
|
1840
|
-
#
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1919
|
+
# Serialize permission prompts - acquire lock before showing UI
|
|
1920
|
+
async with self._permission_lock:
|
|
1921
|
+
# Wait until this agent is active before showing prompt (multi-agent only)
|
|
1922
|
+
if len(self.agents) > 1:
|
|
1923
|
+
while agent.id != self.active_agent_id:
|
|
1924
|
+
await asyncio.sleep(0.1)
|
|
1844
1925
|
|
|
1926
|
+
return await self._show_permission_prompt(agent, request)
|
|
1927
|
+
|
|
1928
|
+
async def _show_permission_prompt(
|
|
1929
|
+
self, agent: Agent, request: PermissionRequest
|
|
1930
|
+
) -> PermissionResponse:
|
|
1931
|
+
"""Show the permission prompt UI (called under _permission_lock)."""
|
|
1845
1932
|
if request.tool_name == ToolName.ASK_USER_QUESTION:
|
|
1846
1933
|
# Handle question prompts
|
|
1847
1934
|
questions = request.tool_input.get("questions", [])
|
|
@@ -1910,3 +1997,25 @@ class ChatApp(App):
|
|
|
1910
1997
|
asyncio.create_task(self.status_footer.refresh_branch(str(cwd)))
|
|
1911
1998
|
except Exception:
|
|
1912
1999
|
pass
|
|
2000
|
+
|
|
2001
|
+
def _update_footer_model(self, model: str | None) -> None:
|
|
2002
|
+
"""Update footer to show agent's model."""
|
|
2003
|
+
if not self._available_models:
|
|
2004
|
+
# No model info yet - show raw value or empty
|
|
2005
|
+
self.status_footer.model = model.capitalize() if model else ""
|
|
2006
|
+
return
|
|
2007
|
+
# Find matching model, or default if model is None
|
|
2008
|
+
active = self._available_models[0]
|
|
2009
|
+
for m in self._available_models:
|
|
2010
|
+
if model and m.get("value") == model:
|
|
2011
|
+
active = m
|
|
2012
|
+
break
|
|
2013
|
+
if not model and m.get("value") == "default":
|
|
2014
|
+
active = m
|
|
2015
|
+
break
|
|
2016
|
+
# Extract short name from description like "Opus 4.5 · ..."
|
|
2017
|
+
desc = active.get("description", "")
|
|
2018
|
+
model_name = (
|
|
2019
|
+
desc.split("·")[0].strip() if "·" in desc else active.get("displayName", "")
|
|
2020
|
+
)
|
|
2021
|
+
self.status_footer.model = model_name
|