delos-cli 2.0.0__tar.gz → 2.0.2__tar.gz

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.
Files changed (110) hide show
  1. {delos_cli-2.0.0 → delos_cli-2.0.2}/.gitignore +35 -4
  2. {delos_cli-2.0.0 → delos_cli-2.0.2}/PKG-INFO +14 -12
  3. {delos_cli-2.0.0 → delos_cli-2.0.2}/README.md +10 -8
  4. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/agent/__init__.py +4 -0
  5. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/agent/session.py +6 -18
  6. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/agent/tools.py +53 -9
  7. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/agent/transport.py +6 -2
  8. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/agent/turn.py +1 -1
  9. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/base.py +4 -1
  10. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/chat/app.py +22 -108
  11. delos_cli-2.0.2/delos_cli/apps/chat/commands.py +187 -0
  12. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/chat/render.py +47 -27
  13. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/replay.py +4 -2
  14. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/scribe/app.py +7 -1
  15. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/scribe/tools.py +12 -7
  16. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/auth/config.py +1 -1
  17. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/auth/mfa.py +3 -12
  18. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/auth/oauth.py +35 -10
  19. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/auth/token_manager.py +1 -1
  20. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/commands/prompts.py +6 -2
  21. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ctx.py +5 -2
  22. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/git_context.py +1 -1
  23. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/main.py +57 -196
  24. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/project_context.py +1 -1
  25. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/serve/app.py +13 -6
  26. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/serve/confirm.py +1 -2
  27. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/serve/server.py +39 -25
  28. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/state.py +2 -41
  29. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/ask_user_input.py +5 -6
  30. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/edit_content.py +7 -9
  31. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/explore.py +76 -14
  32. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/glob_tool.py +5 -17
  33. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/grep.py +8 -18
  34. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/read.py +11 -13
  35. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/run_shell.py +5 -12
  36. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/task.py +6 -10
  37. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/todo_write.py +2 -8
  38. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/verify.py +9 -17
  39. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/write_content.py +11 -11
  40. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/transport/chats.py +1 -3
  41. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/transport/documents.py +10 -4
  42. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/transport/integrations.py +4 -6
  43. delos_cli-2.0.2/delos_cli/ui/collapse.py +158 -0
  44. delos_cli-2.0.2/delos_cli/ui/completer.py +290 -0
  45. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/lexer.py +2 -1
  46. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/output.py +34 -17
  47. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/repl.py +171 -47
  48. delos_cli-2.0.2/delos_cli/usage_report.py +173 -0
  49. {delos_cli-2.0.0 → delos_cli-2.0.2}/pyproject.toml +7 -34
  50. delos_cli-2.0.0/DELOS.md +0 -785
  51. delos_cli-2.0.0/delos_cli/apps/chat/commands.py +0 -340
  52. delos_cli-2.0.0/delos_cli/doctor.py +0 -182
  53. delos_cli-2.0.0/delos_cli/engine/__init__.py +0 -34
  54. delos_cli-2.0.0/delos_cli/engine/compaction.py +0 -514
  55. delos_cli-2.0.0/delos_cli/engine/context.py +0 -577
  56. delos_cli-2.0.0/delos_cli/engine/frontmatter.py +0 -33
  57. delos_cli-2.0.0/delos_cli/engine/llm.py +0 -98
  58. delos_cli-2.0.0/delos_cli/engine/loop.py +0 -837
  59. delos_cli-2.0.0/delos_cli/engine/memory.py +0 -171
  60. delos_cli-2.0.0/delos_cli/engine/prompt.py +0 -313
  61. delos_cli-2.0.0/delos_cli/engine/skills.py +0 -253
  62. delos_cli-2.0.0/delos_cli/engine/streaming.py +0 -210
  63. delos_cli-2.0.0/delos_cli/engine/subagent.py +0 -239
  64. delos_cli-2.0.0/delos_cli/engine/toolset.py +0 -614
  65. delos_cli-2.0.0/delos_cli/engine/transcript.py +0 -175
  66. delos_cli-2.0.0/delos_cli/hooks.py +0 -200
  67. delos_cli-2.0.0/delos_cli/settings.py +0 -148
  68. delos_cli-2.0.0/delos_cli/tools/readonly_shell.py +0 -99
  69. delos_cli-2.0.0/delos_cli/ui/collapse.py +0 -106
  70. delos_cli-2.0.0/delos_cli/ui/completer.py +0 -135
  71. delos_cli-2.0.0/tests/conftest.py +0 -176
  72. delos_cli-2.0.0/tests/test_bench_resilience.py +0 -144
  73. delos_cli-2.0.0/tests/test_chat_app_engine.py +0 -188
  74. delos_cli-2.0.0/tests/test_doctor.py +0 -106
  75. delos_cli-2.0.0/tests/test_engine_compaction.py +0 -614
  76. delos_cli-2.0.0/tests/test_engine_context.py +0 -446
  77. delos_cli-2.0.0/tests/test_engine_loop.py +0 -531
  78. delos_cli-2.0.0/tests/test_engine_memory.py +0 -223
  79. delos_cli-2.0.0/tests/test_engine_plan_mode.py +0 -209
  80. delos_cli-2.0.0/tests/test_engine_skills.py +0 -456
  81. delos_cli-2.0.0/tests/test_engine_subagent.py +0 -439
  82. delos_cli-2.0.0/tests/test_hooks.py +0 -276
  83. delos_cli-2.0.0/tests/test_local_sessions.py +0 -113
  84. delos_cli-2.0.0/tests/test_render_tool_blocks.py +0 -195
  85. delos_cli-2.0.0/tests/test_settings.py +0 -183
  86. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/__init__.py +0 -0
  87. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/__init__.py +0 -0
  88. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/chat/__init__.py +0 -0
  89. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/scribe/__init__.py +0 -0
  90. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/apps/scribe/commands.py +0 -0
  91. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/auth/__init__.py +0 -0
  92. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/commands/__init__.py +0 -0
  93. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/commands/base.py +0 -0
  94. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/commands/builtin.py +0 -0
  95. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/context_report.py +0 -0
  96. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/loop.py +0 -0
  97. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/serve/__init__.py +0 -0
  98. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/serve/protocol.py +0 -0
  99. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/serve/rpc.py +0 -0
  100. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/tools/__init__.py +0 -0
  101. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/transport/__init__.py +0 -0
  102. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/transport/client.py +0 -0
  103. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/transport/models.py +0 -0
  104. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/__init__.py +0 -0
  105. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/banner.py +0 -0
  106. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/chat_picker.py +0 -0
  107. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/document_picker.py +0 -0
  108. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/integrations_picker.py +0 -0
  109. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/model_picker.py +0 -0
  110. {delos_cli-2.0.0 → delos_cli-2.0.2}/delos_cli/ui/style.py +0 -0
