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.
- induscode/__init__.py +56 -0
- induscode/addons/__init__.py +176 -0
- induscode/addons/contract.py +923 -0
- induscode/addons/dispatch/__init__.py +43 -0
- induscode/addons/dispatch/event_dispatcher.py +348 -0
- induscode/addons/dispatch/tool_interceptor.py +349 -0
- induscode/addons/host.py +469 -0
- induscode/addons/loader.py +314 -0
- induscode/addons/manifest.py +232 -0
- induscode/addons/surface.py +199 -0
- induscode/boot/__init__.py +108 -0
- induscode/boot/auth_vault.py +323 -0
- induscode/boot/boot.py +210 -0
- induscode/boot/contract.py +223 -0
- induscode/boot/invocation.py +117 -0
- induscode/boot/runners/__init__.py +42 -0
- induscode/boot/runners/link_runner.py +82 -0
- induscode/boot/runners/oneshot_runner.py +85 -0
- induscode/boot/runners/registry.py +46 -0
- induscode/boot/runners/repl_runner.py +340 -0
- induscode/boot/runners/session.py +549 -0
- induscode/boot/stages.py +198 -0
- induscode/boot/upgrade/__init__.py +36 -0
- induscode/boot/upgrade/apply.py +125 -0
- induscode/boot/upgrade/upgrades.py +136 -0
- induscode/briefing/__init__.py +115 -0
- induscode/briefing/compose.py +414 -0
- induscode/briefing/contract.py +528 -0
- induscode/briefing/macros.py +721 -0
- induscode/briefing/skills.py +417 -0
- induscode/capability_deck/__init__.py +233 -0
- induscode/capability_deck/bridge_ledger/__init__.py +66 -0
- induscode/capability_deck/bridge_ledger/key.py +181 -0
- induscode/capability_deck/bridge_ledger/ledger.py +276 -0
- induscode/capability_deck/bridge_ledger/network.py +336 -0
- induscode/capability_deck/builtin_bridge.py +358 -0
- induscode/capability_deck/cards/__init__.py +116 -0
- induscode/capability_deck/cards/bg_process.py +482 -0
- induscode/capability_deck/cards/memory.py +226 -0
- induscode/capability_deck/cards/saas.py +280 -0
- induscode/capability_deck/cards/task.py +256 -0
- induscode/capability_deck/cards/todo.py +312 -0
- induscode/capability_deck/contract.py +450 -0
- induscode/capability_deck/manifest.py +126 -0
- induscode/capability_deck/provision.py +217 -0
- induscode/channels/__init__.py +146 -0
- induscode/channels/contract.py +585 -0
- induscode/channels/framer.py +132 -0
- induscode/channels/link/__init__.py +50 -0
- induscode/channels/link/dialog.py +246 -0
- induscode/channels/link/driver.py +308 -0
- induscode/channels/link/server.py +217 -0
- induscode/channels/oneshot.py +178 -0
- induscode/channels/ops.py +140 -0
- induscode/channels/session_ops.py +172 -0
- induscode/conductor/__init__.py +240 -0
- induscode/conductor/catalog.py +309 -0
- induscode/conductor/conductor.py +1084 -0
- induscode/conductor/contract.py +1035 -0
- induscode/conductor/matcher.py +291 -0
- induscode/conductor/serialize.py +575 -0
- induscode/conductor/signal_hub.py +382 -0
- induscode/conductor/skill_parse.py +294 -0
- induscode/conductor/transcript_store.py +449 -0
- induscode/console/__init__.py +236 -0
- induscode/console/app.py +1677 -0
- induscode/console/components/__init__.py +62 -0
- induscode/console/components/banner.py +499 -0
- induscode/console/components/banner_sweep.py +188 -0
- induscode/console/components/emblem.py +181 -0
- induscode/console/components/status_bar.py +102 -0
- induscode/console/contract.py +836 -0
- induscode/console/input/__init__.py +107 -0
- induscode/console/input/chord.py +197 -0
- induscode/console/input/dir_reader.py +113 -0
- induscode/console/input/intents.py +258 -0
- induscode/console/input/providers.py +469 -0
- induscode/console/mount.py +137 -0
- induscode/console/overlays/__init__.py +94 -0
- induscode/console/overlays/auth.py +503 -0
- induscode/console/overlays/pickers.py +526 -0
- induscode/console/overlays/router.py +129 -0
- induscode/console/overlays/sessions.py +232 -0
- induscode/console/reducer.py +145 -0
- induscode/console/resume_picker.py +156 -0
- induscode/console/slash_commands/__init__.py +78 -0
- induscode/console/slash_commands/builtins.py +254 -0
- induscode/console/slash_commands/dynamic.py +217 -0
- induscode/console/slash_commands/integrations.py +949 -0
- induscode/console/slash_commands/transcript.py +404 -0
- induscode/console/slash_commands/workbench.py +430 -0
- induscode/console/startup.py +434 -0
- induscode/console/theme/__init__.py +44 -0
- induscode/console/theme/adapter.py +168 -0
- induscode/console/theme/palette.py +128 -0
- induscode/console/theme/resolve.py +123 -0
- induscode/console/theme/tokens.py +185 -0
- induscode/console_slash/__init__.py +111 -0
- induscode/console_slash/contract.py +185 -0
- induscode/console_slash/registry.py +140 -0
- induscode/console_slash/resolve.py +194 -0
- induscode/console_slash/shared.py +172 -0
- induscode/entry.py +108 -0
- induscode/insight/__init__.py +153 -0
- induscode/insight/collector.py +73 -0
- induscode/insight/replay.py +305 -0
- induscode/insight/wrapper.py +1115 -0
- induscode/kit/__init__.py +82 -0
- induscode/kit/clipboard_image.py +215 -0
- induscode/kit/external_editor.py +120 -0
- induscode/kit/image.py +188 -0
- induscode/kit/shell.py +89 -0
- induscode/kit/tool_fetch.py +288 -0
- induscode/launch/__init__.py +224 -0
- induscode/launch/catalog.py +310 -0
- induscode/launch/contract.py +569 -0
- induscode/launch/credentials.py +852 -0
- induscode/launch/invocation/__init__.py +39 -0
- induscode/launch/invocation/attachments.py +281 -0
- induscode/launch/invocation/flags.py +210 -0
- induscode/launch/invocation/read.py +369 -0
- induscode/launch/invocation/usage.py +110 -0
- induscode/launch/oauth.py +808 -0
- induscode/launch/packages.py +299 -0
- induscode/launch/pickers.py +291 -0
- induscode/py.typed +0 -0
- induscode/runtime_bridge/__init__.py +166 -0
- induscode/runtime_bridge/bridges/__init__.py +66 -0
- induscode/runtime_bridge/bridges/_drive.py +268 -0
- induscode/runtime_bridge/bridges/builtins.py +177 -0
- induscode/runtime_bridge/bridges/claude_cli.py +198 -0
- induscode/runtime_bridge/bridges/codex_cli.py +203 -0
- induscode/runtime_bridge/bridges/indusagi_cli.py +217 -0
- induscode/runtime_bridge/broker.py +397 -0
- induscode/runtime_bridge/contract.py +734 -0
- induscode/runtime_bridge/sink.py +351 -0
- induscode/sessions/__init__.py +25 -0
- induscode/sessions/contract.py +119 -0
- induscode/sessions/library.py +350 -0
- induscode/settings/__init__.py +47 -0
- induscode/settings/contract.py +313 -0
- induscode/settings/manager.py +268 -0
- induscode/transcript_export/__init__.py +109 -0
- induscode/transcript_export/contract.py +522 -0
- induscode/transcript_export/publish.py +455 -0
- induscode/transcript_export/sgr.py +566 -0
- induscode/transcript_export/template.py +319 -0
- induscode/transcript_export/theme_bridge.py +325 -0
- induscode/window_budget/__init__.py +76 -0
- induscode/window_budget/budget/__init__.py +26 -0
- induscode/window_budget/budget/estimate.py +273 -0
- induscode/window_budget/budget/gate.py +60 -0
- induscode/window_budget/budget/slice.py +145 -0
- induscode/window_budget/condenser.py +170 -0
- induscode/window_budget/contract.py +329 -0
- induscode/window_budget/summarize/__init__.py +33 -0
- induscode/window_budget/summarize/condense.py +212 -0
- induscode/window_budget/summarize/prompt.py +241 -0
- induscode/workspace/__init__.py +30 -0
- induscode/workspace/brand.py +96 -0
- induscode/workspace/locator.py +269 -0
- induscode-0.1.0.dist-info/METADATA +97 -0
- induscode-0.1.0.dist-info/RECORD +167 -0
- induscode-0.1.0.dist-info/WHEEL +4 -0
- induscode-0.1.0.dist-info/entry_points.txt +3 -0
- induscode-0.1.0.dist-info/licenses/CREDITS.md +22 -0
- induscode-0.1.0.dist-info/licenses/NOTICE +7 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""Workbench slash commands — the pickers, display blocks, and help surfaces.
|
|
2
|
+
|
|
3
|
+
Port of TS ``src/console/slash/commands/workbench.ts``. This group covers
|
|
4
|
+
the overlays a user reaches for to inspect or retune the session: the model
|
|
5
|
+
pickers, the per-scope routing editor, the settings list, the command
|
|
6
|
+
palette (``/help``), the keyboard map (``/keys``), the project changelog
|
|
7
|
+
(``/whats-new``), and a diagnostics dump (``/debug``). The picker handlers
|
|
8
|
+
stay thin — they raise an overlay through ``SlashContext.open_modal`` or
|
|
9
|
+
flip a reducer flag — while the help, keys, whats-new, and debug handlers do
|
|
10
|
+
real work: they read the live slash catalog, the grounded keymap, a
|
|
11
|
+
``CHANGELOG.md`` on disk, and the conductor's session statistics, then
|
|
12
|
+
surface the result as a display block or a written file rather than a dead
|
|
13
|
+
placeholder.
|
|
14
|
+
|
|
15
|
+
``/help`` catalog sourcing — the cycle-killer (plan analysis 03 §6.5/§7):
|
|
16
|
+
the TS build reached the assembled ``SLASH_COMMANDS`` through a *call-time
|
|
17
|
+
dynamic import* of ``builtins.ts``, because ``builtins`` imports this very
|
|
18
|
+
module to assemble the catalog. The Python port replaces that with a
|
|
19
|
+
module-level **late-bound provider**: the catalog assembler installs the
|
|
20
|
+
fully-assembled registry via :func:`set_help_registry_provider` at console
|
|
21
|
+
mount (so ``/help`` always sees dynamic skill/template rows too — fixing the
|
|
22
|
+
TS quirk that dynamic rows were frozen at process start), and until one is
|
|
23
|
+
installed ``/help`` falls back to the static transcript + workbench groups.
|
|
24
|
+
The :class:`~induscode.console_slash.SlashContext` contract is untouched.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import tempfile
|
|
32
|
+
import time
|
|
33
|
+
from collections.abc import Callable, Sequence
|
|
34
|
+
from datetime import datetime, timezone
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Final, TypeAlias
|
|
37
|
+
|
|
38
|
+
from indusagi.react_ink import UiDisplayBlock
|
|
39
|
+
|
|
40
|
+
from induscode.conductor import message_to_dict
|
|
41
|
+
from induscode.console_slash import (
|
|
42
|
+
FAMILY,
|
|
43
|
+
HANDLED,
|
|
44
|
+
SlashCommand,
|
|
45
|
+
SlashContext,
|
|
46
|
+
SlashOutcome,
|
|
47
|
+
SubCommand,
|
|
48
|
+
family_runner,
|
|
49
|
+
info,
|
|
50
|
+
warn,
|
|
51
|
+
)
|
|
52
|
+
from induscode.console.slash_commands.transcript import transcript_commands
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"HelpRegistryProvider",
|
|
56
|
+
"set_help_registry_provider",
|
|
57
|
+
"workbench_commands",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Scoped-model routing family
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
async def _scoped_edit(ctx: SlashContext, rest: str) -> SlashOutcome:
|
|
67
|
+
ctx.open_modal("scopedModels")
|
|
68
|
+
return HANDLED
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def _scoped_show(ctx: SlashContext, rest: str) -> SlashOutcome:
|
|
72
|
+
ctx.open_modal("scopedModels", {"focus": "summary"})
|
|
73
|
+
return HANDLED
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def _scoped_reset(ctx: SlashContext, rest: str) -> SlashOutcome:
|
|
77
|
+
ctx.open_modal("scopedModels", {"intent": "reset"})
|
|
78
|
+
ctx.set_status(info("Per-scope model overrides cleared."))
|
|
79
|
+
return HANDLED
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
#: The verb table backing ``/models-for``. Each verb routes to the same
|
|
83
|
+
#: per-scope picker overlay with a different intent payload, so the picker
|
|
84
|
+
#: can open straight onto the relevant facet rather than its landing view.
|
|
85
|
+
_SCOPED_MODEL_VERBS: Final[tuple[SubCommand, ...]] = (
|
|
86
|
+
SubCommand(
|
|
87
|
+
verb="edit",
|
|
88
|
+
describe="open the per-scope routing editor",
|
|
89
|
+
run=_scoped_edit,
|
|
90
|
+
),
|
|
91
|
+
SubCommand(
|
|
92
|
+
verb="show",
|
|
93
|
+
describe="review the active per-scope assignments",
|
|
94
|
+
run=_scoped_show,
|
|
95
|
+
),
|
|
96
|
+
SubCommand(
|
|
97
|
+
verb="reset",
|
|
98
|
+
describe="clear every per-scope override",
|
|
99
|
+
run=_scoped_reset,
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
_run_scoped_models = family_runner(FAMILY.scoped_models, _SCOPED_MODEL_VERBS)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
# Display-block helper
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
_BASE36_DIGITS: Final = "0123456789abcdefghijklmnopqrstuvwxyz"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _base36(value: int) -> str:
|
|
114
|
+
"""Render a non-negative int in base 36 (TS ``Number.toString(36)``)."""
|
|
115
|
+
if value <= 0:
|
|
116
|
+
return "0"
|
|
117
|
+
digits: list[str] = []
|
|
118
|
+
while value:
|
|
119
|
+
value, rem = divmod(value, 36)
|
|
120
|
+
digits.append(_BASE36_DIGITS[rem])
|
|
121
|
+
return "".join(reversed(digits))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _now_ms() -> int:
|
|
125
|
+
"""Epoch milliseconds (TS ``Date.now()``)."""
|
|
126
|
+
return int(time.time() * 1000)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _block(slug: str, title: str, markdown: str) -> UiDisplayBlock:
|
|
130
|
+
"""Mint a display block with a fresh id and timestamp.
|
|
131
|
+
|
|
132
|
+
The framework's display-block surface carries a single markdown body
|
|
133
|
+
under the ``changelog`` kind, so every text panel the workbench drops —
|
|
134
|
+
the command palette, the hotkey map, and the changelog itself — rides
|
|
135
|
+
that one block kind with its own title. The id slug keeps successive
|
|
136
|
+
invocations distinct rows.
|
|
137
|
+
|
|
138
|
+
:param slug: short id prefix identifying which panel this is
|
|
139
|
+
:param title: the human-facing heading the block renders
|
|
140
|
+
:param markdown: the panel body, in markdown
|
|
141
|
+
"""
|
|
142
|
+
return UiDisplayBlock(
|
|
143
|
+
id=f"{slug}-{_base36(_now_ms())}",
|
|
144
|
+
title=title,
|
|
145
|
+
markdown=markdown,
|
|
146
|
+
timestamp=_now_ms(),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
# Command palette (/help)
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
#: A zero-argument callable yielding the live slash catalog ``/help``
|
|
155
|
+
#: renders — typically a closure over the console's assembled registry.
|
|
156
|
+
HelpRegistryProvider: TypeAlias = Callable[[], Sequence[SlashCommand]]
|
|
157
|
+
|
|
158
|
+
#: The late-bound catalog source. Installed once per console mount by the
|
|
159
|
+
#: catalog assembler; ``None`` selects the static-groups fallback.
|
|
160
|
+
_help_registry_provider: HelpRegistryProvider | None = None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def set_help_registry_provider(provider: HelpRegistryProvider | None) -> None:
|
|
164
|
+
"""Install the catalog source ``/help`` renders (``None`` restores the
|
|
165
|
+
static fallback — useful for tests). See the module docstring for why
|
|
166
|
+
this replaces the TS dynamic-import cycle-breaker."""
|
|
167
|
+
global _help_registry_provider
|
|
168
|
+
_help_registry_provider = provider
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _help_catalog() -> Sequence[SlashCommand]:
|
|
172
|
+
"""The catalog ``/help`` lists: the installed provider's registry when
|
|
173
|
+
one is bound, otherwise the static transcript + workbench groups."""
|
|
174
|
+
if _help_registry_provider is not None:
|
|
175
|
+
return _help_registry_provider()
|
|
176
|
+
return (*transcript_commands, *workbench_commands)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _help_block() -> UiDisplayBlock:
|
|
180
|
+
"""Render the full flat command list as a markdown block.
|
|
181
|
+
|
|
182
|
+
Reads the live catalog through :func:`_help_catalog` so every command —
|
|
183
|
+
built-in and any dynamic skill/template rows spliced in at assembly
|
|
184
|
+
time — appears with its one-line summary.
|
|
185
|
+
"""
|
|
186
|
+
catalog = _help_catalog()
|
|
187
|
+
lines = []
|
|
188
|
+
for command in catalog:
|
|
189
|
+
alts = f" _(/{', /'.join(command.aliases)})_" if command.aliases else ""
|
|
190
|
+
lines.append(f"- **/{command.name}**{alts} — {command.summary}")
|
|
191
|
+
body = "\n".join(
|
|
192
|
+
[f"{len(catalog)} commands available. Type `/` to complete.", "", *lines]
|
|
193
|
+
)
|
|
194
|
+
return _block("help", "Commands", body)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
# Keyboard map (/keys)
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
#: The real keyboard shortcuts, grounded in the console keymap and surface.
|
|
202
|
+
#: Each row is a ``(chord, effect)`` pair the hotkey block renders as a table.
|
|
203
|
+
HOTKEYS: Final[tuple[tuple[str, str], ...]] = (
|
|
204
|
+
("Enter", "Submit the current turn"),
|
|
205
|
+
("Shift+Enter", "Insert a soft newline in the composer"),
|
|
206
|
+
("Esc", "Dismiss an overlay / abort the in-flight turn"),
|
|
207
|
+
("Esc Esc", "Open the transcript-tree navigator"),
|
|
208
|
+
("Shift+Tab", "Step the reasoning-effort (thinking) level"),
|
|
209
|
+
("Tab", "Accept the highlighted completion"),
|
|
210
|
+
("Ctrl+C", "Interrupt the running turn"),
|
|
211
|
+
("Ctrl+T", "Toggle reasoning / thinking rows"),
|
|
212
|
+
("Ctrl+V", "Attach an image from the clipboard"),
|
|
213
|
+
("Ctrl+G", "Edit the composer in your external editor"),
|
|
214
|
+
("Ctrl+O", "Expand or collapse full tool output"),
|
|
215
|
+
("Ctrl+L", "Open the model picker"),
|
|
216
|
+
("Ctrl+R", "Open the session resume list"),
|
|
217
|
+
("Ctrl+N / Ctrl+P", "Cycle the active model"),
|
|
218
|
+
("Ctrl+A / Ctrl+E", "Jump to start / end of the line"),
|
|
219
|
+
("Ctrl+U", "Clear the composer line (twice: exit)"),
|
|
220
|
+
("Ctrl+J", "Insert a newline"),
|
|
221
|
+
("Ctrl+Z", "Suspend the process"),
|
|
222
|
+
("Alt+Up", "Pull the newest queued input back into the composer"),
|
|
223
|
+
("! <cmd>", "Run a shell command (kept in context)"),
|
|
224
|
+
("!! <cmd>", "Run a shell command (excluded from context)"),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _hotkeys_block() -> UiDisplayBlock:
|
|
229
|
+
"""Render the hotkey map as a markdown block."""
|
|
230
|
+
rows = [f"| `{chord}` | {effect} |" for chord, effect in HOTKEYS]
|
|
231
|
+
body = "\n".join(["| Shortcut | Action |", "| --- | --- |", *rows])
|
|
232
|
+
return _block("hotkeys", "Keyboard shortcuts", body)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
# Changelog (/whats-new)
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
#: Candidate locations a project changelog may live at, relative to the cwd.
|
|
240
|
+
_CHANGELOG_NAMES: Final[tuple[str, ...]] = (
|
|
241
|
+
"CHANGELOG.md",
|
|
242
|
+
"CHANGELOG",
|
|
243
|
+
"changelog.md",
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
#: The fallback body shown when no ``CHANGELOG.md`` is present on disk.
|
|
247
|
+
_CHANGELOG_FALLBACK: Final = "\n".join(
|
|
248
|
+
[
|
|
249
|
+
"No `CHANGELOG.md` was found at the workspace root.",
|
|
250
|
+
"",
|
|
251
|
+
"Recent highlights in this build:",
|
|
252
|
+
"- Model and per-scope routing pickers reachable from the composer.",
|
|
253
|
+
"- `/help` and `/keys` render live command and shortcut references.",
|
|
254
|
+
"- Colour scheme switches inline between **midnight** and **daylight**.",
|
|
255
|
+
]
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _read_changelog(cwd: str) -> str | None:
|
|
260
|
+
"""Read the project changelog from disk, returning its markdown when one
|
|
261
|
+
of the candidate files is present and readable, or ``None`` when none is.
|
|
262
|
+
|
|
263
|
+
:param cwd: the workspace directory to resolve the candidates against
|
|
264
|
+
"""
|
|
265
|
+
for name in _CHANGELOG_NAMES:
|
|
266
|
+
try:
|
|
267
|
+
text = Path(cwd, name).read_text(encoding="utf-8")
|
|
268
|
+
except (OSError, UnicodeDecodeError):
|
|
269
|
+
# Not present or unreadable here — try the next candidate.
|
|
270
|
+
continue
|
|
271
|
+
if text.strip():
|
|
272
|
+
return text
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
async def _run_changelog(ctx: SlashContext) -> SlashOutcome:
|
|
277
|
+
"""Drop the changelog block, reading a real ``CHANGELOG.md`` when present
|
|
278
|
+
and falling back to the build highlights otherwise."""
|
|
279
|
+
markdown = _read_changelog(os.getcwd())
|
|
280
|
+
ctx.append_block(
|
|
281
|
+
_block("changelog", "Changelog", markdown)
|
|
282
|
+
if markdown is not None
|
|
283
|
+
else _block("changelog", "What's new", _CHANGELOG_FALLBACK)
|
|
284
|
+
)
|
|
285
|
+
return HANDLED
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
# Diagnostics dump (/debug)
|
|
290
|
+
# ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _iso_now() -> str:
|
|
294
|
+
"""UTC now in the TS ``Date.toISOString()`` shape (millisecond ``Z``)."""
|
|
295
|
+
return (
|
|
296
|
+
datetime.now(timezone.utc)
|
|
297
|
+
.isoformat(timespec="milliseconds")
|
|
298
|
+
.replace("+00:00", "Z")
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
async def _run_debug(ctx: SlashContext) -> SlashOutcome:
|
|
303
|
+
"""Write a JSONL diagnostics log for the active session and report its
|
|
304
|
+
path.
|
|
305
|
+
|
|
306
|
+
Each line is one JSON record: a header carrying the ``SessionStats``
|
|
307
|
+
tally plus the active model, followed by one record per live transcript
|
|
308
|
+
message (``role``/``content`` read via duck-typing and projected through
|
|
309
|
+
the one message⇄dict codec — plan cross-cutting rule 2). Writing it to a
|
|
310
|
+
fresh temp directory makes the dump a real artifact a user can open,
|
|
311
|
+
attach to a bug report, or ``tail``, rather than a transient toast. The
|
|
312
|
+
diagnostics view toggle is flipped as well so on-screen verbosity tracks
|
|
313
|
+
it.
|
|
314
|
+
"""
|
|
315
|
+
ctx.dispatch({"type": "toggle:reasoning"})
|
|
316
|
+
|
|
317
|
+
stats = ctx.conductor.stats()
|
|
318
|
+
messages = ctx.conductor.messages()
|
|
319
|
+
snapshot = ctx.conductor.snapshot()
|
|
320
|
+
|
|
321
|
+
records: list[dict[str, object]] = [
|
|
322
|
+
{
|
|
323
|
+
"type": "session",
|
|
324
|
+
"at": _iso_now(),
|
|
325
|
+
"sessionId": stats.sessionId,
|
|
326
|
+
"modelId": snapshot.modelId,
|
|
327
|
+
"phase": snapshot.phase,
|
|
328
|
+
"stats": message_to_dict(stats),
|
|
329
|
+
}
|
|
330
|
+
]
|
|
331
|
+
for index, message in enumerate(messages):
|
|
332
|
+
role = getattr(message, "role", None)
|
|
333
|
+
record: dict[str, object] = {
|
|
334
|
+
"type": "message",
|
|
335
|
+
"index": index,
|
|
336
|
+
"role": "unknown" if role is None else message_to_dict(role),
|
|
337
|
+
}
|
|
338
|
+
content = getattr(message, "content", None)
|
|
339
|
+
if content is not None:
|
|
340
|
+
# parity: JSON.stringify dropped an undefined `content` key.
|
|
341
|
+
record["content"] = message_to_dict(content)
|
|
342
|
+
records.append(record)
|
|
343
|
+
jsonl = "\n".join(json.dumps(record) for record in records) + "\n"
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
directory = tempfile.mkdtemp(prefix="indus-debug-")
|
|
347
|
+
path = os.path.join(directory, f"session-{stats.sessionId or 'unknown'}.jsonl")
|
|
348
|
+
Path(path).write_text(jsonl, encoding="utf-8")
|
|
349
|
+
ctx.set_status(info(f"Diagnostics written to {path}"))
|
|
350
|
+
except Exception as cause:
|
|
351
|
+
ctx.set_status(warn(f"Could not write the diagnostics log ({cause})."))
|
|
352
|
+
return HANDLED
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# ---------------------------------------------------------------------------
|
|
356
|
+
# Thin picker handlers
|
|
357
|
+
# ---------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
async def _run_model(ctx: SlashContext) -> SlashOutcome:
|
|
361
|
+
ctx.open_modal("models")
|
|
362
|
+
return HANDLED
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
async def _run_settings(ctx: SlashContext) -> SlashOutcome:
|
|
366
|
+
ctx.open_modal("settings")
|
|
367
|
+
return HANDLED
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
async def _run_help(ctx: SlashContext) -> SlashOutcome:
|
|
371
|
+
ctx.append_block(_help_block())
|
|
372
|
+
return HANDLED
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
async def _run_keys(ctx: SlashContext) -> SlashOutcome:
|
|
376
|
+
ctx.append_block(_hotkeys_block())
|
|
377
|
+
return HANDLED
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ---------------------------------------------------------------------------
|
|
381
|
+
# The catalog
|
|
382
|
+
# ---------------------------------------------------------------------------
|
|
383
|
+
|
|
384
|
+
#: The workbench command rows, in listing order. Pickers and help surfaces
|
|
385
|
+
#: open an overlay; the changelog drops a display block; debug flips a
|
|
386
|
+
#: toggle and reports it.
|
|
387
|
+
workbench_commands: list[SlashCommand] = [
|
|
388
|
+
SlashCommand(
|
|
389
|
+
name="model",
|
|
390
|
+
summary="Switch the model bound to this session",
|
|
391
|
+
run=_run_model,
|
|
392
|
+
aliases=("models",),
|
|
393
|
+
),
|
|
394
|
+
SlashCommand(
|
|
395
|
+
name="models-for",
|
|
396
|
+
summary="Edit, show, or reset per-scope model routing",
|
|
397
|
+
run=_run_scoped_models,
|
|
398
|
+
aliases=("scoped-models",),
|
|
399
|
+
family=FAMILY.scoped_models,
|
|
400
|
+
takes_args=True,
|
|
401
|
+
),
|
|
402
|
+
SlashCommand(
|
|
403
|
+
name="settings",
|
|
404
|
+
summary="Open the settings overlay",
|
|
405
|
+
run=_run_settings,
|
|
406
|
+
),
|
|
407
|
+
SlashCommand(
|
|
408
|
+
name="help",
|
|
409
|
+
summary="List the available commands",
|
|
410
|
+
run=_run_help,
|
|
411
|
+
aliases=("?",),
|
|
412
|
+
),
|
|
413
|
+
SlashCommand(
|
|
414
|
+
name="keys",
|
|
415
|
+
summary="Show the keyboard shortcut map",
|
|
416
|
+
run=_run_keys,
|
|
417
|
+
aliases=("hotkeys",),
|
|
418
|
+
),
|
|
419
|
+
SlashCommand(
|
|
420
|
+
name="whats-new",
|
|
421
|
+
summary="Show what changed in this build",
|
|
422
|
+
run=_run_changelog,
|
|
423
|
+
aliases=("changelog",),
|
|
424
|
+
),
|
|
425
|
+
SlashCommand(
|
|
426
|
+
name="debug",
|
|
427
|
+
summary="Write a session diagnostics log and toggle verbose view",
|
|
428
|
+
run=_run_debug,
|
|
429
|
+
),
|
|
430
|
+
]
|