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 +1 -1
- seedcode/app.py +340 -27
- seedcode/cli.py +29 -1
- seedcode/commands/agent.py +6 -6
- seedcode/commands/assist.py +50 -7
- seedcode/commands/chat.py +11 -8
- seedcode/commands/codemode.py +20 -23
- seedcode/commands/mode.py +16 -16
- seedcode/core/models.py +7 -5
- seedcode/core/modes.py +37 -33
- seedcode/ui/__init__.py +4 -0
- seedcode/ui/codemode_header.py +10 -3
- seedcode/ui/content.py +421 -0
- seedcode/ui/dashboard.py +7 -7
- seedcode/ui/header.py +325 -0
- seedcode/ui/state.py +286 -0
- seedcode/ui/statusbar.py +7 -3
- seedcode/ui/tasks.py +16 -5
- seedcode/ui/theme.py +4 -0
- seedcode/ui/tui.py +1212 -0
- {seedcode_cli-8.1.0.dist-info → seedcode_cli-8.2.5.dist-info}/METADATA +165 -71
- {seedcode_cli-8.1.0.dist-info → seedcode_cli-8.2.5.dist-info}/RECORD +25 -21
- {seedcode_cli-8.1.0.dist-info → seedcode_cli-8.2.5.dist-info}/WHEEL +0 -0
- {seedcode_cli-8.1.0.dist-info → seedcode_cli-8.2.5.dist-info}/entry_points.txt +0 -0
- {seedcode_cli-8.1.0.dist-info → seedcode_cli-8.2.5.dist-info}/licenses/LICENSE +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__ = "8.
|
|
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,13 +150,12 @@ 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).
|
|
154
155
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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("
|
|
181
|
-
MenuItem(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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/commands/agent.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
"""Agent Mode commands: /agent and /permission (v8.
|
|
1
|
+
"""Agent Mode commands: /agent and /permission (v8.2.5).
|
|
2
2
|
|
|
3
|
-
/agent is the primary route into Agent Mode — the
|
|
4
|
-
|
|
5
|
-
/desktop remain accepted aliases so an existing
|
|
6
|
-
/permission keeps its dedicated interactive picker;
|
|
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
|
seedcode/commands/assist.py
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
"""Agent Mode: unified
|
|
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
|
|
10
|
-
and route here, but they never
|
|
11
|
-
|
|
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")
|