@@ -1,6 +1,5 @@
1
1
  knip.txt
2
2
  pentest-*
3
- lefthook.yml
4
3
  backend/.vscode
5
4
  backend/cosmos_env
6
5
  # Allow backend/.env (has safe localhost defaults), but ignore all other .env files
@@ -22,16 +21,26 @@ infrastructure/supabase/**/.env
22
21
  backend/.python-version
23
22
  /scripts/
24
23
  */scripts/
24
+ !tooling/scripts/
25
25
  # Re-include the tools-catalog export script (CI drift test imports it)
26
26
  !backend/scripts/
27
27
  backend/scripts/*
28
28
  !backend/scripts/__init__.py
29
29
  !backend/scripts/export_tools_catalog.py
30
30
  !backend/scripts/check_supabase_models.py
31
+ !backend/scripts/dump_openapi.py
32
+ !backend/scripts/backfill_agent_issues.py
33
+ !backend/scripts/backfill_directive_recurrence.py
34
+ !backend/scripts/analyze_prompt_dumps.py
35
+ !backend/scripts/check_route_response_types.py
36
+ !backend/scripts/fly_source_recovery.py
37
+ !backend/scripts/audit_sprite_regions.py
38
+ !backend/scripts/migrate_vibe_sprites.py
31
39
 
32
40
  .claude/worktrees
33
41
 
34
42
  data/documents2
43
+ data/logs
35
44
  **/**0
36
45
  _local
37
46
  .env.old
@@ -68,10 +77,24 @@ next-env.d.ts
68
77
  # production
69
78
  build
70
79
 
