seedcode-cli 7.2.5__py3-none-any.whl → 8.2.5__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.
- seedcode/__init__.py +1 -1
- seedcode/app.py +353 -36
- seedcode/cli.py +29 -1
- seedcode/codemode_state.py +1 -1
- seedcode/commands/agent.py +10 -10
- seedcode/commands/assist.py +80 -32
- seedcode/commands/chat.py +11 -8
- seedcode/commands/codemode.py +20 -23
- seedcode/commands/desktop.py +8 -8
- seedcode/commands/mode.py +26 -23
- seedcode/commands/palette.py +5 -5
- seedcode/commands/status.py +8 -7
- seedcode/computer/permissions.py +4 -4
- seedcode/core/models.py +40 -3
- seedcode/core/modes.py +130 -0
- seedcode/tools/desktop.py +2 -2
- seedcode/ui/__init__.py +5 -1
- seedcode/ui/codemode_header.py +10 -3
- seedcode/ui/content.py +421 -0
- seedcode/ui/dashboard.py +16 -14
- seedcode/ui/header.py +325 -0
- seedcode/ui/reference.py +1 -1
- seedcode/ui/state.py +286 -0
- seedcode/ui/statusbar.py +8 -2
- seedcode/ui/tasks.py +17 -6
- seedcode/ui/theme.py +4 -0
- seedcode/ui/tui.py +1212 -0
- {seedcode_cli-7.2.5.dist-info → seedcode_cli-8.2.5.dist-info}/METADATA +185 -78
- {seedcode_cli-7.2.5.dist-info → seedcode_cli-8.2.5.dist-info}/RECORD +32 -27
- {seedcode_cli-7.2.5.dist-info → seedcode_cli-8.2.5.dist-info}/licenses/LICENSE +8 -0
- {seedcode_cli-7.2.5.dist-info → seedcode_cli-8.2.5.dist-info}/WHEEL +0 -0
- {seedcode_cli-7.2.5.dist-info → seedcode_cli-8.2.5.dist-info}/entry_points.txt +0 -0
seedcode/__init__.py
CHANGED
|
@@ -6,7 +6,7 @@ Plant ideas. Grow code.
|
|
|
6
6
|
# Single source of truth for the application version. Every consumer reads it:
|
|
7
7
|
# the PyPI build (via [tool.hatch.version]), the CLI --version flag, the
|
|
8
8
|
# Windows installer metadata, the GitHub release tag, and the WinGet manifest.
|
|
9
|
-
__version__ = "
|
|
9
|
+
__version__ = "8.2.5"
|
|
10
10
|
__author__ = "Al Shahriar Sowan"
|
|
11
11
|
__publisher__ = "Eagox Studio"
|
|
12
12
|
# Current author/company links, surfaced by /about and the README credits.
|
seedcode/app.py
CHANGED
|
@@ -45,7 +45,8 @@ from .ui import UI
|
|
|
45
45
|
from .ui.badges import badge_for_status
|
|
46
46
|
from .ui.menu import MenuItem, run_menu
|
|
47
47
|
from .ui.reference import CONTROLS_HINT, INPUT_HINT
|
|
48
|
-
from .ui.
|
|
48
|
+
from .ui.state import AppState, Status
|
|
49
|
+
from .ui.tasks import TaskFlow, TaskState, activity_for
|
|
49
50
|
from .ui.textbox import prompt_label
|
|
50
51
|
from .ui.theme import pt_style, set_active_theme
|
|
51
52
|
from .utils.logger import get_logger
|
|
@@ -149,35 +150,43 @@ def _key_status(config: AppConfig) -> str:
|
|
|
149
150
|
return config.masked_key()
|
|
150
151
|
|
|
151
152
|
|
|
152
|
-
def
|
|
153
|
-
"""Menu status for the
|
|
153
|
+
def _workspace_status(config: AppConfig) -> str:
|
|
154
|
+
"""Menu status for the workspace capability (ON only when really active).
|
|
155
|
+
|
|
156
|
+
The workspace (`.seedcode` project memory + index) is a capability of Agent
|
|
157
|
+
Mode, not a separate mode, so its status reflects that session alone rather
|
|
158
|
+
than the mode name.
|
|
159
|
+
"""
|
|
154
160
|
try:
|
|
155
161
|
from .codemode_state import codemode_state
|
|
156
162
|
|
|
157
|
-
if codemode_state().enabled
|
|
158
|
-
return "ON"
|
|
163
|
+
return "ON" if codemode_state().enabled else "OFF"
|
|
159
164
|
except Exception:
|
|
160
|
-
|
|
161
|
-
return "ON" if config.agent_mode else "OFF"
|
|
165
|
+
return "OFF"
|
|
162
166
|
|
|
163
167
|
|
|
164
168
|
def _main_menu(config: AppConfig):
|
|
165
169
|
"""The interactive main menu; returns an action id or None (exit).
|
|
166
170
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
171
|
+
v8.2.5 layout: the mode/setup actions carry Ctrl+1..Ctrl+4 shortcuts that
|
|
172
|
+
execute the real handlers (no decorative items), followed by the
|
|
173
|
+
chat/setup actions kept from earlier releases. There are exactly two
|
|
174
|
+
modes — Chat and Agent — and Code Mode's workspace capability lives inside
|
|
175
|
+
Agent Mode.
|
|
170
176
|
"""
|
|
171
177
|
provider = PROVIDERS.get(config.provider)
|
|
172
178
|
badge = badge_for_status(provider.status if provider is not None else "")
|
|
173
179
|
return run_menu(
|
|
174
180
|
[
|
|
175
|
-
MenuItem("
|
|
176
|
-
MenuItem(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
+
MenuItem("Agent Mode", "agent", shortcut="1"),
|
|
182
|
+
MenuItem(
|
|
183
|
+
"Project Workspace",
|
|
184
|
+
"memory",
|
|
185
|
+
status=_workspace_status(config),
|
|
186
|
+
shortcut="2",
|
|
187
|
+
),
|
|
188
|
+
MenuItem("Settings", "settings", shortcut="3"),
|
|
189
|
+
MenuItem("Exit", "exit", shortcut="4"),
|
|
181
190
|
MenuItem("Start Chat", "chat", status=_model_status(config), badge=badge),
|
|
182
191
|
MenuItem("Provider", "provider", status=_provider_status(config)),
|
|
183
192
|
MenuItem("API Key", "apikey", status=_key_status(config)),
|
|
@@ -290,6 +299,20 @@ class _TaskPresenter:
|
|
|
290
299
|
self._echo_lines = 0
|
|
291
300
|
self._echo_elided = False
|
|
292
301
|
|
|
302
|
+
# --- live status (persistent TUI only) ----------------------------------
|
|
303
|
+
def _set_status(self, status: Status, operation: str | None = None) -> None:
|
|
304
|
+
"""Reflect a real phase on the live header; a no-op on the plain UI.
|
|
305
|
+
|
|
306
|
+
Only the TUI exposes ``state``, so the sequential console is untouched.
|
|
307
|
+
"""
|
|
308
|
+
state = getattr(self.ui, "state", None)
|
|
309
|
+
if state is None:
|
|
310
|
+
return
|
|
311
|
+
try:
|
|
312
|
+
state.set_status(status, operation)
|
|
313
|
+
except Exception:
|
|
314
|
+
pass # a cosmetic status must never break a task
|
|
315
|
+
|
|
293
316
|
# --- one command's live output ------------------------------------------
|
|
294
317
|
def reset_echo(self) -> None:
|
|
295
318
|
self._echo_lines = 0
|
|
@@ -297,6 +320,8 @@ class _TaskPresenter:
|
|
|
297
320
|
|
|
298
321
|
def on_output(self, line: str) -> None:
|
|
299
322
|
"""Compact live echo: the first few lines, then a single elision note."""
|
|
323
|
+
if self._echo_lines == 0:
|
|
324
|
+
self._set_status(Status.EXECUTING, "running a command")
|
|
300
325
|
if self._echo_lines < self.MAX_ECHO_LINES:
|
|
301
326
|
self.ui.dim(f" │ {line[:200]}")
|
|
302
327
|
elif not self._echo_elided:
|
|
@@ -315,6 +340,7 @@ class _TaskPresenter:
|
|
|
315
340
|
return
|
|
316
341
|
if kind == "error":
|
|
317
342
|
self.ui.dim(f" ✖ {detail.splitlines()[0][:120]}")
|
|
343
|
+
self._set_status(Status.ERROR, "an operation failed")
|
|
318
344
|
elif kind == "switching_provider":
|
|
319
345
|
# A provider request failed and a healthy fallback took over; the
|
|
320
346
|
# task continues rather than restarting (v7.2.5).
|
|
@@ -324,11 +350,12 @@ class _TaskPresenter:
|
|
|
324
350
|
elif kind == "retry":
|
|
325
351
|
self.ui.dim(f" {detail}")
|
|
326
352
|
elif kind == "limit":
|
|
327
|
-
self.ui.warning(f"
|
|
353
|
+
self.ui.warning(f"Agent Mode stopped: {detail}")
|
|
328
354
|
elif kind == "call" and flow is None:
|
|
329
355
|
# With a task view on screen the step list already shows the tool
|
|
330
356
|
# work; without one, narrate it (the plain/legacy experience).
|
|
331
357
|
self.ui.dim(f" ⚒ {detail}")
|
|
358
|
+
self._set_status(Status.EXECUTING, detail)
|
|
332
359
|
|
|
333
360
|
# --- structured activity -> step states ---------------------------------
|
|
334
361
|
def on_step(self, payload: dict) -> None:
|
|
@@ -341,6 +368,7 @@ class _TaskPresenter:
|
|
|
341
368
|
if phase == "tool_start":
|
|
342
369
|
self.reset_echo()
|
|
343
370
|
flow.observe_tool_start(name, args)
|
|
371
|
+
self._set_status(Status.EXECUTING, activity_for(name, args))
|
|
344
372
|
elif phase == "tool_done":
|
|
345
373
|
flow.observe_tool_done(
|
|
346
374
|
name,
|
|
@@ -348,6 +376,7 @@ class _TaskPresenter:
|
|
|
348
376
|
str(payload.get("output") or ""),
|
|
349
377
|
args,
|
|
350
378
|
)
|
|
379
|
+
self._set_status(Status.WORKING, "continuing")
|
|
351
380
|
elif phase == "say":
|
|
352
381
|
flow.observe_text(str(payload.get("text") or ""))
|
|
353
382
|
|
|
@@ -364,6 +393,21 @@ class _TaskPresenter:
|
|
|
364
393
|
|
|
365
394
|
flow = self.flow
|
|
366
395
|
session = session_mod.current_session()
|
|
396
|
+
# Reflect the real session phase on the live header (TUI only).
|
|
397
|
+
phase = {
|
|
398
|
+
"plan": (Status.THINKING, "planning the work"),
|
|
399
|
+
"plan_ready": (Status.WORKING, "plan ready"),
|
|
400
|
+
"task_start": (Status.WORKING, detail),
|
|
401
|
+
"verify": (Status.WORKING, "verifying acceptance criteria"),
|
|
402
|
+
"recovery": (Status.WORKING, "diagnosing and repairing"),
|
|
403
|
+
"reconnect": (Status.THINKING, "reconnecting to the provider"),
|
|
404
|
+
"network_lost": (Status.ERROR, detail),
|
|
405
|
+
"final_verify": (Status.EXECUTING, "final verification"),
|
|
406
|
+
"task_blocked": (Status.ERROR, detail),
|
|
407
|
+
"task_failed": (Status.ERROR, detail),
|
|
408
|
+
}.get(kind)
|
|
409
|
+
if phase is not None:
|
|
410
|
+
self._set_status(*phase)
|
|
367
411
|
if kind == "plan":
|
|
368
412
|
self.ui.dim(" planning the work…")
|
|
369
413
|
return
|
|
@@ -497,15 +541,10 @@ def agent_presenter(agent: AgentEngine) -> _TaskPresenter | None:
|
|
|
497
541
|
|
|
498
542
|
|
|
499
543
|
def _task_mode_label(config: AppConfig) -> str:
|
|
500
|
-
"""The mode named in a task header (
|
|
501
|
-
|
|
502
|
-
from .codemode_state import codemode_state
|
|
544
|
+
"""The mode named in a task header (Chat Mode / Agent Mode only)."""
|
|
545
|
+
from .core.modes import active_mode, mode_title
|
|
503
546
|
|
|
504
|
-
|
|
505
|
-
return "Code Mode"
|
|
506
|
-
except Exception:
|
|
507
|
-
pass
|
|
508
|
-
return "Assist Mode"
|
|
547
|
+
return mode_title(active_mode(config))
|
|
509
548
|
|
|
510
549
|
|
|
511
550
|
def _save_codemode_summary(agent: AgentEngine, text: str, outcome: str) -> None:
|
|
@@ -560,7 +599,9 @@ def _handle_codemode(
|
|
|
560
599
|
|
|
561
600
|
lc = lifecycle()
|
|
562
601
|
workspace, store = _codemode_workspace()
|
|
563
|
-
|
|
602
|
+
config = getattr(agent, "config", None)
|
|
603
|
+
label = _task_mode_label(config) if config is not None else "Agent Mode"
|
|
604
|
+
flow = TaskFlow.for_ui(ui, mode_label=label, task=text)
|
|
564
605
|
if presenter is not None:
|
|
565
606
|
presenter.flow = flow
|
|
566
607
|
if flow is not None:
|
|
@@ -681,7 +722,7 @@ def _handle_agent(
|
|
|
681
722
|
text: str,
|
|
682
723
|
presenter: _TaskPresenter | None = None,
|
|
683
724
|
) -> None:
|
|
684
|
-
"""Run one full
|
|
725
|
+
"""Run one full Agent/Code turn (tool loop) and render the final answer.
|
|
685
726
|
|
|
686
727
|
The turn is shown as a compact live task flow whose step states follow real
|
|
687
728
|
engine activity (see :mod:`seedcode.ui.tasks`) — never a fake progress bar.
|
|
@@ -766,7 +807,7 @@ def _handle_agent(
|
|
|
766
807
|
def _make_agent(
|
|
767
808
|
ui: UI, config: AppConfig, presenter: "_TaskPresenter | None" = None
|
|
768
809
|
) -> AgentEngine:
|
|
769
|
-
"""Build an
|
|
810
|
+
"""Build an Agent Mode engine bound to the CWD and configured permissions."""
|
|
770
811
|
# Lazy import (matching _make_desktop_session): the optional, platform-
|
|
771
812
|
# specific computer package stays out of app.py's top-level import graph.
|
|
772
813
|
from .computer import is_available
|
|
@@ -818,7 +859,7 @@ def _make_desktop_session(ui: UI):
|
|
|
818
859
|
|
|
819
860
|
The session is bound to the process-wide
|
|
820
861
|
:class:`~seedcode.computer.SessionPermissionManager` so a permission the
|
|
821
|
-
user granted once at ``/
|
|
862
|
+
user granted once at ``/agent on`` keeps holding even though this gate is
|
|
822
863
|
rebuilt whenever the permission level changes.
|
|
823
864
|
"""
|
|
824
865
|
from .computer import DesktopGrant, DesktopSession, session_permissions
|
|
@@ -866,14 +907,14 @@ def _chat_loop(
|
|
|
866
907
|
) -> None:
|
|
867
908
|
"""Interactive chat until /exit (returns to the main menu).
|
|
868
909
|
|
|
869
|
-
Only an explicit exit leaves here: a finished task (Code Mode,
|
|
870
|
-
Mode
|
|
910
|
+
Only an explicit exit leaves here: a finished task (Code Mode, Agent
|
|
911
|
+
Mode or a plain chat turn) returns to the prompt with
|
|
871
912
|
``Ready for next task.`` and the session keeps running.
|
|
872
913
|
"""
|
|
873
914
|
ctx = CommandContext(ui=ui, config=config, engine=engine)
|
|
874
915
|
ui.dim(INPUT_HINT)
|
|
875
916
|
|
|
876
|
-
# The
|
|
917
|
+
# The Agent engine is built lazily on the first agent-mode turn and
|
|
877
918
|
# rebuilt when the permission or desktop mode changes (its system
|
|
878
919
|
# prompt and permission gates reflect both). The presenter that owns the
|
|
879
920
|
# live step view lives for the whole chat session, so each turn can bind
|
|
@@ -1035,6 +1076,286 @@ def _build_chat_session(ui: UI) -> PromptSession:
|
|
|
1035
1076
|
return PromptSession(key_bindings=kb)
|
|
1036
1077
|
|
|
1037
1078
|
|
|
1079
|
+
# ---------------------------------------------------------------------------
|
|
1080
|
+
# Persistent TUI session (v8.2.5)
|
|
1081
|
+
# ---------------------------------------------------------------------------
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def _terminal_size() -> tuple[int, int]:
|
|
1085
|
+
"""The real terminal size (80x24 when unknown, e.g. tests/CI)."""
|
|
1086
|
+
import os
|
|
1087
|
+
|
|
1088
|
+
try:
|
|
1089
|
+
size = os.get_terminal_size()
|
|
1090
|
+
return max(20, int(size.columns)), max(6, int(size.lines))
|
|
1091
|
+
except Exception:
|
|
1092
|
+
return 80, 24
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
class _TuiController:
|
|
1096
|
+
"""Drives the persistent session.
|
|
1097
|
+
|
|
1098
|
+
A message runs its turn on a worker thread while the prompt_toolkit
|
|
1099
|
+
application keeps the screen, so the header stays fixed and output streams
|
|
1100
|
+
into the middle region. Commands return control for one dispatch (a selector
|
|
1101
|
+
or menu needs the terminal to itself), then the session resumes.
|
|
1102
|
+
"""
|
|
1103
|
+
|
|
1104
|
+
def __init__(self, ui, tui, config: AppConfig) -> None:
|
|
1105
|
+
self.ui = ui
|
|
1106
|
+
self.tui = tui
|
|
1107
|
+
self.config = config
|
|
1108
|
+
self.engine = ChatEngine(config, on_event=self._on_provider_event)
|
|
1109
|
+
self.history = HistoryStore(provider_id=config.provider)
|
|
1110
|
+
self.presenter = _TaskPresenter(ui)
|
|
1111
|
+
self.ctx = CommandContext(ui=ui, config=config, engine=self.engine)
|
|
1112
|
+
self.agent: AgentEngine | None = None
|
|
1113
|
+
self.agent_perm = config.permission_mode
|
|
1114
|
+
self.agent_codemode = _codemode_enabled()
|
|
1115
|
+
self._backend = config.provider
|
|
1116
|
+
tui.on_submit = self.submit
|
|
1117
|
+
# Mode switches run in place: no application exit, no rebuild.
|
|
1118
|
+
tui.on_command = self._inplace_command
|
|
1119
|
+
|
|
1120
|
+
# --- provider/backend --------------------------------------------------
|
|
1121
|
+
def _on_provider_event(self, kind: str, detail: str) -> None:
|
|
1122
|
+
if kind == "switching_provider":
|
|
1123
|
+
self.ui.warning(f"Switching provider: {detail}")
|
|
1124
|
+
elif kind == "retry":
|
|
1125
|
+
self.ui.dim(f" {detail}")
|
|
1126
|
+
|
|
1127
|
+
def _rebuild_backend(self) -> None:
|
|
1128
|
+
"""Rebuild the chat backend/history after a provider change."""
|
|
1129
|
+
self.engine = ChatEngine(self.config, on_event=self._on_provider_event)
|
|
1130
|
+
self.history = HistoryStore(provider_id=self.config.provider)
|
|
1131
|
+
self.ctx = CommandContext(ui=self.ui, config=self.config, engine=self.engine)
|
|
1132
|
+
self.agent = None
|
|
1133
|
+
self._backend = self.config.provider
|
|
1134
|
+
|
|
1135
|
+
# --- one turn (worker thread) ------------------------------------------
|
|
1136
|
+
def submit(self, text: str) -> None:
|
|
1137
|
+
"""Run one user turn to completion (never raises into the TUI)."""
|
|
1138
|
+
from .core.modes import active_mode, agentic
|
|
1139
|
+
|
|
1140
|
+
with lifecycle().task_span():
|
|
1141
|
+
if agentic(active_mode(self.config)):
|
|
1142
|
+
self._ensure_agent()
|
|
1143
|
+
_handle_agent(self.ui, self.agent, self.history, text, self.presenter)
|
|
1144
|
+
else:
|
|
1145
|
+
_handle_chat(self.ui, self.engine, self.history, text)
|
|
1146
|
+
try:
|
|
1147
|
+
self.tui.sync_from_config(self.config)
|
|
1148
|
+
except Exception:
|
|
1149
|
+
pass
|
|
1150
|
+
|
|
1151
|
+
def _inplace_command(self, text: str) -> None:
|
|
1152
|
+
"""Dispatch a mode-switch command without leaving the TUI.
|
|
1153
|
+
|
|
1154
|
+
The engine/agent are rebuilt only when the mode actually changed, and
|
|
1155
|
+
the header is refreshed from live config — a pure state transition.
|
|
1156
|
+
"""
|
|
1157
|
+
backend_before = self.config.provider
|
|
1158
|
+
mode_before = self.config.mode
|
|
1159
|
+
try:
|
|
1160
|
+
dispatch(self.ctx, text)
|
|
1161
|
+
except (KeyboardInterrupt, EOFError):
|
|
1162
|
+
self.ui.dim("Cancelled.")
|
|
1163
|
+
except Exception as exc: # a broken command must not kill the session
|
|
1164
|
+
_report_command_error(self.ui, exc)
|
|
1165
|
+
if self.config.provider != backend_before:
|
|
1166
|
+
self._rebuild_backend()
|
|
1167
|
+
elif self.config.mode != mode_before:
|
|
1168
|
+
# A mode change invalidates the agent built for the old mode.
|
|
1169
|
+
self.agent = None
|
|
1170
|
+
self.tui.sync_from_config(self.config)
|
|
1171
|
+
|
|
1172
|
+
def _ensure_agent(self) -> None:
|
|
1173
|
+
codemode_now = _codemode_enabled()
|
|
1174
|
+
if (
|
|
1175
|
+
self.agent is None
|
|
1176
|
+
or self.agent_perm != self.config.permission_mode
|
|
1177
|
+
or self.agent_codemode != codemode_now
|
|
1178
|
+
):
|
|
1179
|
+
self.agent = _make_agent(self.ui, self.config, self.presenter)
|
|
1180
|
+
self.agent_perm = self.config.permission_mode
|
|
1181
|
+
self.agent_codemode = codemode_now
|
|
1182
|
+
|
|
1183
|
+
# --- the session loop --------------------------------------------------
|
|
1184
|
+
def run(self) -> None:
|
|
1185
|
+
while True:
|
|
1186
|
+
kind, payload = self.tui.run_once()
|
|
1187
|
+
if kind == "exit":
|
|
1188
|
+
self.tui.shutdown()
|
|
1189
|
+
_exit_application(self.ui, "tui exit")
|
|
1190
|
+
return
|
|
1191
|
+
if kind == "key":
|
|
1192
|
+
_run_key_action(
|
|
1193
|
+
self.ui, self.ctx, self.config, _KEY_ACTIONS.get(payload or "", "")
|
|
1194
|
+
)
|
|
1195
|
+
self.tui.sync_from_config(self.config)
|
|
1196
|
+
continue
|
|
1197
|
+
if kind == "toolbar":
|
|
1198
|
+
# A header control was activated: run the real application
|
|
1199
|
+
# action (the selector owns the terminal for one step).
|
|
1200
|
+
self._toolbar_action(payload or "")
|
|
1201
|
+
self.tui.sync_from_config(self.config)
|
|
1202
|
+
continue
|
|
1203
|
+
if kind != "command" or not payload:
|
|
1204
|
+
continue
|
|
1205
|
+
backend_before = self.config.provider
|
|
1206
|
+
try:
|
|
1207
|
+
result = dispatch(self.ctx, payload)
|
|
1208
|
+
except (KeyboardInterrupt, EOFError):
|
|
1209
|
+
self.ui.dim("Cancelled.")
|
|
1210
|
+
continue
|
|
1211
|
+
except Exception as exc: # a broken command must not kill the session
|
|
1212
|
+
parts = payload.split()
|
|
1213
|
+
_log.exception("command failed: %s", parts[0] if parts else payload)
|
|
1214
|
+
_report_command_error(self.ui, exc)
|
|
1215
|
+
continue
|
|
1216
|
+
if result.should_exit:
|
|
1217
|
+
if self._menu():
|
|
1218
|
+
self.tui.shutdown()
|
|
1219
|
+
_exit_application(self.ui, "menu exit")
|
|
1220
|
+
return
|
|
1221
|
+
continue
|
|
1222
|
+
if self.config.provider != backend_before:
|
|
1223
|
+
self._rebuild_backend()
|
|
1224
|
+
self.tui.sync_from_config(self.config)
|
|
1225
|
+
|
|
1226
|
+
def _toolbar_action(self, action: str) -> None:
|
|
1227
|
+
"""Run a real action behind a header control (provider/model/mode).
|
|
1228
|
+
|
|
1229
|
+
The control is real UI, so the action is the same one the equivalent
|
|
1230
|
+
slash command performs: provider/model open their selectors and mode
|
|
1231
|
+
opens the three-way mode picker. A provider change rebuilds the chat
|
|
1232
|
+
backend exactly as a mid-chat switch does.
|
|
1233
|
+
"""
|
|
1234
|
+
try:
|
|
1235
|
+
if action == "provider":
|
|
1236
|
+
select_provider(self.ui, self.config)
|
|
1237
|
+
elif action == "model":
|
|
1238
|
+
select_model(self.ui, self.config)
|
|
1239
|
+
elif action == "mode":
|
|
1240
|
+
self._mode_menu()
|
|
1241
|
+
except (KeyboardInterrupt, EOFError):
|
|
1242
|
+
self.ui.dim("Cancelled.")
|
|
1243
|
+
except Exception as exc: # a control must never crash the session
|
|
1244
|
+
_log.exception("toolbar action failed: %s", action)
|
|
1245
|
+
_report_command_error(self.ui, exc)
|
|
1246
|
+
if self.config.provider != self._backend:
|
|
1247
|
+
self._rebuild_backend()
|
|
1248
|
+
|
|
1249
|
+
def _mode_menu(self) -> None:
|
|
1250
|
+
"""Mode picker for the header's Mode control (a real mode switch)."""
|
|
1251
|
+
from .core.modes import Mode, active_mode
|
|
1252
|
+
|
|
1253
|
+
current = active_mode(self.config)
|
|
1254
|
+
choice = run_menu(
|
|
1255
|
+
[
|
|
1256
|
+
MenuItem(
|
|
1257
|
+
"Chat", "chat", status="current" if current is Mode.CHAT else ""
|
|
1258
|
+
),
|
|
1259
|
+
MenuItem(
|
|
1260
|
+
"Agent", "agent", status="current" if current is Mode.AGENT else ""
|
|
1261
|
+
),
|
|
1262
|
+
],
|
|
1263
|
+
title="Mode",
|
|
1264
|
+
hint="\u2191\u2193 move Enter select Esc cancel",
|
|
1265
|
+
)
|
|
1266
|
+
if choice:
|
|
1267
|
+
self._command(f"/mode {choice}")
|
|
1268
|
+
|
|
1269
|
+
def _menu(self) -> bool:
|
|
1270
|
+
"""The interactive main menu; returns True when the user quits."""
|
|
1271
|
+
while True:
|
|
1272
|
+
if self.config.provider != self._backend:
|
|
1273
|
+
self._rebuild_backend()
|
|
1274
|
+
try:
|
|
1275
|
+
choice = _main_menu(self.config)
|
|
1276
|
+
except (KeyboardInterrupt, EOFError):
|
|
1277
|
+
return True
|
|
1278
|
+
if choice == "chat":
|
|
1279
|
+
return False
|
|
1280
|
+
try:
|
|
1281
|
+
if choice == "provider":
|
|
1282
|
+
select_provider(self.ui, self.config)
|
|
1283
|
+
elif choice == "apikey":
|
|
1284
|
+
apikey_menu(self.ui, self.config)
|
|
1285
|
+
elif choice == "model":
|
|
1286
|
+
select_model(self.ui, self.config)
|
|
1287
|
+
elif choice == "agent":
|
|
1288
|
+
self._command("/agent on")
|
|
1289
|
+
elif choice == "memory":
|
|
1290
|
+
self._command("/codemode status")
|
|
1291
|
+
elif choice == "settings":
|
|
1292
|
+
settings_menu(self.ui, self.config)
|
|
1293
|
+
elif choice == "theme":
|
|
1294
|
+
pick_theme(self.ui, self.config)
|
|
1295
|
+
elif choice == "about":
|
|
1296
|
+
show_about(self.ui, self.config)
|
|
1297
|
+
elif choice in ("exit", None):
|
|
1298
|
+
return True
|
|
1299
|
+
except (KeyboardInterrupt, EOFError):
|
|
1300
|
+
self.ui.dim("Cancelled.")
|
|
1301
|
+
except Exception as exc: # menu actions must never crash the app
|
|
1302
|
+
_log.exception("menu action failed: %s", choice)
|
|
1303
|
+
_report_command_error(self.ui, exc)
|
|
1304
|
+
self.tui.sync_from_config(self.config)
|
|
1305
|
+
|
|
1306
|
+
def _command(self, text: str) -> None:
|
|
1307
|
+
try:
|
|
1308
|
+
dispatch(self.ctx, text)
|
|
1309
|
+
except Exception:
|
|
1310
|
+
_log.exception("menu command failed: %s", text)
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
def run_persistent(*, input_device=None, output_device=None) -> None:
|
|
1314
|
+
"""Start the persistent three-region TUI (interactive consoles only).
|
|
1315
|
+
|
|
1316
|
+
Guided setup runs *before* the TUI owns the screen, because its selectors
|
|
1317
|
+
need the terminal to themselves and its output must be visible.
|
|
1318
|
+
"""
|
|
1319
|
+
from .ui.tui import ChatTUI, TuiUI
|
|
1320
|
+
from .utils.terminal_env import workspace_root
|
|
1321
|
+
|
|
1322
|
+
config = load_config()
|
|
1323
|
+
sync_custom_providers(config)
|
|
1324
|
+
set_active_theme(config.theme)
|
|
1325
|
+
|
|
1326
|
+
if not config.is_configured():
|
|
1327
|
+
setup_ui = UI()
|
|
1328
|
+
setup_ui.apply_theme(config.theme)
|
|
1329
|
+
if not _guided_setup(setup_ui, config):
|
|
1330
|
+
setup_ui.dim("Setup incomplete — a provider and a model are required.")
|
|
1331
|
+
return
|
|
1332
|
+
|
|
1333
|
+
workspace = str(workspace_root())
|
|
1334
|
+
columns, _rows = _terminal_size()
|
|
1335
|
+
state = AppState.from_config(config, workspace=workspace)
|
|
1336
|
+
tui = ChatTUI(
|
|
1337
|
+
state,
|
|
1338
|
+
workspace=workspace,
|
|
1339
|
+
width=columns,
|
|
1340
|
+
input_device=input_device,
|
|
1341
|
+
output_device=output_device,
|
|
1342
|
+
)
|
|
1343
|
+
ui = TuiUI(tui)
|
|
1344
|
+
ui.apply_theme(config.theme)
|
|
1345
|
+
tui.banner_text()
|
|
1346
|
+
|
|
1347
|
+
# Best-effort teardown for the one legitimate shutdown path.
|
|
1348
|
+
lifecycle().on_shutdown(_release_desktop_resources)
|
|
1349
|
+
|
|
1350
|
+
_log.info(
|
|
1351
|
+
"persistent TUI started: provider=%s model=%s mode=%s",
|
|
1352
|
+
config.provider,
|
|
1353
|
+
config.model or "(none)",
|
|
1354
|
+
getattr(config, "mode", "chat"),
|
|
1355
|
+
)
|
|
1356
|
+
_TuiController(ui, tui, config).run()
|
|
1357
|
+
|
|
1358
|
+
|
|
1038
1359
|
def run(ui: UI) -> None:
|
|
1039
1360
|
"""Show the startup dashboard, drop straight into chat, then the menu.
|
|
1040
1361
|
|
|
@@ -1116,12 +1437,8 @@ def run(ui: UI) -> None:
|
|
|
1116
1437
|
apikey_menu(ui, config)
|
|
1117
1438
|
elif choice == "model":
|
|
1118
1439
|
select_model(ui, config)
|
|
1119
|
-
elif choice == "codemode":
|
|
1120
|
-
_dispatch_command(ui, config, engine, "/codemode on")
|
|
1121
1440
|
elif choice == "agent":
|
|
1122
1441
|
_dispatch_command(ui, config, engine, "/agent on")
|
|
1123
|
-
elif choice == "assist":
|
|
1124
|
-
_dispatch_command(ui, config, engine, "/assist on")
|
|
1125
1442
|
elif choice == "memory":
|
|
1126
1443
|
_dispatch_command(ui, config, engine, "/codemode status")
|
|
1127
1444
|
elif choice == "settings":
|
seedcode/cli.py
CHANGED
|
@@ -9,9 +9,30 @@ traceback. Heavy imports are deferred into :func:`main` so ``--version``
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import os
|
|
12
13
|
import sys
|
|
13
14
|
|
|
14
15
|
|
|
16
|
+
def _interactive() -> bool:
|
|
17
|
+
"""True when a real console is attached (the TUI needs one to draw)."""
|
|
18
|
+
try:
|
|
19
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
20
|
+
except (AttributeError, ValueError):
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _use_persistent_tui(plain: bool) -> bool:
|
|
25
|
+
"""Whether to start the persistent TUI instead of the sequential console.
|
|
26
|
+
|
|
27
|
+
The TUI owns the whole screen, so it is only used on a real interactive
|
|
28
|
+
console, never when the host mangles ANSI (``SEEDCODE_PLAIN``) or the user
|
|
29
|
+
opts out with ``SEEDCODE_NO_TUI``.
|
|
30
|
+
"""
|
|
31
|
+
if plain or os.environ.get("SEEDCODE_NO_TUI"):
|
|
32
|
+
return False
|
|
33
|
+
return _interactive()
|
|
34
|
+
|
|
35
|
+
|
|
15
36
|
def _prepare_console() -> None:
|
|
16
37
|
"""Make stdout/stderr UTF-8-safe, primarily for Windows.
|
|
17
38
|
|
|
@@ -86,7 +107,14 @@ def main() -> None:
|
|
|
86
107
|
|
|
87
108
|
ui = UI(plain=terminal.plain)
|
|
88
109
|
try:
|
|
89
|
-
|
|
110
|
+
if _use_persistent_tui(terminal.plain):
|
|
111
|
+
# v8.2.5: a real console gets the persistent three-region TUI;
|
|
112
|
+
# every other host keeps the sequential console experience.
|
|
113
|
+
from .app import run_persistent
|
|
114
|
+
|
|
115
|
+
run_persistent()
|
|
116
|
+
else:
|
|
117
|
+
run(ui)
|
|
90
118
|
log.info("Clean exit.")
|
|
91
119
|
except KeyboardInterrupt:
|
|
92
120
|
ui.blank()
|
seedcode/codemode_state.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Code Mode state (v6.2.0): workspace-aware coding agent session state.
|
|
2
2
|
|
|
3
|
-
Code Mode is
|
|
3
|
+
Code Mode is the workspace-aware coding mode for the current project:
|
|
4
4
|
|
|
5
5
|
* the CWD at ``/codemode on`` becomes the *workspace* root;
|
|
6
6
|
* ``.seedcode/`` project memory is ensured and the index refreshed
|
seedcode/commands/agent.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Agent Mode commands: /agent and /permission (v8.2.5).
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
/
|
|
5
|
-
|
|
3
|
+
/agent is the primary route into Agent Mode — the unified autonomous
|
|
4
|
+
workspace/coding agent that the retired Code Mode and Assist Mode folded
|
|
5
|
+
into. /assist, /desktop and /codemode remain accepted aliases so an existing
|
|
6
|
+
habit keeps working. /permission keeps its dedicated interactive picker;
|
|
7
|
+
/index and /tools stay as inspection commands.
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
from __future__ import annotations
|
|
@@ -15,7 +17,7 @@ from . import CommandContext, CommandResult, command, show_session_bar
|
|
|
15
17
|
from .assist import disable_assist, enable_assist
|
|
16
18
|
|
|
17
19
|
|
|
18
|
-
@command("agent", "
|
|
20
|
+
@command("agent", "Select Agent Mode (general-purpose execution). Usage: /agent [on|off]")
|
|
19
21
|
def _agent(ctx: CommandContext, arg: str) -> CommandResult:
|
|
20
22
|
raw = arg.strip().lower()
|
|
21
23
|
if raw in ("on", "off"):
|
|
@@ -27,8 +29,6 @@ def _agent(ctx: CommandContext, arg: str) -> CommandResult:
|
|
|
27
29
|
ctx.ui.dim("Expected: /agent on|off")
|
|
28
30
|
return CommandResult()
|
|
29
31
|
|
|
30
|
-
# Agent Mode was merged into Assist Mode — route there transparently.
|
|
31
|
-
ctx.ui.dim("(/agent is now Assist Mode)")
|
|
32
32
|
if enable:
|
|
33
33
|
enable_assist(ctx.ui, ctx.config)
|
|
34
34
|
else:
|
|
@@ -39,7 +39,7 @@ def _agent(ctx: CommandContext, arg: str) -> CommandResult:
|
|
|
39
39
|
|
|
40
40
|
@command(
|
|
41
41
|
"permission",
|
|
42
|
-
"Show or set the
|
|
42
|
+
"Show or set the tool permission level",
|
|
43
43
|
aliases=("perm", "permissions"),
|
|
44
44
|
)
|
|
45
45
|
def _permission(ctx: CommandContext, arg: str) -> CommandResult:
|
|
@@ -85,7 +85,7 @@ def _index(ctx: CommandContext, arg: str) -> CommandResult:
|
|
|
85
85
|
return CommandResult()
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
@command("tools", "List the tools available
|
|
88
|
+
@command("tools", "List the tools available to the agent modes")
|
|
89
89
|
def _tools(ctx: CommandContext, arg: str) -> CommandResult:
|
|
90
90
|
from ..ui.layout import columns_grid
|
|
91
91
|
|
|
@@ -95,6 +95,6 @@ def _tools(ctx: CommandContext, arg: str) -> CommandResult:
|
|
|
95
95
|
kind = "changes files/system" if tool.mutates else "read-only"
|
|
96
96
|
rows.append((name, f"{tool.description} ({kind})"))
|
|
97
97
|
ctx.ui.panel(
|
|
98
|
-
columns_grid(rows, ("seed.primary", "seed.text")), title="
|
|
98
|
+
columns_grid(rows, ("seed.primary", "seed.text")), title="Agent Tools"
|
|
99
99
|
)
|
|
100
100
|
return CommandResult()
|