induscode 0.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.
Files changed (167) hide show
  1. induscode/__init__.py +56 -0
  2. induscode/addons/__init__.py +176 -0
  3. induscode/addons/contract.py +923 -0
  4. induscode/addons/dispatch/__init__.py +43 -0
  5. induscode/addons/dispatch/event_dispatcher.py +348 -0
  6. induscode/addons/dispatch/tool_interceptor.py +349 -0
  7. induscode/addons/host.py +469 -0
  8. induscode/addons/loader.py +314 -0
  9. induscode/addons/manifest.py +232 -0
  10. induscode/addons/surface.py +199 -0
  11. induscode/boot/__init__.py +108 -0
  12. induscode/boot/auth_vault.py +323 -0
  13. induscode/boot/boot.py +210 -0
  14. induscode/boot/contract.py +223 -0
  15. induscode/boot/invocation.py +117 -0
  16. induscode/boot/runners/__init__.py +42 -0
  17. induscode/boot/runners/link_runner.py +82 -0
  18. induscode/boot/runners/oneshot_runner.py +85 -0
  19. induscode/boot/runners/registry.py +46 -0
  20. induscode/boot/runners/repl_runner.py +340 -0
  21. induscode/boot/runners/session.py +549 -0
  22. induscode/boot/stages.py +198 -0
  23. induscode/boot/upgrade/__init__.py +36 -0
  24. induscode/boot/upgrade/apply.py +125 -0
  25. induscode/boot/upgrade/upgrades.py +136 -0
  26. induscode/briefing/__init__.py +115 -0
  27. induscode/briefing/compose.py +414 -0
  28. induscode/briefing/contract.py +528 -0
  29. induscode/briefing/macros.py +721 -0
  30. induscode/briefing/skills.py +417 -0
  31. induscode/capability_deck/__init__.py +233 -0
  32. induscode/capability_deck/bridge_ledger/__init__.py +66 -0
  33. induscode/capability_deck/bridge_ledger/key.py +181 -0
  34. induscode/capability_deck/bridge_ledger/ledger.py +276 -0
  35. induscode/capability_deck/bridge_ledger/network.py +336 -0
  36. induscode/capability_deck/builtin_bridge.py +358 -0
  37. induscode/capability_deck/cards/__init__.py +116 -0
  38. induscode/capability_deck/cards/bg_process.py +482 -0
  39. induscode/capability_deck/cards/memory.py +226 -0
  40. induscode/capability_deck/cards/saas.py +280 -0
  41. induscode/capability_deck/cards/task.py +256 -0
  42. induscode/capability_deck/cards/todo.py +312 -0
  43. induscode/capability_deck/contract.py +450 -0
  44. induscode/capability_deck/manifest.py +126 -0
  45. induscode/capability_deck/provision.py +217 -0
  46. induscode/channels/__init__.py +146 -0
  47. induscode/channels/contract.py +585 -0
  48. induscode/channels/framer.py +132 -0
  49. induscode/channels/link/__init__.py +50 -0
  50. induscode/channels/link/dialog.py +246 -0
  51. induscode/channels/link/driver.py +308 -0
  52. induscode/channels/link/server.py +217 -0
  53. induscode/channels/oneshot.py +178 -0
  54. induscode/channels/ops.py +140 -0
  55. induscode/channels/session_ops.py +172 -0
  56. induscode/conductor/__init__.py +240 -0
  57. induscode/conductor/catalog.py +309 -0
  58. induscode/conductor/conductor.py +1084 -0
  59. induscode/conductor/contract.py +1035 -0
  60. induscode/conductor/matcher.py +291 -0
  61. induscode/conductor/serialize.py +575 -0
  62. induscode/conductor/signal_hub.py +382 -0
  63. induscode/conductor/skill_parse.py +294 -0
  64. induscode/conductor/transcript_store.py +449 -0
  65. induscode/console/__init__.py +236 -0
  66. induscode/console/app.py +1677 -0
  67. induscode/console/components/__init__.py +62 -0
  68. induscode/console/components/banner.py +499 -0
  69. induscode/console/components/banner_sweep.py +188 -0
  70. induscode/console/components/emblem.py +181 -0
  71. induscode/console/components/status_bar.py +102 -0
  72. induscode/console/contract.py +836 -0
  73. induscode/console/input/__init__.py +107 -0
  74. induscode/console/input/chord.py +197 -0
  75. induscode/console/input/dir_reader.py +113 -0
  76. induscode/console/input/intents.py +258 -0
  77. induscode/console/input/providers.py +469 -0
  78. induscode/console/mount.py +137 -0
  79. induscode/console/overlays/__init__.py +94 -0
  80. induscode/console/overlays/auth.py +503 -0
  81. induscode/console/overlays/pickers.py +526 -0
  82. induscode/console/overlays/router.py +129 -0
  83. induscode/console/overlays/sessions.py +232 -0
  84. induscode/console/reducer.py +145 -0
  85. induscode/console/resume_picker.py +156 -0
  86. induscode/console/slash_commands/__init__.py +78 -0
  87. induscode/console/slash_commands/builtins.py +254 -0
  88. induscode/console/slash_commands/dynamic.py +217 -0
  89. induscode/console/slash_commands/integrations.py +949 -0
  90. induscode/console/slash_commands/transcript.py +404 -0
  91. induscode/console/slash_commands/workbench.py +430 -0
  92. induscode/console/startup.py +434 -0
  93. induscode/console/theme/__init__.py +44 -0
  94. induscode/console/theme/adapter.py +168 -0
  95. induscode/console/theme/palette.py +128 -0
  96. induscode/console/theme/resolve.py +123 -0
  97. induscode/console/theme/tokens.py +185 -0
  98. induscode/console_slash/__init__.py +111 -0
  99. induscode/console_slash/contract.py +185 -0
  100. induscode/console_slash/registry.py +140 -0
  101. induscode/console_slash/resolve.py +194 -0
  102. induscode/console_slash/shared.py +172 -0
  103. induscode/entry.py +108 -0
  104. induscode/insight/__init__.py +153 -0
  105. induscode/insight/collector.py +73 -0
  106. induscode/insight/replay.py +305 -0
  107. induscode/insight/wrapper.py +1115 -0
  108. induscode/kit/__init__.py +82 -0
  109. induscode/kit/clipboard_image.py +215 -0
  110. induscode/kit/external_editor.py +120 -0
  111. induscode/kit/image.py +188 -0
  112. induscode/kit/shell.py +89 -0
  113. induscode/kit/tool_fetch.py +288 -0
  114. induscode/launch/__init__.py +224 -0
  115. induscode/launch/catalog.py +310 -0
  116. induscode/launch/contract.py +569 -0
  117. induscode/launch/credentials.py +852 -0
  118. induscode/launch/invocation/__init__.py +39 -0
  119. induscode/launch/invocation/attachments.py +281 -0
  120. induscode/launch/invocation/flags.py +210 -0
  121. induscode/launch/invocation/read.py +369 -0
  122. induscode/launch/invocation/usage.py +110 -0
  123. induscode/launch/oauth.py +808 -0
  124. induscode/launch/packages.py +299 -0
  125. induscode/launch/pickers.py +291 -0
  126. induscode/py.typed +0 -0
  127. induscode/runtime_bridge/__init__.py +166 -0
  128. induscode/runtime_bridge/bridges/__init__.py +66 -0
  129. induscode/runtime_bridge/bridges/_drive.py +268 -0
  130. induscode/runtime_bridge/bridges/builtins.py +177 -0
  131. induscode/runtime_bridge/bridges/claude_cli.py +198 -0
  132. induscode/runtime_bridge/bridges/codex_cli.py +203 -0
  133. induscode/runtime_bridge/bridges/indusagi_cli.py +217 -0
  134. induscode/runtime_bridge/broker.py +397 -0
  135. induscode/runtime_bridge/contract.py +734 -0
  136. induscode/runtime_bridge/sink.py +351 -0
  137. induscode/sessions/__init__.py +25 -0
  138. induscode/sessions/contract.py +119 -0
  139. induscode/sessions/library.py +350 -0
  140. induscode/settings/__init__.py +47 -0
  141. induscode/settings/contract.py +313 -0
  142. induscode/settings/manager.py +268 -0
  143. induscode/transcript_export/__init__.py +109 -0
  144. induscode/transcript_export/contract.py +522 -0
  145. induscode/transcript_export/publish.py +455 -0
  146. induscode/transcript_export/sgr.py +566 -0
  147. induscode/transcript_export/template.py +319 -0
  148. induscode/transcript_export/theme_bridge.py +325 -0
  149. induscode/window_budget/__init__.py +76 -0
  150. induscode/window_budget/budget/__init__.py +26 -0
  151. induscode/window_budget/budget/estimate.py +273 -0
  152. induscode/window_budget/budget/gate.py +60 -0
  153. induscode/window_budget/budget/slice.py +145 -0
  154. induscode/window_budget/condenser.py +170 -0
  155. induscode/window_budget/contract.py +329 -0
  156. induscode/window_budget/summarize/__init__.py +33 -0
  157. induscode/window_budget/summarize/condense.py +212 -0
  158. induscode/window_budget/summarize/prompt.py +241 -0
  159. induscode/workspace/__init__.py +30 -0
  160. induscode/workspace/brand.py +96 -0
  161. induscode/workspace/locator.py +269 -0
  162. induscode-0.1.0.dist-info/METADATA +97 -0
  163. induscode-0.1.0.dist-info/RECORD +167 -0
  164. induscode-0.1.0.dist-info/WHEEL +4 -0
  165. induscode-0.1.0.dist-info/entry_points.txt +3 -0
  166. induscode-0.1.0.dist-info/licenses/CREDITS.md +22 -0
  167. induscode-0.1.0.dist-info/licenses/NOTICE +7 -0
