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 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__ = "7.2.5"
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.tasks import TaskFlow, TaskState
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 _mode_status(config: AppConfig) -> str:
153
- """Menu status for the Code Mode item (ON only when really active)."""
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
- pass
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
- v6.2.5 reference layout: the six mode/setup actions carry Ctrl+1..Ctrl+6
168
- shortcuts that execute the real handlers (no decorative items), followed
169
- by the chat/setup actions kept from earlier releases.
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("Code Mode", "codemode", status=_mode_status(config), shortcut="1"),
176
- MenuItem("Agent Mode", "agent", shortcut="2"),
177
- MenuItem("Assist Mode", "assist", shortcut="3"),
178
- MenuItem("Project Memory", "memory", shortcut="4"),
179
- MenuItem("Settings", "settings", shortcut="5"),
180
- MenuItem("Exit", "exit", shortcut="6"),
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"Assist stopped: {detail}")
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 (Code Mode sharpens Assist Mode)."""
501
- try:
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
- if codemode_state().enabled:
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
- flow = TaskFlow.for_ui(ui, mode_label="Code Mode", task=text)
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 Assist/Code turn (tool loop) and render the final answer.
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 Assist engine bound to the CWD and the configured permissions."""
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 ``/assist on`` keeps holding even though this gate is
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, Assist
870
- Mode, Agent Mode or a plain chat turn) returns to the prompt with
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 Assist engine is built lazily on the first assist-mode turn and
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
- run(ui)
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()
@@ -1,6 +1,6 @@
1
1
  """Code Mode state (v6.2.0): workspace-aware coding agent session state.
2
2
 
3
- Code Mode is Assist Mode sharpened for the current project:
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
@@ -1,8 +1,10 @@
1
- """Legacy commands: /agent and /permission.
1
+ """Agent Mode commands: /agent and /permission (v8.2.5).
2
2
 
3
- The old Agent Mode was merged into Assist Mode — /agent now routes there.
4
- /permission keeps its dedicated interactive picker; /index and /tools stay
5
- as inspection commands.
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", "Legacy alias for Assist Mode. Usage: /agent [on|off]")
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 Assist permission mode",
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 in Assist Mode")
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="Assist Tools"
98
+ columns_grid(rows, ("seed.primary", "seed.text")), title="Agent Tools"
99
99
  )
100
100
  return CommandResult()