80
+ # Vite/tsc build output. Global rather than per-app: the office add-ins each
81
+ # went untracked-but-not-ignored until someone built them locally.
82
+ dist/
83
+
71
84
  # misc
72
85
  .DS_Store
73
86
  *.pem
74
87
 
88
+ # pnpm is the only package manager here, including the standalone services.
89
+ # One recorded exception: cvn/whatsapp-bridge is an npm-managed runtime sidecar
90
+ # (baileys needs the git-only `libsignal`, which blockExoticSubdeps rejects and
91
+ # pnpm has no scoped exception for — pnpm/pnpm#11799); its lockfile is tracked
92
+ # so the git ref stays pinned.
93
+ package-lock.json
94
+ !cvn/whatsapp-bridge/package-lock.json
95
+ yarn.lock
96
+ npm-shrinkwrap.json
97
+
75
98
  # debug
76
99
  npm-debug.log*
77
100
  yarn-debug.log*
@@ -108,6 +131,10 @@ yarn-error.log*
108
131
  # process-compose logs
109
132
  logs/
110
133
 
134
+ # python tool caches
135
+ .ruff_cache/
136
+ .pytest_cache/
137
+
111
138
  /pentest/
112
139
 
113
140
  # terraform
@@ -166,6 +193,10 @@ __pycache__/
166
193
  .sprite
167
194
  /apps/web/supabase/snippets
168
195
 
169
- # delos-cli per-developer settings (engine/model/approvals). The shared
170
- # .delos/settings.json IS committed; this one is personal by design.
171
- .delos/settings.local.json
196
+ # Supabase CLI local link state (project ref, pooler URL, remote versions).
197
+ # Rewritten by every `supabase link`, and belongs to a machine, not to the repo.
198
+ **/supabase/.temp/
199
+ apps/workers-mobile/eas-asc-key.p8
200
+
201
+ # backfill_fts.py keyset cursor (local run state)
202
+ projects/0904_drivesearch/.fts-cursor.json
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: delos-cli
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Terminal REPL for the Delos agent — the Claude-Code-style CLI for Delos.
5
5
  Project-URL: Homepage, https://delos.so
6
6
  Project-URL: Repository, https://github.com/Delos-Intelligence/cosmos-saas
@@ -16,10 +16,10 @@ Classifier: License :: OSI Approved :: MIT License
16
16
  Classifier: Operating System :: MacOS
17
17
  Classifier: Operating System :: POSIX :: Linux
18
18
  Classifier: Programming Language :: Python :: 3
19
- Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.14
20
20
  Classifier: Topic :: Terminals
21
21
  Classifier: Topic :: Utilities
22
- Requires-Python: ==3.12.*
22
+ Requires-Python: ==3.14.*
23
23
  Requires-Dist: httpx>=0.27.0
24
24
  Requires-Dist: prompt-toolkit>=3.0.47
25
25
  Requires-Dist: questionary>=2.0.1
@@ -70,13 +70,13 @@ Conversations are scoped to the directory you launch from — running
70
70
 
71
71
  ### In the REPL
72
72
 
73
- | Action | Key |
74
- |---|---|
75
- | Send a message | `Enter` |
76
- | New line | `Alt+Enter` |
77
- | Stop the agent mid-answer | `Esc` (or `/stop`) |
78
- | Quit | `Ctrl+D` (or `/quit`) |
79
- | Scroll | mouse wheel · `PageUp` / `PageDown` · `End` to follow again |
73
+ | Action | Key |
74
+ | ------------------------- | ----------------------------------------------------------- |
75
+ | Send a message | `Enter` |
76
+ | New line | `Alt+Enter` |
77
+ | Stop the agent mid-answer | `Esc` (or `/stop`) |
78
+ | Quit | `Ctrl+D` (or `/quit`) |
79
+ | Scroll | mouse wheel · `PageUp` / `PageDown` · `End` to follow again |
80
80
 
81
81
  ### Slash commands
82
82
 
@@ -87,6 +87,8 @@ Conversations are scoped to the directory you launch from — running
87
87
  /delete delete the current chat and go back to the picker
88
88
  /resume back to the picker without deleting
89
89
  /stop interrupt the agent
90
+ /usage show plan usage limits and remaining credits
91
+ /context show what fills the agent's context window
90
92
  /quit exit
