seedcode-cli 8.1.0__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__ = "8.1.0"
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,13 +150,12 @@ 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).
154
155
 
155
- Code Mode has its own workspace session, so its status must reflect that
156
- session alone. Reading ``agent_mode`` here made the item read ON whenever
157
- Agent Mode was on, even though no Code Mode workspace was active — a live
158
- status that did not match the live state.
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
159
  """
160
160
  try:
161
161
  from .codemode_state import codemode_state
@@ -168,20 +168,25 @@ def _mode_status(config: AppConfig) -> str:
168
168
  def _main_menu(config: AppConfig):
169
169
  """The interactive main menu; returns an action id or None (exit).
170
170
 
171
- v6.2.5 reference layout: the mode/setup actions carry Ctrl+1..Ctrl+5
172
- shortcuts that execute the real handlers (no decorative items), followed
173
- by the chat/setup actions kept from earlier releases. There are exactly
174
- three modes; Assist Mode no longer exists as a menu entry.
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.
175
176
  """
176
177
  provider = PROVIDERS.get(config.provider)
177
178
  badge = badge_for_status(provider.status if provider is not None else "")
178
179
  return run_menu(
179
180
  [
180
- MenuItem("Code Mode", "codemode", status=_mode_status(config), shortcut="1"),
181
- MenuItem("Agent Mode", "agent", shortcut="2"),
182
- MenuItem("Project Memory", "memory", shortcut="3"),
183
- MenuItem("Settings", "settings", shortcut="4"),
184
- MenuItem("Exit", "exit", shortcut="5"),
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"),
185
190
  MenuItem("Start Chat", "chat", status=_model_status(config), badge=badge),
186
191
  MenuItem("Provider", "provider", status=_provider_status(config)),
187
192
  MenuItem("API Key", "apikey", status=_key_status(config)),
@@ -294,6 +299,20 @@ class _TaskPresenter:
294
299
  self._echo_lines = 0
295
300
  self._echo_elided = False
296
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
+
297
316
  # --- one command's live output ------------------------------------------
298
317
  def reset_echo(self) -> None:
299
318
  self._echo_lines = 0
@@ -301,6 +320,8 @@ class _TaskPresenter:
301
320
 
302
321
  def on_output(self, line: str) -> None:
303
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")
304
325
  if self._echo_lines < self.MAX_ECHO_LINES:
305
326
  self.ui.dim(f" │ {line[:200]}")
306
327
  elif not self._echo_elided:
@@ -319,6 +340,7 @@ class _TaskPresenter:
319
340
  return
320
341
  if kind == "error":
321
342
  self.ui.dim(f" ✖ {detail.splitlines()[0][:120]}")
343
+ self._set_status(Status.ERROR, "an operation failed")
322
344
  elif kind == "switching_provider":
323
345
  # A provider request failed and a healthy fallback took over; the
324
346
  # task continues rather than restarting (v7.2.5).
@@ -333,6 +355,7 @@ class _TaskPresenter:
333
355
  # With a task view on screen the step list already shows the tool
334
356
  # work; without one, narrate it (the plain/legacy experience).
335
357
  self.ui.dim(f" ⚒ {detail}")
358
+ self._set_status(Status.EXECUTING, detail)
336
359
 
337
360
  # --- structured activity -> step states ---------------------------------
338
361
  def on_step(self, payload: dict) -> None:
@@ -345,6 +368,7 @@ class _TaskPresenter:
345
368
  if phase == "tool_start":
346
369
  self.reset_echo()
347
370
  flow.observe_tool_start(name, args)
371
+ self._set_status(Status.EXECUTING, activity_for(name, args))
348
372
  elif phase == "tool_done":
349
373
  flow.observe_tool_done(
350
374
  name,
@@ -352,6 +376,7 @@ class _TaskPresenter:
352
376
  str(payload.get("output") or ""),
353
377
  args,
354
378
  )
379
+ self._set_status(Status.WORKING, "continuing")
355
380
  elif phase == "say":
356
381
  flow.observe_text(str(payload.get("text") or ""))
357
382
 
@@ -368,6 +393,21 @@ class _TaskPresenter:
368
393
 
369
394
  flow = self.flow
370
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)
371
411
  if kind == "plan":
372
412
  self.ui.dim(" planning the work…")
373
413
  return
@@ -501,14 +541,7 @@ def agent_presenter(agent: AgentEngine) -> _TaskPresenter | None:
501
541
 
502
542
 
503
543
  def _task_mode_label(config: AppConfig) -> str:
504
- """The mode named in a task header (Chat / Code Mode / Agent Mode)."""
505
- try:
506
- from .codemode_state import codemode_state
507
-
508
- if codemode_state().enabled:
509
- return "Code Mode"
510
- except Exception:
511
- pass
544
+ """The mode named in a task header (Chat Mode / Agent Mode only)."""
512
545
  from .core.modes import active_mode, mode_title
513
546
 
514
547
  return mode_title(active_mode(config))
@@ -566,7 +599,9 @@ def _handle_codemode(
566
599
 
567
600
  lc = lifecycle()
568
601
  workspace, store = _codemode_workspace()
569
- 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)
570
605
  if presenter is not None:
571
606
  presenter.flow = flow
572
607
  if flow is not None:
@@ -1041,6 +1076,286 @@ def _build_chat_session(ui: UI) -> PromptSession:
1041
1076
  return PromptSession(key_bindings=kb)
1042
1077
 
1043
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
+
1044
1359
  def run(ui: UI) -> None:
1045
1360
  """Show the startup dashboard, drop straight into chat, then the menu.
1046
1361
 
@@ -1122,8 +1437,6 @@ def run(ui: UI) -> None:
1122
1437
  apikey_menu(ui, config)
