seedcode-cli 6.2.5__py3-none-any.whl → 7.1.0__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 +5 -2
- seedcode/_default_key_template.py +1 -1
- seedcode/app.py +484 -48
- seedcode/codemode.py +48 -8
- seedcode/commands/__init__.py +19 -1
- seedcode/commands/about.py +10 -1
- seedcode/commands/agent.py +2 -1
- seedcode/commands/assist.py +2 -1
- seedcode/commands/chat.py +3 -3
- seedcode/commands/codemode.py +10 -1
- seedcode/commands/desktop.py +2 -1
- seedcode/commands/mode.py +3 -1
- seedcode/commands/session.py +137 -0
- seedcode/commands/status.py +47 -4
- seedcode/computer/browser.py +15 -2
- seedcode/computer/browser_engine.py +23 -3
- seedcode/computer/controller.py +16 -3
- seedcode/computer/lifecycle_guard.py +173 -0
- seedcode/computer/windows.py +13 -1
- seedcode/core/agent.py +239 -19
- seedcode/core/models.py +37 -1
- seedcode/core/session.py +967 -0
- seedcode/core/tasks.py +902 -0
- seedcode/defaults.py +8 -4
- seedcode/tools/base.py +11 -1
- seedcode/tools/filesystem.py +13 -3
- seedcode/tools/terminal.py +4 -1
- seedcode/tools/textio.py +10 -2
- seedcode/ui/__init__.py +100 -18
- seedcode/ui/codemode_header.py +348 -0
- seedcode/ui/dashboard.py +326 -68
- seedcode/ui/layout.py +39 -0
- seedcode/ui/renderer.py +23 -4
- seedcode/ui/session_view.py +280 -0
- seedcode/ui/tasks.py +698 -0
- seedcode/utils/text.py +107 -0
- {seedcode_cli-6.2.5.dist-info → seedcode_cli-7.1.0.dist-info}/METADATA +242 -46
- {seedcode_cli-6.2.5.dist-info → seedcode_cli-7.1.0.dist-info}/RECORD +41 -34
- seedcode/assets/logo.txt +0 -15
- {seedcode_cli-6.2.5.dist-info → seedcode_cli-7.1.0.dist-info}/WHEEL +0 -0
- {seedcode_cli-6.2.5.dist-info → seedcode_cli-7.1.0.dist-info}/entry_points.txt +0 -0
- {seedcode_cli-6.2.5.dist-info → seedcode_cli-7.1.0.dist-info}/licenses/LICENSE +0 -0
seedcode/__init__.py
CHANGED
|
@@ -6,9 +6,12 @@ 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__ = "
|
|
10
|
-
__author__ = "Al
|
|
9
|
+
__version__ = "7.1.0"
|
|
10
|
+
__author__ = "Al Shahriar Sowan"
|
|
11
11
|
__publisher__ = "Eagox Studio"
|
|
12
|
+
# Current author/company links, surfaced by /about and the README credits.
|
|
13
|
+
__portfolio_url__ = "https://alshahriarsayon.vercel.app/"
|
|
14
|
+
__studio_url__ = "https://eagoxstudio.vercel.app/"
|
|
12
15
|
|
|
13
16
|
APP_NAME = "Seed Code"
|
|
14
17
|
TAGLINE = "Plant ideas. Grow code."
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Embedded built-in credential for the Default provider (
|
|
1
|
+
"""Embedded built-in credential for the Default provider (v7.1.0 artifact).
|
|
2
2
|
|
|
3
3
|
This file is NOT committed to source control. The release build generates
|
|
4
4
|
``seedcode/_default_key.py`` from the local ``.env`` (see
|
seedcode/app.py
CHANGED
|
@@ -30,7 +30,7 @@ from .commands.theme import pick_theme
|
|
|
30
30
|
from .config import load_config
|
|
31
31
|
from .core.agent import AgentEngine, strip_tool_blocks
|
|
32
32
|
from .core.chat import ChatEngine, ChatError
|
|
33
|
-
from .core.lifecycle import lifecycle
|
|
33
|
+
from .core.lifecycle import LifecycleError, lifecycle
|
|
34
34
|
from .core.models import AppConfig
|
|
35
35
|
from .core.providers import PROVIDERS, provider_label, provider_ready
|
|
36
36
|
from .core.providers.freemodel import AUTO_MODEL
|
|
@@ -40,6 +40,7 @@ from .ui import UI
|
|
|
40
40
|
from .ui.badges import badge_for_status
|
|
41
41
|
from .ui.menu import MenuItem, run_menu
|
|
42
42
|
from .ui.reference import CONTROLS_HINT, INPUT_HINT
|
|
43
|
+
from .ui.tasks import TaskFlow, TaskState
|
|
43
44
|
from .ui.textbox import prompt_label
|
|
44
45
|
from .ui.theme import pt_style, set_active_theme
|
|
45
46
|
from .utils.logger import get_logger
|
|
@@ -84,7 +85,17 @@ def _release_desktop_resources() -> None:
|
|
|
84
85
|
Cleanup clears caches and closes driver connections only. It must never
|
|
85
86
|
terminate SeedCode — historically a finally-block that "cleaned up" by
|
|
86
87
|
exiting was one of the auto-exit paths.
|
|
88
|
+
|
|
89
|
+
v7.1.0 additionally guarantees the other direction: cleanup never closes a
|
|
90
|
+
user-facing application. Applications Seed Code opened stay open, which is
|
|
91
|
+
why this hook releases only internal resources (sockets, drivers, caches).
|
|
87
92
|
"""
|
|
93
|
+
try:
|
|
94
|
+
from .computer import lifecycle_guard
|
|
95
|
+
|
|
96
|
+
lifecycle_guard.release_internal() # logs what is deliberately left open
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
88
99
|
try:
|
|
89
100
|
from .computer.browser_cdp import reset as _cdp_reset
|
|
90
101
|
|
|
@@ -256,56 +267,469 @@ def _handle_chat(ui: UI, engine: ChatEngine, history: HistoryStore, text: str) -
|
|
|
256
267
|
ui.dim("(no response)")
|
|
257
268
|
|
|
258
269
|
|
|
259
|
-
|
|
260
|
-
"""
|
|
270
|
+
class _TaskPresenter:
|
|
271
|
+
"""Bridges engine activity to the live step view (and plain narration).
|
|
261
272
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
273
|
+
One presenter per chat session; ``flow`` is set for the duration of a turn
|
|
274
|
+
so the engine's callbacks land in that turn's task view. Everything here is
|
|
275
|
+
best-effort: the presenter can never fail a task.
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
# Live command output is echoed compactly; the full output still goes to
|
|
279
|
+
# the model and the log file, and errors are never hidden.
|
|
280
|
+
MAX_ECHO_LINES = 8
|
|
281
|
+
|
|
282
|
+
def __init__(self, ui: UI) -> None:
|
|
283
|
+
self.ui = ui
|
|
284
|
+
self.flow: TaskFlow | None = None
|
|
285
|
+
self._echo_lines = 0
|
|
286
|
+
self._echo_elided = False
|
|
287
|
+
|
|
288
|
+
# --- one command's live output ------------------------------------------
|
|
289
|
+
def reset_echo(self) -> None:
|
|
290
|
+
self._echo_lines = 0
|
|
291
|
+
self._echo_elided = False
|
|
292
|
+
|
|
293
|
+
def on_output(self, line: str) -> None:
|
|
294
|
+
"""Compact live echo: the first few lines, then a single elision note."""
|
|
295
|
+
if self._echo_lines < self.MAX_ECHO_LINES:
|
|
296
|
+
self.ui.dim(f" │ {line[:200]}")
|
|
297
|
+
elif not self._echo_elided:
|
|
298
|
+
self._echo_elided = True
|
|
299
|
+
self.ui.dim(
|
|
300
|
+
f" │ … output continues (first {self.MAX_ECHO_LINES} lines shown; "
|
|
301
|
+
"the agent and the log file get it all)"
|
|
302
|
+
)
|
|
303
|
+
self._echo_lines += 1
|
|
304
|
+
|
|
305
|
+
# --- narration (human-readable) -----------------------------------------
|
|
306
|
+
def on_event(self, kind: str, detail: str) -> None:
|
|
307
|
+
flow = self.flow
|
|
308
|
+
if kind == "say" and flow is not None:
|
|
309
|
+
flow.observe_text(detail)
|
|
310
|
+
return
|
|
311
|
+
if kind == "error":
|
|
312
|
+
self.ui.dim(f" ✖ {detail.splitlines()[0][:120]}")
|
|
313
|
+
elif kind == "limit":
|
|
314
|
+
self.ui.warning(f"Assist stopped: {detail}")
|
|
315
|
+
elif kind == "call" and flow is None:
|
|
316
|
+
# With a task view on screen the step list already shows the tool
|
|
317
|
+
# work; without one, narrate it (the plain/legacy experience).
|
|
318
|
+
self.ui.dim(f" ⚒ {detail}")
|
|
319
|
+
|
|
320
|
+
# --- structured activity -> step states ---------------------------------
|
|
321
|
+
def on_step(self, payload: dict) -> None:
|
|
322
|
+
flow = self.flow
|
|
323
|
+
if flow is None:
|
|
324
|
+
return
|
|
325
|
+
phase = str(payload.get("phase") or "")
|
|
326
|
+
name = str(payload.get("name") or "")
|
|
327
|
+
args = payload.get("args") or {}
|
|
328
|
+
if phase == "tool_start":
|
|
329
|
+
self.reset_echo()
|
|
330
|
+
flow.observe_tool_start(name, args)
|
|
331
|
+
elif phase == "tool_done":
|
|
332
|
+
flow.observe_tool_done(
|
|
333
|
+
name,
|
|
334
|
+
bool(payload.get("ok")),
|
|
335
|
+
str(payload.get("output") or ""),
|
|
336
|
+
args,
|
|
337
|
+
)
|
|
338
|
+
elif phase == "say":
|
|
339
|
+
flow.observe_text(str(payload.get("text") or ""))
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# --- persistent session events (Code Mode) ------------------------------
|
|
343
|
+
def on_session_event(self, kind: str, detail: str) -> None:
|
|
344
|
+
"""Render compact progress for the persistent session (v7.1.0).
|
|
345
|
+
|
|
346
|
+
Only what the user needs: the plan, the task that just finished, the
|
|
347
|
+
live action, a repair in progress, and the outcome. Raw model output is
|
|
348
|
+
never dumped here.
|
|
349
|
+
"""
|
|
350
|
+
from .core import session as session_mod
|
|
351
|
+
|
|
352
|
+
flow = self.flow
|
|
353
|
+
session = session_mod.current_session()
|
|
354
|
+
if kind == "plan":
|
|
355
|
+
self.ui.dim(" planning the work…")
|
|
356
|
+
return
|
|
357
|
+
if kind == "plan_ready":
|
|
358
|
+
self.ui.dim(f" plan: {detail}")
|
|
359
|
+
elif kind == "task_start":
|
|
360
|
+
active = None
|
|
361
|
+
if flow is not None and session is not None:
|
|
362
|
+
flow.attach_plan(session.graph)
|
|
363
|
+
active = session.graph.active() or session.graph.next_task()
|
|
364
|
+
if active is not None:
|
|
365
|
+
flow.set_progress(active.id, session.graph.total, active.title)
|
|
366
|
+
# The live action line reports the real phase (v7.1.0).
|
|
367
|
+
flow.set_activity(f"Working on: {active.title}")
|
|
368
|
+
self.ui.dim(f" → {detail}")
|
|
369
|
+
elif kind == "verify":
|
|
370
|
+
# A real phase, not decoration: the session is checking acceptance
|
|
371
|
+
# criteria against observed evidence right now.
|
|
372
|
+
if flow is not None:
|
|
373
|
+
flow.set_activity("Verifying acceptance criteria")
|
|
374
|
+
elif kind == "task_done":
|
|
375
|
+
if flow is not None and session is not None:
|
|
376
|
+
# Refresh the checklist so the finished task shows as done.
|
|
377
|
+
flow.attach_plan(session.graph)
|
|
378
|
+
self.ui.success(detail)
|
|
379
|
+
elif kind == "recovery":
|
|
380
|
+
if flow is not None:
|
|
381
|
+
flow.set_activity("Diagnosing the failure and repairing")
|
|
382
|
+
self.ui.warning(detail)
|
|
383
|
+
elif kind == "task_blocked":
|
|
384
|
+
self.ui.warning(detail)
|
|
385
|
+
elif kind == "task_failed":
|
|
386
|
+
self.ui.error(detail)
|
|
387
|
+
elif kind == "final_verify":
|
|
388
|
+
self.ui.dim(" final verification…")
|
|
389
|
+
if flow is not None:
|
|
390
|
+
flow.set_activity("Verifying the project (tests / build)")
|
|
391
|
+
elif kind in ("retry", "continue"):
|
|
392
|
+
self.ui.dim(f" {detail}")
|
|
393
|
+
if flow is not None and session is not None:
|
|
394
|
+
flow.note_call(session.model_calls)
|
|
395
|
+
state = {
|
|
396
|
+
"session_paused": "paused",
|
|
397
|
+
"session_cancelled": "cancelled",
|
|
398
|
+
"session_failed": "failed",
|
|
399
|
+
"session_completed": "completed",
|
|
400
|
+
}.get(kind)
|
|
401
|
+
if flow is not None and state is not None:
|
|
402
|
+
if session is not None:
|
|
403
|
+
flow.attach_plan(session.graph) # final, real plan state
|
|
404
|
+
flow.set_state(state)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _advance(lc, method: str) -> None:
|
|
408
|
+
"""Best-effort lifecycle transition around one turn.
|
|
409
|
+
|
|
410
|
+
The REPL owns the turn span (``PLANNING`` → … → ``IDLE``); this lets the
|
|
411
|
+
turn *narrate* where it is without ever turning a phase mismatch into a
|
|
412
|
+
task failure — a lifecycle bookkeeping problem must not stop the work.
|
|
413
|
+
"""
|
|
414
|
+
try:
|
|
415
|
+
getattr(lc, method)()
|
|
416
|
+
except LifecycleError:
|
|
417
|
+
_log.debug("lifecycle transition %s skipped", method)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _codemode_enabled() -> bool:
|
|
421
|
+
"""Whether Code Mode is active (the persistent-session path)."""
|
|
422
|
+
try:
|
|
423
|
+
from . import codemode_state as _cms
|
|
424
|
+
|
|
425
|
+
state = _cms.codemode_state()
|
|
426
|
+
return bool(state.enabled and state.workspace is not None)
|
|
427
|
+
except Exception:
|
|
428
|
+
return False
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def resume_codemode_session(ui: UI, config: AppConfig, history=None) -> bool:
|
|
432
|
+
"""Continue a paused/checkpointed Code Mode session (used by /resume).
|
|
433
|
+
|
|
434
|
+
Returns whether a session was actually resumed. The agent is rebuilt for
|
|
435
|
+
the current config (exactly as a fresh turn would), so a resume after a
|
|
436
|
+
provider or permission change still runs with the live settings.
|
|
266
437
|
"""
|
|
438
|
+
from .core import session as session_mod
|
|
439
|
+
from .core.session import CodeSession
|
|
440
|
+
|
|
441
|
+
workspace, store = _codemode_workspace()
|
|
442
|
+
if store is None:
|
|
443
|
+
return False
|
|
444
|
+
agent = _make_agent(ui, config, _TaskPresenter(ui))
|
|
445
|
+
presenter = agent_presenter(agent)
|
|
446
|
+
session = CodeSession.resume(
|
|
447
|
+
agent,
|
|
448
|
+
store=store,
|
|
449
|
+
workspace=workspace,
|
|
450
|
+
on_event=presenter.on_session_event if presenter is not None else None,
|
|
451
|
+
)
|
|
452
|
+
if session is None:
|
|
453
|
+
return False
|
|
454
|
+
session.control.clear()
|
|
455
|
+
_handle_codemode(
|
|
456
|
+
ui,
|
|
457
|
+
agent,
|
|
458
|
+
history if history is not None else HistoryStore(provider_id=config.provider),
|
|
459
|
+
session.request,
|
|
460
|
+
presenter,
|
|
461
|
+
session=session,
|
|
462
|
+
)
|
|
463
|
+
session_mod.set_current_session(None)
|
|
464
|
+
return True
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def agent_presenter(agent: AgentEngine) -> _TaskPresenter | None:
|
|
468
|
+
"""The presenter an engine was built with (used by /resume)."""
|
|
469
|
+
presenter = getattr(agent, "presenter", None)
|
|
470
|
+
return presenter if isinstance(presenter, _TaskPresenter) else None
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _task_mode_label(config: AppConfig) -> str:
|
|
474
|
+
"""The mode named in a task header (Code Mode sharpens Assist Mode)."""
|
|
475
|
+
try:
|
|
476
|
+
from .codemode_state import codemode_state
|
|
477
|
+
|
|
478
|
+
if codemode_state().enabled:
|
|
479
|
+
return "Code Mode"
|
|
480
|
+
except Exception:
|
|
481
|
+
pass
|
|
482
|
+
return "Assist Mode"
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _save_codemode_summary(agent: AgentEngine, text: str, outcome: str) -> None:
|
|
486
|
+
"""v6.2.0: Code Mode sessions leave a compact summary in .seedcode/sessions."""
|
|
487
|
+
try:
|
|
488
|
+
from . import codemode_state as _cms
|
|
489
|
+
|
|
490
|
+
state = _cms.codemode_state()
|
|
491
|
+
if state.enabled and state.store is not None:
|
|
492
|
+
state.store.save_session_summary({
|
|
493
|
+
"goal": text[:200],
|
|
494
|
+
"outcome": (outcome or "(no response)")[:400],
|
|
495
|
+
"tool_calls": sum(1 for m in agent.messages if m.role == "tool"),
|
|
496
|
+
})
|
|
497
|
+
except Exception:
|
|
498
|
+
pass
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _codemode_workspace():
|
|
502
|
+
"""The active Code Mode workspace + store, or (CWD, None) when unavailable."""
|
|
503
|
+
try:
|
|
504
|
+
from . import codemode_state as _cms
|
|
505
|
+
|
|
506
|
+
state = _cms.codemode_state()
|
|
507
|
+
if state.enabled and state.workspace is not None:
|
|
508
|
+
return state.workspace, state.store
|
|
509
|
+
except Exception:
|
|
510
|
+
_log.exception("could not read the Code Mode workspace")
|
|
511
|
+
from pathlib import Path
|
|
512
|
+
|
|
513
|
+
return Path.cwd(), None
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def _handle_codemode(
|
|
517
|
+
ui: UI,
|
|
518
|
+
agent: AgentEngine,
|
|
519
|
+
history: HistoryStore,
|
|
520
|
+
text: str,
|
|
521
|
+
presenter: _TaskPresenter | None = None,
|
|
522
|
+
*,
|
|
523
|
+
session=None,
|
|
524
|
+
) -> None:
|
|
525
|
+
"""Run one persistent Code Mode session: plan → tasks → verification.
|
|
526
|
+
|
|
527
|
+
Unlike a single agent turn, this keeps working until every planned task is
|
|
528
|
+
verified (or the session is paused, cancelled, or blocked), then runs final
|
|
529
|
+
verification. The task view shows the plan and real progress, and the CLI
|
|
530
|
+
is ready for the next task in every outcome.
|
|
531
|
+
"""
|
|
532
|
+
from .core import session as session_mod
|
|
533
|
+
from .core.session import CodeSession, SessionStatus
|
|
534
|
+
|
|
267
535
|
lc = lifecycle()
|
|
536
|
+
workspace, store = _codemode_workspace()
|
|
537
|
+
flow = TaskFlow.for_ui(ui, mode_label="Code Mode", task=text)
|
|
538
|
+
if presenter is not None:
|
|
539
|
+
presenter.flow = flow
|
|
540
|
+
if flow is not None:
|
|
541
|
+
flow.begin()
|
|
542
|
+
flow.start()
|
|
543
|
+
|
|
544
|
+
if session is None:
|
|
545
|
+
session = CodeSession(
|
|
546
|
+
agent,
|
|
547
|
+
workspace=workspace,
|
|
548
|
+
request=text,
|
|
549
|
+
store=store,
|
|
550
|
+
on_event=presenter.on_session_event if presenter is not None else None,
|
|
551
|
+
)
|
|
552
|
+
elif presenter is not None:
|
|
553
|
+
session.set_observer(presenter.on_session_event)
|
|
554
|
+
|
|
555
|
+
session_mod.set_current_session(session)
|
|
556
|
+
status = SessionStatus.IDLE
|
|
557
|
+
try:
|
|
558
|
+
_advance(lc, "to_executing")
|
|
559
|
+
status = session.run()
|
|
560
|
+
_advance(lc, "to_verifying")
|
|
561
|
+
except KeyboardInterrupt:
|
|
562
|
+
session_mod.stop(session)
|
|
563
|
+
status = SessionStatus.CANCELLED
|
|
564
|
+
session.reason = "cancelled by the user"
|
|
565
|
+
except Exception as exc: # an engine bug must not kill the session either
|
|
566
|
+
status = SessionStatus.FAILED
|
|
567
|
+
session.reason = f"{type(exc).__name__}: {exc}"
|
|
568
|
+
_log.exception("code mode session failed")
|
|
569
|
+
finally:
|
|
570
|
+
if presenter is not None:
|
|
571
|
+
presenter.flow = None
|
|
572
|
+
|
|
573
|
+
if session.store is not None and status is not SessionStatus.PAUSED:
|
|
574
|
+
_save_codemode_summary(agent, text, session.reason or status.value)
|
|
575
|
+
|
|
576
|
+
if status is SessionStatus.COMPLETED:
|
|
577
|
+
_advance(lc, "to_responding")
|
|
578
|
+
history.save(agent.transcript)
|
|
579
|
+
_report_session_summary(ui, session)
|
|
580
|
+
if flow is not None:
|
|
581
|
+
flow.finish("completed")
|
|
582
|
+
return
|
|
583
|
+
|
|
584
|
+
if status is SessionStatus.PAUSED:
|
|
585
|
+
# State is preserved and checkpointed: /resume continues this session.
|
|
586
|
+
if flow is not None:
|
|
587
|
+
flow.set_state("paused")
|
|
588
|
+
flow.stop()
|
|
589
|
+
ui.warning("Session paused — state saved. Send /resume to continue.")
|
|
590
|
+
session_mod.set_current_session(session)
|
|
591
|
+
return
|
|
592
|
+
|
|
593
|
+
session_mod.set_current_session(None)
|
|
594
|
+
if status is SessionStatus.CANCELLED:
|
|
595
|
+
ui.blank()
|
|
596
|
+
# v7.1.0: state is checkpointed on the way out, so say so — the session
|
|
597
|
+
# is resumable rather than lost (the terminal itself stays open).
|
|
598
|
+
ui.dim(
|
|
599
|
+
"(session stopped — the plan, completed work and the project files "
|
|
600
|
+
"were kept; /resume continues from the checkpoint)"
|
|
601
|
+
)
|
|
602
|
+
if flow is not None:
|
|
603
|
+
flow.finish("cancelled")
|
|
604
|
+
return
|
|
605
|
+
reason = session.reason or "the session could not finish the plan"
|
|
606
|
+
ui.error(reason)
|
|
607
|
+
if flow is not None:
|
|
608
|
+
flow.finish("failed", reason)
|
|
268
609
|
|
|
610
|
+
|
|
611
|
+
def _report_session_summary(ui: UI, session) -> None:
|
|
612
|
+
"""The final block: the evidence, in one compact summary (v7.1.0).
|
|
613
|
+
|
|
614
|
+
Completion is evidence-gated, so the closing block reports the evidence
|
|
615
|
+
itself rather than a restatement of the model's replies: the verification
|
|
616
|
+
result, the tests that actually ran, the commands that actually succeeded,
|
|
617
|
+
and the files the engine really touched. Lines that have nothing to report
|
|
618
|
+
are omitted instead of being filled in with a placeholder.
|
|
619
|
+
"""
|
|
620
|
+
graph = session.graph
|
|
621
|
+
done, total = graph.progress()
|
|
622
|
+
ui.success(f"Project completed — {done}/{total} tasks verified")
|
|
623
|
+
evidence = session.session_evidence
|
|
624
|
+
if evidence.tests:
|
|
625
|
+
# What matters is the final state: the last test run passed.
|
|
626
|
+
passed = evidence.tests[-1].ok
|
|
627
|
+
ui.dim(f" ✓ Verification: {'accepted' if passed else 'tests NOT passing'}")
|
|
628
|
+
ui.dim(f" ✓ Tests: {'passed' if passed else 'FAILED'}")
|
|
629
|
+
else:
|
|
630
|
+
ui.dim(" ✓ Verification: accepted (no test run in this project)")
|
|
631
|
+
if session.state.changed_files:
|
|
632
|
+
ui.dim(f" ✓ Files: {len(session.state.changed_files)} affected")
|
|
633
|
+
commands = evidence.commands
|
|
634
|
+
if commands:
|
|
635
|
+
ok = sum(1 for record in commands if record.ok)
|
|
636
|
+
ui.dim(f" ✓ Commands: {ok}/{len(commands)} ok")
|
|
637
|
+
if session.state.inspected:
|
|
638
|
+
ui.dim(f" • Inspected: {len(session.state.inspected)} item(s)")
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _handle_agent(
|
|
642
|
+
ui: UI,
|
|
643
|
+
agent: AgentEngine,
|
|
644
|
+
history: HistoryStore,
|
|
645
|
+
text: str,
|
|
646
|
+
presenter: _TaskPresenter | None = None,
|
|
647
|
+
) -> None:
|
|
648
|
+
"""Run one full Assist/Code turn (tool loop) and render the final answer.
|
|
649
|
+
|
|
650
|
+
The turn is shown as a compact live task flow whose step states follow real
|
|
651
|
+
engine activity (see :mod:`seedcode.ui.tasks`) — never a fake progress bar.
|
|
652
|
+
|
|
653
|
+
Lifecycle note: every outcome — success, tool failure, provider error,
|
|
654
|
+
Ctrl+C — ends by printing a status, finishing the task view and RETURNING.
|
|
655
|
+
Nothing here may terminate the process; the surrounding ``task_span``
|
|
656
|
+
guarantees the lifecycle returns to IDLE and the REPL keeps prompting, and
|
|
657
|
+
the explicit task-flow finish line always says the CLI is ready for more.
|
|
658
|
+
"""
|
|
659
|
+
if _codemode_enabled():
|
|
660
|
+
_handle_codemode(ui, agent, history, text, presenter)
|
|
661
|
+
return
|
|
662
|
+
|
|
663
|
+
lc = lifecycle()
|
|
664
|
+
flow = TaskFlow.for_ui(
|
|
665
|
+
ui, mode_label=_task_mode_label(agent.config), task=text
|
|
666
|
+
)
|
|
667
|
+
if presenter is not None:
|
|
668
|
+
presenter.flow = flow
|
|
669
|
+
if flow is not None:
|
|
670
|
+
flow.begin()
|
|
671
|
+
flow.start()
|
|
672
|
+
|
|
673
|
+
outcome = "completed"
|
|
674
|
+
reason = ""
|
|
675
|
+
reply: str | None = None
|
|
269
676
|
try:
|
|
270
|
-
|
|
271
|
-
|
|
677
|
+
if flow is None: # the task view already shows that work is happening
|
|
678
|
+
with ui.thinking("Working"):
|
|
679
|
+
_advance(lc, "to_executing")
|
|
680
|
+
reply = agent.run_turn(text)
|
|
681
|
+
else:
|
|
682
|
+
_advance(lc, "to_executing")
|
|
272
683
|
reply = agent.run_turn(text)
|
|
273
|
-
lc
|
|
684
|
+
_advance(lc, "to_verifying")
|
|
274
685
|
except ChatError as exc:
|
|
275
|
-
|
|
276
|
-
return
|
|
686
|
+
outcome, reason = "failed", str(exc)
|
|
277
687
|
except KeyboardInterrupt:
|
|
278
|
-
# Ctrl+C aborts the remaining
|
|
279
|
-
|
|
280
|
-
|
|
688
|
+
# Ctrl+C aborts the remaining steps; work already done stays.
|
|
689
|
+
outcome = "cancelled"
|
|
690
|
+
except Exception as exc: # an engine bug must not kill the session either
|
|
691
|
+
outcome, reason = "failed", f"{type(exc).__name__}: {exc}"
|
|
692
|
+
_log.exception("assist turn failed")
|
|
693
|
+
finally:
|
|
694
|
+
if presenter is not None:
|
|
695
|
+
presenter.flow = None
|
|
696
|
+
|
|
697
|
+
if outcome != "completed":
|
|
698
|
+
if reason:
|
|
699
|
+
ui.error(reason)
|
|
700
|
+
else:
|
|
701
|
+
ui.blank()
|
|
702
|
+
ui.dim("(task cancelled — completed tool actions were kept)")
|
|
703
|
+
_save_codemode_summary(agent, text, reason or "task cancelled")
|
|
704
|
+
if flow is not None:
|
|
705
|
+
flow.finish(outcome, reason)
|
|
281
706
|
return
|
|
282
707
|
|
|
283
|
-
lc
|
|
284
|
-
|
|
708
|
+
_advance(lc, "to_responding")
|
|
709
|
+
if flow is not None:
|
|
710
|
+
flow.update("verify", TaskState.RUNNING, "checking the result")
|
|
711
|
+
# Hand the screen back before streaming the answer: one live display at
|
|
712
|
+
# a time, and the answer deserves the full width.
|
|
713
|
+
flow.stop()
|
|
714
|
+
final = strip_tool_blocks(reply or "")
|
|
285
715
|
if final.strip():
|
|
286
716
|
with ui.streaming() as renderer:
|
|
287
717
|
renderer.feed(final)
|
|
288
718
|
else:
|
|
289
719
|
ui.dim("(no response)")
|
|
720
|
+
if flow is not None:
|
|
721
|
+
flow.update("verify", TaskState.COMPLETED, "answer produced")
|
|
290
722
|
history.save(agent.transcript)
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if state.enabled and state.store is not None:
|
|
297
|
-
state.store.save_session_summary({
|
|
298
|
-
"goal": text[:200],
|
|
299
|
-
"outcome": (final.strip() or "(no response)")[:400],
|
|
300
|
-
"tool_calls": sum(
|
|
301
|
-
1 for m in agent.messages if m.role == "tool"
|
|
302
|
-
),
|
|
303
|
-
})
|
|
304
|
-
except Exception:
|
|
305
|
-
pass
|
|
723
|
+
_save_codemode_summary(agent, text, final.strip() or "(no response)")
|
|
724
|
+
if flow is not None:
|
|
725
|
+
# Prints the persistent final block and leaves the prompt ready for the
|
|
726
|
+
# next task — the terminal is never closed from here.
|
|
727
|
+
flow.finish("completed")
|
|
306
728
|
|
|
307
729
|
|
|
308
|
-
def _make_agent(
|
|
730
|
+
def _make_agent(
|
|
731
|
+
ui: UI, config: AppConfig, presenter: "_TaskPresenter | None" = None
|
|
732
|
+
) -> AgentEngine:
|
|
309
733
|
"""Build an Assist engine bound to the CWD and the configured permissions."""
|
|
310
734
|
# Lazy import (matching _make_desktop_session): the optional, platform-
|
|
311
735
|
# specific computer package stays out of app.py's top-level import graph.
|
|
@@ -318,18 +742,20 @@ def _make_agent(ui: UI, config: AppConfig) -> AgentEngine:
|
|
|
318
742
|
if permissions.level.allows_desktop and is_available()[0]:
|
|
319
743
|
permissions.desktop = _make_desktop_session(ui)
|
|
320
744
|
permissions.gate = _make_action_gate(ui)
|
|
321
|
-
# Live terminal output: echo each line a running command prints.
|
|
322
|
-
permissions.on_output = lambda line: ui.dim(f" │ {line[:200]}")
|
|
323
|
-
|
|
324
|
-
def narrate(kind: str, detail: str) -> None:
|
|
325
|
-
if kind == "call":
|
|
326
|
-
ui.dim(f" ⚒ {detail}")
|
|
327
|
-
elif kind == "error":
|
|
328
|
-
ui.dim(f" ✖ {detail.splitlines()[0][:120]}")
|
|
329
|
-
elif kind == "limit":
|
|
330
|
-
ui.warning(f"Assist stopped: {detail}")
|
|
331
745
|
|
|
332
|
-
|
|
746
|
+
presenter = presenter or _TaskPresenter(ui)
|
|
747
|
+
# Live terminal output: a compact echo of what a running command prints.
|
|
748
|
+
permissions.on_output = presenter.on_output
|
|
749
|
+
|
|
750
|
+
engine = AgentEngine(
|
|
751
|
+
config,
|
|
752
|
+
permissions,
|
|
753
|
+
on_event=presenter.on_event,
|
|
754
|
+
on_step=presenter.on_step,
|
|
755
|
+
)
|
|
756
|
+
# Attached so /resume can continue a paused session with the same view.
|
|
757
|
+
engine.presenter = presenter # type: ignore[attr-defined]
|
|
758
|
+
return engine
|
|
333
759
|
|
|
334
760
|
|
|
335
761
|
def _make_action_gate(ui: UI):
|
|
@@ -402,14 +828,22 @@ def _chat_loop(
|
|
|
402
828
|
history: HistoryStore,
|
|
403
829
|
session: PromptSession | _PlainSession,
|
|
404
830
|
) -> None:
|
|
405
|
-
"""Interactive chat until /exit (returns to the main menu).
|
|
831
|
+
"""Interactive chat until /exit (returns to the main menu).
|
|
832
|
+
|
|
833
|
+
Only an explicit exit leaves here: a finished task (Code Mode, Assist
|
|
834
|
+
Mode, Agent Mode or a plain chat turn) returns to the prompt with
|
|
835
|
+
``Ready for next task.`` and the session keeps running.
|
|
836
|
+
"""
|
|
406
837
|
ctx = CommandContext(ui=ui, config=config, engine=engine)
|
|
407
838
|
ui.dim(INPUT_HINT)
|
|
408
839
|
|
|
409
840
|
# The Assist engine is built lazily on the first assist-mode turn and
|
|
410
841
|
# rebuilt when the permission or desktop mode changes (its system
|
|
411
|
-
# prompt and permission gates reflect both).
|
|
842
|
+
# prompt and permission gates reflect both). The presenter that owns the
|
|
843
|
+
# live step view lives for the whole chat session, so each turn can bind
|
|
844
|
+
# its own task flow to the same engine callbacks.
|
|
412
845
|
agent: AgentEngine | None = None
|
|
846
|
+
presenter = _TaskPresenter(ui)
|
|
413
847
|
agent_perm = config.permission_mode
|
|
414
848
|
# v6.2.0: track Code Mode so a /codemode toggle rebuilds the agent with
|
|
415
849
|
# workspace context (the engine is constructed lazily per turn).
|
|
@@ -481,10 +915,12 @@ def _chat_loop(
|
|
|
481
915
|
or agent_perm != config.permission_mode
|
|
482
916
|
or agent_codemode != codemode_now
|
|
483
917
|
):
|
|
484
|
-
agent = _make_agent(ui, config)
|
|
918
|
+
agent = _make_agent(ui, config, presenter)
|
|
485
919
|
agent_perm = config.permission_mode
|
|
486
920
|
agent_codemode = codemode_now
|
|
487
|
-
|
|
921
|
+
# One task flow per turn: the engine reports real activity into
|
|
922
|
+
# it, and the REPL keeps prompting when the turn ends.
|
|
923
|
+
_handle_agent(ui, agent, history, text, presenter)
|
|
488
924
|
else:
|
|
489
925
|
_handle_chat(ui, engine, history, text)
|
|
490
926
|
|