91
93
  ```
92
94
 
@@ -104,7 +106,7 @@ See `apps/vscode/DEVELOPING.md` in the monorepo for the protocol reference.
104
106
 
105
107
  ## Requirements
106
108
 
107
- - Python 3.12
109
+ - Python 3.14
108
110
  - A [Delos](https://delos.so) account
109
111
 
110
112
  ## Uninstall
@@ -39,13 +39,13 @@ Conversations are scoped to the directory you launch from — running
39
39
 
40
40
  ### In the REPL
41
41
 
42
- | Action | Key |
43
- |---|---|
44
- | Send a message | `Enter` |
45
- | New line | `Alt+Enter` |
46
- | Stop the agent mid-answer | `Esc` (or `/stop`) |
47
- | Quit | `Ctrl+D` (or `/quit`) |
48
- | Scroll | mouse wheel · `PageUp` / `PageDown` · `End` to follow again |
42
+ | Action | Key |
43
+ | ------------------------- | ----------------------------------------------------------- |
44
+ | Send a message | `Enter` |
45
+ | New line | `Alt+Enter` |
46
+ | Stop the agent mid-answer | `Esc` (or `/stop`) |
47
+ | Quit | `Ctrl+D` (or `/quit`) |
48
+ | Scroll | mouse wheel · `PageUp` / `PageDown` · `End` to follow again |
49
49
 
50
50
  ### Slash commands
51
51
 
@@ -56,6 +56,8 @@ Conversations are scoped to the directory you launch from — running
56
56
  /delete delete the current chat and go back to the picker
57
57
  /resume back to the picker without deleting
58
58
  /stop interrupt the agent
59
+ /usage show plan usage limits and remaining credits
60
+ /context show what fills the agent's context window
59
61
  /quit exit
60
62
  ```
61
63
 
@@ -73,7 +75,7 @@ See `apps/vscode/DEVELOPING.md` in the monorepo for the protocol reference.
73
75
 
74
76
  ## Requirements
75
77
 