@@ -0,0 +1,404 @@
1
+ """Transcript & session-control slash commands.
2
+
3
+ Port of TS ``src/console/slash/commands/transcript.ts``: the first topic
4
+ group of the console's slash catalog — the verbs that reset, rename, branch,
5
+ inspect, or leave the live session view. Every row is a thin
6
+ :class:`~induscode.console_slash.SlashCommand` whose ``run`` drives the
7
+ injected :class:`~induscode.console_slash.SlashContext` — a reducer event, a
8
+ conductor call, a modal, or an exit request — and settles to ``HANDLED``.
9
+ Nothing here reaches into the TUI, the filesystem, or a provider SDK
10
+ directly; backend work flows through the ``SessionConductor``.
11
+
12
+ The rename, stats, condense, and reload verbs are backed by real conductor
13
+ surface (``set_session_name`` / ``stats`` / ``condense``). Where the
14
+ conductor cannot accept a richer input (the condense path is parameterless
15
+ on this build), the closest real action is taken and the status line
16
+ explains precisely what was applied.
17
+
18
+ Async/fire-and-forget translation (the plan's locked cross-cutting rule 3):
19
+ every ``run`` here is ``async def`` and the dispatcher awaits it, so the TS
20
+ ``void (async () => {...})()`` bodies of ``/summarize-context`` and
21
+ ``/reload`` run *inline* — the busy→done status sequence becomes
22
+ deterministic. Only the conductor reset behind ``/clear`` / ``/new`` stays
23
+ fire-and-forget (it must not delay the success toast); it rides a tracked
24
+ :func:`asyncio.create_task` whose failure is swallowed into a status warn —
25
+ never a bare task, never a crash.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import asyncio
31
+ import json
32
+ import time
33
+ from typing import TYPE_CHECKING, Final
34
+
35
+ from indusagi.react_ink import StatusMessage, UiDisplayBlock
36
+
37
+ from induscode.console_slash import (
38
+ HANDLED,
39
+ SlashCommand,
40
+ SlashContext,
41
+ SlashOutcome,
42
+ info,
43
+ warn,
44
+ )
45
+
46
+ if TYPE_CHECKING:
47
+ from induscode.conductor import SessionStats
48
+
49
+ __all__ = ["transcript_commands"]
50
+
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Block-id minting (TS `Date.now().toString(36)`)
54
+ # ---------------------------------------------------------------------------
55
+
56
+ _BASE36_DIGITS: Final = "0123456789abcdefghijklmnopqrstuvwxyz"
57
+
58
+
59
+ def _base36(value: int) -> str:
60
+ """Render a non-negative int in base 36 (TS ``Number.toString(36)``)."""
61
+ if value <= 0:
62
+ return "0"
63
+ digits: list[str] = []
64
+ while value:
65
+ value, rem = divmod(value, 36)
66
+ digits.append(_BASE36_DIGITS[rem])
67
+ return "".join(reversed(digits))
68
+
69
+
70
+ def _now_ms() -> int:
71
+ """Epoch milliseconds (TS ``Date.now()``)."""
72
+ return int(time.time() * 1000)
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Fire-and-forget task anchoring
77
+ # ---------------------------------------------------------------------------
78
+
79
+ #: Strong references to in-flight background tasks so they cannot be
80
+ #: garbage-collected mid-flight (the asyncio create_task anchoring pattern).
81
+ #: Each settled coroutine swallows its own failure into a status warn, so a
82
+ #: task here never raises. The M5 console host may later own this set on its
83
+ #: per-console holder; the anchor-and-discard discipline stays the same.
84
+ _BACKGROUND_TASKS: Final[set[asyncio.Task[None]]] = set()
85
+
86
+
87
+ def _spawn(coro) -> None:
88
+ """Run ``coro`` as a tracked background task (never a bare task)."""
89
+ task = asyncio.create_task(coro)
90
+ _BACKGROUND_TASKS.add(task)
91
+ task.add_done_callback(_BACKGROUND_TASKS.discard)
92
+
93
+
94
+ # ---------------------------------------------------------------------------
95
+ # View wipe + session reset
96
+ # ---------------------------------------------------------------------------
97
+
98
+
99
+ def _wipe_view(ctx: SlashContext) -> None:
100
+ """Blank everything the console is rendering: the row list, the display
101
+ blocks, and the status line. This is the view half of a clear;
102
+ :func:`_start_new_session` pairs it with the conductor-side conversation
103
+ reset."""
104
+ ctx.dispatch({"type": "rows:set", "rows": []})
105
+ ctx.dispatch({"type": "blocks:clear"})
106
+ ctx.dispatch({"type": "status:clear"})
107
+
108
+
109
+ def _start_new_session(ctx: SlashContext, toast: str) -> SlashOutcome:
110
+ """Start a fresh session: wipe the rendered view, then drop the
111
+ conductor's conversation and open a new session id via ``new_session``.
112
+
113
+ The conductor reset is what actually empties the transcript — the
114
+ rendered conversation comes from ``conductor.messages()``, so clearing
115
+ only the view would leave every prior turn on screen (the old ``/clear``
116
+ bug). The reset is effectively synchronous internally; fire it without
117
+ blocking the success toast and only surface a status if it unexpectedly
118
+ rejects.
119
+ """
120
+ _wipe_view(ctx)
121
+ ctx.set_status(info(toast))
122
+
123
+ async def settle() -> None:
124
+ try:
125
+ await ctx.conductor.new_session()
126
+ except Exception:
127
+ ctx.set_status(warn("Could not start a new session."))
128
+
129
+ _spawn(settle())
130
+ return HANDLED
131
+
132
+
133
+ async def _run_clear(ctx: SlashContext) -> SlashOutcome:
134
+ """Clear the conversation and begin a brand-new session."""
135
+ return _start_new_session(ctx, "Started a new session.")
136
+
137
+
138
+ async def _run_new(ctx: SlashContext) -> SlashOutcome:
139
+ """Begin a fresh session for a new line of work (same as ``/clear``)."""
140
+ return _start_new_session(ctx, "Started a new session.")
141
+
142
+
143
+ # ---------------------------------------------------------------------------
144
+ # Condense (/summarize-context)
145
+ # ---------------------------------------------------------------------------
146
+
147
+
148
+ async def _run_compact(ctx: SlashContext) -> SlashOutcome:
149
+ """Condense the live transcript to reclaim context window.
150
+
151
+ Shows a busy status, then drives the conductor's condense path. With
152
+ trailing instructions present the command first records them as a
153
+ session note via the shell-note path so they re-enter the agent's
154
+ context to steer the next condense, then runs the parameterless
155
+ ``condense`` — the one condense entry point this build exposes. A
156
+ completion status reports what ran once the path settles.
157
+ """
158
+ instructions = ctx.args.strip()
159
+ busy_text = (
160
+ f"Condensing with guidance: {instructions}"
161
+ if instructions
162
+ else "Condensing conversation context..."
163
+ )
164
+ ctx.set_status(StatusMessage(kind="busy", text=busy_text))
165
+ try:
166
+ if instructions:
167
+ # JSON quoting doubles as shell quoting here, exactly as the TS
168
+ # `JSON.stringify` interpolation did (ensure_ascii=False keeps
169
+ # non-ASCII guidance verbatim, like JSON.stringify).
170
+ quoted = json.dumps(
171
+ f"Compaction guidance: {instructions}", ensure_ascii=False
172
+ )
173
+ await ctx.conductor.execute_bash(f"printf '%s' {quoted}")
174
+ await ctx.conductor.condense()
175
+ ctx.set_status(
176
+ info(
177
+ "Context condensed (guidance recorded for the agent)."
178
+ if instructions
179
+ else "Context condensed."
180
+ )
181
+ )
182
+ except Exception:
183
+ ctx.set_status(
184
+ warn("Condense could not complete; the transcript is unchanged.")
185
+ )
186
+ return HANDLED
187
+
188
+
189
+ # ---------------------------------------------------------------------------
190
+ # Pickers (/resume, /branch, /timeline)
191
+ # ---------------------------------------------------------------------------
192
+
193
+
194
+ async def _run_resume(ctx: SlashContext) -> SlashOutcome:
195
+ """Raise the session list to resume a persisted session."""
196
+ ctx.open_modal("sessions")
197
+ return HANDLED
198
+
199
+
200
+ async def _run_fork(ctx: SlashContext) -> SlashOutcome:
201
+ """Raise the prior-user-turn picker to branch the transcript."""
202
+ ctx.open_modal("userTurns")
203
+ return HANDLED
204
+
205
+
206
+ async def _run_tree(ctx: SlashContext) -> SlashOutcome:
207
+ """Raise the transcript-tree navigator."""
208
+ ctx.open_modal("tree")
209
+ return HANDLED
210
+
211
+
212
+ # ---------------------------------------------------------------------------
213
+ # Session stats (/session)
214
+ # ---------------------------------------------------------------------------
215
+
216
+
217
+ def _stats_markdown(stats: SessionStats, name: str | None) -> str:
218
+ """Render a session-stats tally as a Markdown table for the display
219
+ block."""
220
+ dollars = f"${stats.cost:.4f}"
221
+ t = stats.tokens
222
+ rows: tuple[tuple[str, str], ...] = (
223
+ ("Session", f"{name} ({stats.sessionId})" if name else stats.sessionId),
224
+ ("Messages", str(stats.totalMessages)),
225
+ ("User / assistant", f"{stats.userMessages} / {stats.assistantMessages}"),
226
+ ("Tool calls / results", f"{stats.toolCalls} / {stats.toolResults}"),
227
+ ("Tokens (in / out)", f"{t.input} / {t.output}"),
228
+ ("Tokens (cache r/w)", f"{t.cacheRead} / {t.cacheWrite}"),
229
+ ("Tokens (total)", str(t.total)),
230
+ ("Cost", dollars),
231
+ )
232
+ body = "\n".join(f"| {key} | {value} |" for key, value in rows)
233
+ return f"| Field | Value |\n| --- | --- |\n{body}"
234
+
235
+
236
+ async def _run_session(ctx: SlashContext) -> SlashOutcome:
237
+ """Append a session-statistics display block.
238
+
239
+ Reads a live ``SessionStats`` tally from the conductor and renders it as
240
+ a changelog-kind display block (the only block kind the surface
241
+ supports), plus a one-line info toast. Distinct from ``/resume``, which
242
+ opens the session picker.
243
+ """
244
+ stats = ctx.conductor.stats()
245
+ name = ctx.conductor.session_name()
246
+ block = UiDisplayBlock(
247
+ id=f"session-stats-{_base36(_now_ms())}",
248
+ title=f"Session: {name}" if name else "Session statistics",
249
+ markdown=_stats_markdown(stats, name),
250
+ timestamp=_now_ms(),
251
+ )
252
+ ctx.append_block(block)
253
+ ctx.set_status(
254
+ info(
255
+ f"{stats.totalMessages} messages, {stats.tokens.total} tokens, "
256
+ f"${stats.cost:.4f}."
257
+ )
258
+ )
259
+ return HANDLED
260
+
261
+
262
+ # ---------------------------------------------------------------------------
263
+ # Naming (/name)
264
+ # ---------------------------------------------------------------------------
265
+
266
+
267
+ async def _run_name(ctx: SlashContext) -> SlashOutcome:
268
+ """Name the live session, or report its current name.
269
+
270
+ With an argument the command stores the label through the conductor's
271
+ ``set_session_name`` and confirms it. Bare, it reads back the current
272
+ ``session_name`` as a status line rather than warning, so the verb
273
+ doubles as an inspector.
274
+ """
275
+ label = ctx.args.strip()
276
+ if len(label) == 0:
277
+ current = ctx.conductor.session_name()
278
+ ctx.set_status(
279
+ info("This session has no name. Set one with /name <label>.")
280
+ if current is None or len(current) == 0
281
+ else info(f"Current session name: {current}")
282
+ )
283
+ return HANDLED
284
+ ctx.conductor.set_session_name(label)
285
+ ctx.set_status(info(f'Session named "{label}".'))
286
+ return HANDLED
287
+
288
+
289
+ # ---------------------------------------------------------------------------
290
+ # Reload (/reload)
291
+ # ---------------------------------------------------------------------------
292
+
293
+
294
+ async def _run_reload(ctx: SlashContext) -> SlashOutcome:
295
+ """Re-read the live session's resources.
296
+
297
+ Drives the conductor through a no-op tree navigation onto its current
298
+ leaf, which rebuilds and replays the active branch onto the agent — the
299
+ closest real "reload session resources" action this surface exposes —
300
+ then reports it. When the transcript is empty (no leaf yet) there is
301
+ nothing to rebuild, so the command reports that instead of issuing a
302
+ navigation.
303
+ """
304
+ leaf = ctx.conductor.snapshot().head.leaf
305
+ if leaf is None:
306
+ ctx.set_status(info("Session is empty; nothing to reload."))
307
+ return HANDLED
308
+ ctx.set_status(StatusMessage(kind="busy", text="Reloading session resources..."))
309
+ try:
310
+ await ctx.conductor.navigate_tree(leaf)
311
+ ctx.set_status(info("Reloaded session resources."))
312
+ except Exception:
313
+ ctx.set_status(warn("Could not reload session resources."))
314
+ return HANDLED
315
+
316
+
317
+ # ---------------------------------------------------------------------------
318
+ # Leaving (/quit, /exit)
319
+ # ---------------------------------------------------------------------------
320
+
321
+
322
+ async def _run_quit(ctx: SlashContext) -> SlashOutcome:
323
+ """Ask the host process to leave the interactive console."""
324
+ ctx.request_exit()
325
+ return HANDLED
326
+
327
+
328
+ async def _run_exit(ctx: SlashContext) -> SlashOutcome:
329
+ """Alternate token for leaving the interactive console."""
330
+ ctx.request_exit()
331
+ return HANDLED
332
+
333
+
334
+ # ---------------------------------------------------------------------------
335
+ # The group, in listing order
336
+ # ---------------------------------------------------------------------------
337
+
338
+ #: The transcript / session-control group, in listing order. Appended into
339
+ #: the slash registry by the catalog integrator; this module owns only the
340
+ #: rows, never the wiring.
341
+ transcript_commands: list[SlashCommand] = [
342
+ SlashCommand(
343
+ name="clear",
344
+ summary="Clear the conversation and start a new session",
345
+ run=_run_clear,
346
+ aliases=("reset",),
347
+ ),
348
+ SlashCommand(
349
+ name="new",
350
+ summary="Start a fresh session",
351
+ run=_run_new,
352
+ ),
353
+ SlashCommand(
354
+ name="summarize-context",
355
+ summary="Condense the conversation context",
356
+ run=_run_compact,
357
+ aliases=("condense", "compact"),
358
+ takes_args=True,
359
+ ),
360
+ SlashCommand(
361
+ name="resume",
362
+ summary="Resume a persisted session",
363
+ run=_run_resume,
364
+ aliases=("sessions",),
365
+ ),
366
+ SlashCommand(
367
+ name="session",
368
+ summary="Show live session statistics (ids, counts, tokens, cost)",
369
+ run=_run_session,
370
+ ),
371
+ SlashCommand(
372
+ name="branch",
373
+ summary="Branch the transcript from a prior turn",
374
+ run=_run_fork,
375
+ aliases=("fork",),
376
+ ),
377
+ SlashCommand(
378
+ name="timeline",
379
+ summary="Navigate the transcript tree",
380
+ run=_run_tree,
381
+ aliases=("tree",),
382
+ ),
383
+ SlashCommand(
384
+ name="name",
385
+ summary="Name the session, or show the current name",
386
+ run=_run_name,
387
+ takes_args=True,
388
+ ),
389
+ SlashCommand(
390
+ name="reload",
391
+ summary="Reload session resources",
392
+ run=_run_reload,
393
+ ),
394
+ SlashCommand(
395
+ name="quit",
396
+ summary="Leave the interactive console",
397
+ run=_run_quit,
398
+ ),
399
+ SlashCommand(
400
+ name="exit",
401
+ summary="Leave the interactive console",
402
+ run=_run_exit,
403
+ ),
404
+ ]