1123
1438
  elif choice == "model":
1124
1439
  select_model(ui, config)
1125
- elif choice == "codemode":
1126
- _dispatch_command(ui, config, engine, "/codemode on")
1127
1440
  elif choice == "agent":
1128
1441
  _dispatch_command(ui, config, engine, "/agent on")
1129
1442
  elif choice == "memory":
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,10 +1,10 @@
1
- """Agent Mode commands: /agent and /permission (v8.1.0).
1
+ """Agent Mode commands: /agent and /permission (v8.2.5).
2
2
 
3
- /agent is the primary route into Agent Mode — the single general-purpose
4
- execution mode that the retired Assist Mode folded into. /assist and
5
- /desktop remain accepted aliases so an existing habit keeps working.
6
- /permission keeps its dedicated interactive picker; /index and /tools stay
7
- 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.
8
8
  """
9
9
 
10
10
  from __future__ import annotations
@@ -1,14 +1,15 @@
1
- """Agent Mode: unified AI + tool/computer execution (v8.1.0).
1
+ """Agent Mode: the unified autonomous workspace/coding agent (v8.2.5).
2
2
 
3
3
  /agent on — selects Agent Mode: the full capability set (AI, filesystem,
4
4
  terminal, git, browser, keyboard, mouse, windows, vision, OCR,
5
- desktop automation).
5
+ desktop automation) plus the workspace coding capability
6
+ (``.seedcode`` project memory + index, plan → execute → verify).
6
7
  /agent off — back to plain Chat Mode.
7
8
 
8
9
  Agent Mode is the single general-purpose execution mode. The retired Assist
9
- Mode is this mode under its old name: ``/assist`` and ``/desktop`` still work
10
- and route here, but they never create a fourth mode. Code Mode is the
11
- workspace-aware coding agent (see :mod:`seedcode.commands.codemode`).
10
+ Mode and Code Mode are this mode under their old names: ``/assist``,
11
+ ``/desktop`` and ``/codemode`` still work and route here, but they never
12
+ create a third mode.
12
13
  """
13
14
 
14
15
  from __future__ import annotations
@@ -93,7 +94,12 @@ def _ocr_available() -> bool:
93
94
 
94
95
 
95
96
  def enable_assist(ui, config) -> None:
96
- """Select Agent Mode: the full capability set in one switch."""
97
+ """Select Agent Mode: the full capability set in one switch.
98
+
99
+ Agent Mode is the unified workspace/coding agent: turning it on also
100
+ activates the workspace capability (``.seedcode`` memory + index) so the
101
+ former Code Mode behaviour is available automatically inside Agent Mode.
102
+ """
97
103
  config.mode = "agent"
98
104
 
99
105
  # Desktop capabilities require the Computer Engine. When available, Agent
@@ -107,11 +113,17 @@ def enable_assist(ui, config) -> None:
107
113
  )
108
114
  save_config(config)
109
115
 
116
+ # The workspace coding capability (former Code Mode) is native to Agent
117
+ # Mode: activate it for the current directory automatically.
118
+ workspace_ready = _ensure_workspace()
119
+
110
120
  ui.success("Agent Mode ON")
111
121
  ui.blank()
112
122
  ui.panel(capability_table(desktop_ok), title="Agent Mode")
113
123
  if not desktop_ok:
114
124
  ui.dim(f"Desktop capabilities unavailable: {desktop_reason}")
125
+ if workspace_ready:
126
+ ui.dim("Project workspace ready — .seedcode memory + index active.")
115
127
  ui.blank()
116
128
 
117
129
  level_label = "desktop" if desktop_ok else "workspace"
@@ -124,6 +136,25 @@ def enable_assist(ui, config) -> None:
124
136
  ui.dim("The AI picks the right tools for each task automatically.")
125
137
 
126
138
 
139
+ def _ensure_workspace() -> bool:
140
+ """Activate the ``.seedcode`` workspace capability for the CWD.
141
+
142
+ Best-effort: a filesystem problem here must never stop Agent Mode from
143
+ being selected. Returns whether the workspace is now active.
144
+ """
145
+ try:
146
+ from pathlib import Path
147
+
148
+ from .. import codemode_state as cms
149
+
150
+ if cms.codemode_state().enabled:
151
+ return True
152
+ cms.enable(Path.cwd())
153
+ return True
154
+ except Exception:
155
+ return False
156
+
157
+
127
158
  def request_session_permissions(ui) -> bool:
128
159
  """Request every routine desktop permission for the session, in one prompt.
129
160
 
@@ -169,13 +200,25 @@ def request_session_permissions(ui) -> bool:
169
200
 
170
201
 
171
202
  def disable_assist(ui, config) -> None:
172
- """Leave Agent Mode: back to plain Chat."""
203
+ """Leave Agent Mode: back to plain Chat.
204
+
205
+ The workspace capability is deactivated too, but the ``.seedcode`` memory
206
+ stays on disk for the next Agent Mode session.
207
+ """
173
208
  from ..computer.permissions import session_permissions
174
209
 
175
210
  config.mode = "chat"
176
211
  # Drop back to the safe editing level (removes desktop capability).
177
212
  config.permission_mode = PermissionMode.WORKSPACE.value_str
178
213
  save_config(config)
214
+ # The workspace coding capability (former Code Mode) belongs to Agent Mode.
215
+ try:
216
+ from .. import codemode_state as cms
217
+
218
+ if cms.codemode_state().enabled:
219
+ cms.disable()
220
+ except Exception:
221
+ pass
179
222
  # Forget the session-wide desktop grant: turning Agent Mode back on asks again.
180
223
  session_permissions().reset()
181
224
  ui.success("Agent Mode OFF — back to plain chat")