76
- - Python 3.12
78
+ - Python 3.14
77
79
  - A [Delos](https://delos.so) account
78
80
 
79
81
  ## Uninstall
@@ -20,6 +20,8 @@ from .tools import (
20
20
  ToolRenderer,
21
21
  default_handler,
22
22
  default_renderer,
23
+ friendly_tool_name,
24
+ tool_line,
23
25
  )
24
26
  from .transport import AgentTransport
25
27
  from .turn import AgentTurn
@@ -33,4 +35,6 @@ __all__ = [
33
35
  "ToolRenderer",
34
36
  "default_handler",
35
37
  "default_renderer",
38
+ "friendly_tool_name",
39
+ "tool_line",
36
40
  ]
@@ -15,8 +15,6 @@ import asyncio
15
15
  from itertools import starmap
16
16
  from typing import TYPE_CHECKING, Any
17
17
 
18
- from delos_cli.state import tool_allowed_in
19
-
20
18
  if TYPE_CHECKING:
21
19
  from collections.abc import AsyncIterator
22
20
 
@@ -102,22 +100,9 @@ class AgentSession:
102
100
  # into a unique-constraint 500. Execution is the slow part;
103
101
  # the posts are cheap.
104
102
  async def _execute(
105
- tcid: str, meta: dict[str, Any],
103
+ tcid: str,
104
+ meta: dict[str, Any],
106
105
  ) -> tuple[str, dict[str, Any], str]:
107
- # Plan mode is enforced here, not only in ``ctx.confirm``. On
108
- # this engine the backend builds the tool list and pins
109
- # ``mode="build"`` (BACKEND_COORDINATION.md item 6), so the
110
- # mutating tools are still advertised and the model still calls
111
- # them — but nothing may actually run. Refusing at the dispatch
112
- # point is what makes plan mode hold for callers whose confirm
113
- # auto-accepts (``delos serve``, the benchmark harness).
114
- mode = getattr(ctx.state, "permission_mode", "default")
115
- if not tool_allowed_in(mode, meta["name"]):
116
- return tcid, meta, (
117
- f"Error: '{meta['name']}' is not available in plan mode — it would modify "
118
- "the user's files. Present your plan instead; nothing is changed until the "
119
- "user approves it."
120
- )
121
106
  handler = self._registry.handler_for(meta["name"])
122
107
  result = await handler(meta["input"], ctx)
123
108
  return tcid, meta, result
@@ -131,7 +116,10 @@ class AgentSession:
131
116
  raise outcome
132
117
  tcid, meta, result = outcome
133
118
  await self._transport.submit_tool_result(
134
- self._chat_id, tcid, meta["name"], result,
119
+ self._chat_id,
120
+ tcid,
121
+ meta["name"],
122
+ result,
135
123
  )
136
124
  if emit_tool_output:
137
125
  yield {
@@ -16,7 +16,9 @@ Defaults:
16
16
 
17
17
  - :func:`default_renderer` — a one-line ``→ tool_name`` indicator on
18
18
  ``tool-input-available``; nothing on output / delta events. Custom
19
- renderers can opt into args, output, or streaming deltas.
19
+ renderers keep the same ``→`` prefix and the category colours
20
+ (cyan = read-only, magenta = write/agent, green = shell, yellow = user
21
+ prompt); they can opt into args, output, or streaming deltas.
20
22
  - :func:`default_handler` — prints the input dict, asks for a free-form
21
23
  text answer via a one-shot prompt_toolkit prompt.
22
24
  """
@@ -63,14 +65,59 @@ class ToolHandler(Protocol):
63
65
  """
64
66
 
65
67
  async def __call__(
66
- self, tool_input: dict[str, Any], ctx: Ctx,
68
+ self,
69
+ tool_input: dict[str, Any],
70
+ ctx: Ctx,
67
71
  ) -> str:
68
72
  """Collect input from the user and return the result string."""
69
73
  ...
70
74
 
71
75
 
76
+ def friendly_tool_name(name: str) -> str:
77
+ """Humanize a raw tool name the way the VSCode webview does.
78
+
79
+ Same rule as the webview's ``friendlyToolName`` fallback
80
+ (``tool_name.replace(/_/g, ' ')``), capitalised: ``todo_write`` →
81
+ ``Todo write``. Tools with a dedicated renderer have their own
82
+ friendly labels (``Read file``, ``Run shell command``, …) matching the
83
+ webview's ``TOOL_FRIENDLY_NAMES``.
84
+ """
85
+ return name.replace("_", " ").capitalize()
86
+
87
+
88
+ def tool_line(
89
+ marker: str,
90
+ label: str,
91
+ value: str = "",
92
+ *,
93
+ color: str,
94
+ suffix: str = "",
95
+ ) -> Text:
96
+ """Build a one-line tool indicator with a clear visual hierarchy.
97
+
98
+ Layout: ``<marker> <label> <value> <suffix>`` where
99
+
100
+ - ``marker`` (``→`` running / ``✓`` done) is bold + ``color`` — the
101
+ category colour is carried only by this two-char glyph.
102
+ - ``label`` (``Read file``, ``Run shell command``…) is **dim** so it
103
+ reads as a quiet caption and no longer competes with the payload.
104
+ - ``value`` (path, command, pattern…) is bold + ``color`` — this is
105
+ the information the user actually scans for.
106
+ - ``suffix`` (line ranges, elapsed time, question counts…) stays dim.
107
+ """
108
+ line = Text()
109
+ line.append(f"{marker} ", style=f"bold {color}")
110
+ line.append(label, style="dim")
111
+ if value:
112
+ line.append(" ", style="dim")
113
+ line.append(value, style=f"bold {color}")
114
+ if suffix:
115
+ line.append(f" {suffix}", style="dim")
116
+ return line
117
+
118
+
72
119
  def default_renderer(event: dict[str, Any]) -> RenderableType | None:
73
- """Show a single ``→ tool_name`` line on ``tool-input-available``; ignore the rest.
120
+ """Show a single ``→ Tool name`` line on ``tool-input-available``; ignore the rest.
74
121
 
75
122
  Args are intentionally hidden — most tool calls are noisy enough as JSON
76
123
  that they hurt readability for the default case. Custom renderers can
@@ -79,11 +126,8 @@ def default_renderer(event: dict[str, Any]) -> RenderableType | None:
79
126
  """
80
127
  if event.get("type") != "tool-input-available":
81
128
  return None
82
- name = event.get("toolName", "?")
83
- line = Text()
84
- line.append("→ ", style="bold cyan")
85
- line.append(name, style="cyan")
86
- return line
129
+ name = friendly_tool_name(event.get("toolName", "?"))
130
+ return tool_line("→", name, color="cyan")
87
131
 
88
132
 
89
133
  async def default_handler(tool_input: dict[str, Any], ctx: Ctx) -> str:
@@ -95,7 +139,7 @@ async def default_handler(tool_input: dict[str, Any], ctx: Ctx) -> str:
95
139
  """
96
140
  try:
97
141
  pretty = json.dumps(tool_input, indent=2, ensure_ascii=False)
98
- except (TypeError, ValueError):
142
+ except TypeError, ValueError:
99
143
  pretty = str(tool_input)
100
144
  ctx.console.print(
101
145
  Panel(
@@ -39,7 +39,8 @@ class AgentTransport:
39
39
  return f"{base}/{chat_id}" if chat_id is not None else base
40
40
 
41
41
  async def stream_completions(
42
- self, body: dict[str, Any],
42
+ self,
43
+ body: dict[str, Any],
43
44
  ) -> AsyncIterator[dict[str, Any]]:
44
45
  """``POST /{app}/agent/completions/{org}`` → parsed SSE events."""
45
46
  async for event in self._http.sse_post(self._path("completions"), body):
@@ -90,7 +91,10 @@ class AgentTransport:
90
91
  return list(queue)
91
92
 
92
93
  async def edit_queued(
93
- self, chat_id: str, message_id: str, content: str,
94
+ self,
95
+ chat_id: str,
96
+ message_id: str,
97
+ content: str,
94
98
  ) -> None:
95
99
  """``PUT /queue/{org}/{chat_id}/{msg_id}``."""
96
100
  path = f"{self._path('queue', chat_id)}/{message_id}"
@@ -1,7 +1,7 @@
1
1
  """AgentTurn — a container for one agentic REPL turn that can be interrupted.
2
2
 
3
3
  Modeled on the voice pipeline's ``SpeechHandle``
4
- (``backend/cosmos/api/functions/voice/speech_handle.py``): a job/turn
4
+ (``backend/cosmos/api/functions/realtime/speech_handle.py``): a job/turn
5
5
  container that owns its running task and races it against an interruption
6
6
  future, rather than a standalone interrupt flag that the work has to poll.
7
7
 
@@ -29,7 +29,10 @@ class Renderer(Protocol):
29
29
  live updates on the buffer.
30
30
  """
31
31
 
32
- assistant_text: str
32
+ @property
33
+ def assistant_text(self) -> str:
34
+ """The assistant text accumulated since the last block commit."""
35
+ ...
33
36
 
34
37
  def apply(self, event: dict[str, Any]) -> None:
35
38
  """Handle one streamed event."""
@@ -1,17 +1,9 @@
1
1
  """ChatApp — the delos-cli agent.
2
2
 
3
- Two engines behind one :class:`~delos_cli.apps.base.App`:
4
-
5
- * ``backend`` (default today) wraps an :class:`AgentSession` against the
6
- ``/cli/agent/*`` routes + ``app_cli`` / ``app_cli_messages`` Supabase
7
- tables. The chat row is INSERT'd at picker time (cf.
8
- :func:`main._run_repl`), so the agent always has a row to append to.
9
- * ``local`` — drives :class:`~delos_cli.engine.LocalEngine` in this process,
10
- reaching the model through ``/openai-proxy/{org}/chat/completions`` and
11
- persisting to a JSONL transcript instead of Supabase.
12
-
13
- Both produce the same v6 event stream, so :meth:`send` is the only place the
14
- choice shows up and the REPL is engine-agnostic.
3
+ Wraps an :class:`AgentSession` against the ``/cli/agent/*`` backend
4
+ endpoints + ``app_cli`` / ``app_cli_messages`` Supabase tables. The
5
+ chat row is INSERT'd at picker time (cf. :func:`main._run_repl`), so by
6
+ the time we get here the agent always has a row to append messages to.
15
7
 
16
8
  Owns the per-app :class:`ToolRegistry` (so users can plug in custom
17
9
  renderers / handlers per tool). The :class:`V6Renderer` returned from
@@ -23,14 +15,13 @@ from __future__ import annotations
23
15
  import contextlib
24
16
  from dataclasses import dataclass, field
25
17
  from pathlib import Path
26
- from typing import TYPE_CHECKING, Any
18
+ from typing import TYPE_CHECKING, Any, override
27
19
 
28
20
  from rich.text import Text
29
21
 
30
22
  from delos_cli.agent import AgentSession, AgentTransport, ToolRegistry
31
23
  from delos_cli.apps.base import App
32
24
  from delos_cli.apps.replay import replay_messages
33
- from delos_cli.engine import LocalEngine, Transcript
34
25
  from delos_cli.git_context import collect_git_context
35
26
  from delos_cli.state import TodoStore
36
27
  from delos_cli.tools import (
@@ -128,40 +119,9 @@ class ChatApp(App):
128
119
  #: Set by ``main._run_repl`` after the picker resolves a chat_id, so
129
120
  #: history is rendered the moment the REPL takes over the screen.
130
121
  pending_replay: list[dict[str, Any]] = field(default_factory=list)
131
- #: ``"backend"`` (loop on the server, tools here) or ``"local"`` (loop
132
- #: here, LLM through the OpenAI-compatible proxy). Set by ``--engine``.
133
- engine: str = "backend"
134
122
  _current_session: AgentSession | None = field(default=None, init=False, repr=False)
135
- #: The in-process loop, created lazily per ``conv_id`` (``/clear`` and
136
- #: ``/resume`` both change it, and each conversation owns a transcript).
137
- _local: LocalEngine | None = field(default=None, init=False, repr=False)
138
- #: Set while a local turn is streaming, so ``/stop`` and mid-turn queueing
139
- #: have something to reach. Mirrors ``_current_session`` for the backend.
140
- _active_local: LocalEngine | None = field(default=None, init=False, repr=False)
141
-
142
- @property
143
- def is_local(self) -> bool:
144
- """True when turns run in this process."""
145
- return self.engine == "local"
146
-
147
- @property
148
- def local_engine(self) -> LocalEngine | None:
149
- """The bound in-process engine, or ``None`` before the first local turn.
150
-
151
- Read-only accessor for introspection commands (``/context``).
152
- """
153
- return self._local
154
-
155
- def _local_engine(self, ctx: Ctx) -> LocalEngine:
156
- """The :class:`LocalEngine` bound to the active conversation."""
157
- conv_id = ctx.state.conv_id
158
- model = ctx.state.model
159
- if self._local is None or self._local.conv_id != conv_id:
160
- self._local = LocalEngine(conv_id=conv_id, model=model)
161
- elif model and model != self._local.model:
162
- self._local.set_model(model)
163
- return self._local
164
123
 
124
+ @override
165
125
  async def on_enter(self, ctx: Ctx) -> None:
166
126
  """Prefetch model list, then replay any staged conversation history."""
167
127
  try:
@@ -170,34 +130,23 @@ class ChatApp(App):
170
130
  sink = ctx.output if ctx.output is not None else ctx.console
171
131
  sink.print(Text(f"(could not fetch models: {e})", style="yellow"))
172
132
 
173
- # Local sessions persist to a JSONL transcript, not to
174
- # ``app_cli_messages`` — so whatever the picker staged from Supabase is
175
- # irrelevant and the transcript is the history to show.
176
- if self.is_local:
177
- self.pending_replay = Transcript.open(ctx.state.conv_id).replay()
178
-
179
133
  if self.pending_replay and ctx.output is not None:
180
134
  replay_messages(self.pending_replay, self, ctx.output)
181
135
  # Always drain — even a failed replay shouldn't replay again next time.
182
136
  self.pending_replay = []
183
137
 
138
+ @override
184
139
  async def send(
185
140
  self,
186
141
  ctx: Ctx,
187
142
  messages: list[dict[str, str]],
188
143
  ) -> AsyncIterator[dict[str, Any]]:
189
- """Stream one turn's v6 events, from whichever engine is active.
144
+ """Stream one turn through :class:`AgentSession` against ``/cli/agent/...``.
190
145
 
191
- ``local`` drives the in-process loop; ``backend`` streams
192
- :class:`AgentSession` against ``/cli/agent/...`` (the ``app_cli`` row is
193
- created at picker time, or by ``/clear``, so the backend always finds
194
- the chat — no ``ensure_chat_row`` dance).
146
+ The ``app_cli`` row is created at picker time (or by ``/clear``),
147
+ so the backend always finds the chat when this runs — no
148
+ ``ensure_chat_row`` dance.
195
149
  """
196
- if self.is_local:
197
- async for event in self._send_local(ctx, messages):
198
- yield event
199
- return
200
-
201
150
  transport = AgentTransport(ctx.http, "cli", ctx.cfg.org_uuid)
202
151
  session = AgentSession(transport, ctx.state.conv_id, self.tools)
203
152
 
@@ -228,63 +177,29 @@ class ChatApp(App):
228
177
  finally:
229
178
  self._current_session = None
230
179
 
231
- async def _send_local(
232
- self,
233
- ctx: Ctx,
234
- messages: list[dict[str, str]],
235
- ) -> AsyncIterator[dict[str, Any]]:
236
- """Drive the in-process loop for one user turn.
237
-
238
- The REPL sends exactly one new user message per turn; anything typed
239
- while the turn runs goes through :meth:`queue_message` and is injected
240
- at the next batch boundary, so only the last user message matters here.
241
- """
242
- engine = self._local_engine(ctx)
243
- user_content = next(
244
- (m.get("content", "") for m in reversed(messages) if m.get("role") == "user"),
245
- "",
246
- )
247
- # Fresh git snapshot per turn — it rides in the turn's user message (not
248
- # the system prompt) so the cacheable prefix stays byte-stable.
249
- git_ctx = await collect_git_context(Path.cwd())
250
- self._active_local = engine
251
- try:
252
- async for event in engine.run_turn(ctx, user_content, git_context=git_ctx):
253
- yield event
254
- finally:
255
- self._active_local = None
256
-
180
+ @override
257
181
  def make_renderer(self, output: OutputBuffer) -> V6Renderer:
258
182
  """Build a fresh :class:`V6Renderer` wired to this app's tool registry."""
259
183
  return V6Renderer(output, self.tools)
260
184
 
185
+ @override
261
186
  async def cancel(self, ctx: Ctx) -> None:
262
- """Stop the in-flight turn, if any.
187
+ """Send the backend stop signal for the in-flight turn, if any.
263
188
 
264
- Local: flag the loop so it stops at its next checkpoint (the REPL also
265
- cancels the driving task, which is what makes it feel instant).
266
- Backend: send the Redis stop signal. No-op when no turn is active —
267
- :meth:`send` clears its reference as soon as the iterator finishes.
189
+ No-op when no turn is active :meth:`send` clears the session ref
190
+ as soon as the iterator finishes.
268
191
  """
269
192
  _ = ctx
270
- if self.is_local:
271
- if self._active_local is not None:
272
- self._active_local.request_stop()
273
- return
274
193
  if self._current_session is None:
275
194
  return
276
195
  # Best-effort — the loop has already shown an interrupt message.
277
196
  with contextlib.suppress(TransportError):
278
197
  await self._current_session.stop()
279
198
 
199
+ @override
280
200
  async def queue_message(self, ctx: Ctx, content: str) -> bool:
281
- """Queue a message to be injected into the turn already in flight."""
201
+ """Append a message to the backend's FIFO queue while a turn is running."""
282
202
  _ = ctx
283
- if self.is_local:
284
- if self._active_local is None:
285
- return False
286
- self._active_local.queue_message(content)
287
- return True
288
203
  if self._current_session is None:
289
204
  return False
290
205
  try:
@@ -293,10 +208,9 @@ class ChatApp(App):
293
208
  return False
294
209
  return True
295
210
 
211
+ @override
296
212
  def toolbar_fragment(self, ctx: Ctx) -> str:
297
- """Show the active model, and the engine when it isn't the default."""
213
+ """Show the active model so the user can see what ``/model`` would change."""
214
+ _ = self
298
215
  model = ctx.state.model
299
- parts = [f" model: {model}"] if model else []
300
- if self.is_local:
301
- parts.append("engine: local")
302
- return " ".join(parts)
216
+ return f" model: {model}" if model else ""