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,526 @@
|
|
|
1
|
+
"""Picker overlays — the selection dialogs raised over the transcript.
|
|
2
|
+
|
|
3
|
+
Port of TS ``src/console/overlays/pickers.tsx``. This module owns the four
|
|
4
|
+
modal kinds that present a list to pick from: the single-model picker
|
|
5
|
+
(``models``), the per-scope model picker (``scopedModels``), the
|
|
6
|
+
colour-scheme picker (``theme``), and the settings list (``settings``).
|
|
7
|
+
|
|
8
|
+
Dialog-API inversion (port plan analysis 02, risk 1) — the TS group was an
|
|
9
|
+
always-mounted component routed by ``ModalState.kind``, feeding callback
|
|
10
|
+
props (``onSelect``/``onSave``/``onClose``) into dialogs that stayed mounted
|
|
11
|
+
until the host dropped the modal state. The Python framework dialogs are
|
|
12
|
+
``ModalScreen[Result]`` values dismissed *with* a result, so each kind here
|
|
13
|
+
is an awaited ``push_screen_wait`` flow instead:
|
|
14
|
+
|
|
15
|
+
- every flow is ``async def run_*(app, payload, services) -> events`` — it
|
|
16
|
+
pushes the framework dialog, awaits the dismissal result, applies the side
|
|
17
|
+
effects (conductor / preference-store writes) *after* the await, and
|
|
18
|
+
returns the reducer events the App folds once the overlay closes;
|
|
19
|
+
- the TS ``useEffect`` one-shots (the scoped-models ``{intent: "reset"}``
|
|
20
|
+
fire-on-mount path) become a plain early return before any screen is
|
|
21
|
+
pushed;
|
|
22
|
+
- the theme picker's preview-before-commit machinery (TS: ``onHighlight`` →
|
|
23
|
+
``scheme:set`` dispatch re-rendering the whole surface) collapses into the
|
|
24
|
+
framework ``ThemeDialog`` itself — highlight sets ``app.theme`` live,
|
|
25
|
+
Esc restores the captured original, Enter dismisses with the committed
|
|
26
|
+
scheme token; only the *persist + reducer bookkeeping* remains here;
|
|
27
|
+
- the auth-probe race the TS model picker tolerated (``null`` until the
|
|
28
|
+
vault probe settled → "show all") collapses: the probe is awaited before
|
|
29
|
+
the dialog ever opens;
|
|
30
|
+
- the TS settings rows dispatched ``toggle:*`` / ``scheme:set`` live while
|
|
31
|
+
the dialog was up; here each row writes the preference store immediately
|
|
32
|
+
and appends the matching event to an accumulator the flow returns, folded
|
|
33
|
+
by the App after the dialog closes (the modal covers the surface in the
|
|
34
|
+
meantime; the colour-scheme row still re-themes live via ``app.theme``).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
from typing import TYPE_CHECKING, Any, Final, cast
|
|
40
|
+
|
|
41
|
+
from indusagi.llmgateway.contract import CostSheet, ModelCard
|
|
42
|
+
from indusagi.react_ink import (
|
|
43
|
+
ModelDialog,
|
|
44
|
+
ScopedModelsDialog,
|
|
45
|
+
SettingsDialog,
|
|
46
|
+
SettingsDialogItem,
|
|
47
|
+
ThemeDialog,
|
|
48
|
+
ThemeDialogItem,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
from induscode.console.contract import (
|
|
52
|
+
ConsoleEvent,
|
|
53
|
+
OverlayServices,
|
|
54
|
+
SchemeSet,
|
|
55
|
+
StatusMessage,
|
|
56
|
+
StatusSet,
|
|
57
|
+
ToggleImages,
|
|
58
|
+
ToggleReasoning,
|
|
59
|
+
is_theme_scheme,
|
|
60
|
+
)
|
|
61
|
+
from induscode.settings import DELIVERY_MODES, is_delivery_mode
|
|
62
|
+
|
|
63
|
+
if TYPE_CHECKING:
|
|
64
|
+
from textual.app import App
|
|
65
|
+
|
|
66
|
+
from induscode.conductor import ModelCardRef
|
|
67
|
+
|
|
68
|
+
__all__ = [
|
|
69
|
+
"THEME_CHOICES",
|
|
70
|
+
"THEME_NAMES",
|
|
71
|
+
"TOGGLE_VALUES",
|
|
72
|
+
"authenticated_providers",
|
|
73
|
+
"build_settings_items",
|
|
74
|
+
"card_catalog_id",
|
|
75
|
+
"list_model_refs",
|
|
76
|
+
"read_scoped_payload",
|
|
77
|
+
"ref_to_card",
|
|
78
|
+
"run_model_picker",
|
|
79
|
+
"run_scoped_models",
|
|
80
|
+
"run_settings_picker",
|
|
81
|
+
"run_theme_picker",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Theme choices (TS THEME_CHOICES verbatim, already in the dialog item shape)
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
#: The colour schemes the theme picker offers, in listing order — the two
|
|
90
|
+
#: base schemes followed by their daltonized (color-blind-safe) counterparts.
|
|
91
|
+
THEME_CHOICES: Final[tuple[ThemeDialogItem, ...]] = (
|
|
92
|
+
ThemeDialogItem(id="midnight", label="Midnight", description="dark terminals"),
|
|
93
|
+
ThemeDialogItem(id="daylight", label="Daylight", description="light terminals"),
|
|
94
|
+
ThemeDialogItem(
|
|
95
|
+
id="midnight-cb",
|
|
96
|
+
label="Midnight (color-blind)",
|
|
97
|
+
description="deuteran-safe, dark",
|
|
98
|
+
),
|
|
99
|
+
ThemeDialogItem(
|
|
100
|
+
id="daylight-cb",
|
|
101
|
+
label="Daylight (color-blind)",
|
|
102
|
+
description="deuteran-safe, light",
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
#: The scheme tokens the theme picker offers, derived from THEME_CHOICES.
|
|
107
|
+
THEME_NAMES: Final[tuple[str, ...]] = tuple(choice.id for choice in THEME_CHOICES)
|
|
108
|
+
|
|
109
|
+
#: The on/off vocabulary the boolean settings rows toggle between.
|
|
110
|
+
TOGGLE_VALUES: Final[tuple[str, ...]] = ("on", "off")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Catalog → framework-card mapping (TS refToModel)
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def ref_to_card(ref: "ModelCardRef") -> ModelCard:
|
|
119
|
+
"""Lift a catalog :class:`ModelCardRef` into the framework
|
|
120
|
+
:class:`ModelCard` shape the model dialogs render.
|
|
121
|
+
|
|
122
|
+
The dialogs read identity/label/reasoning and hand the value back through
|
|
123
|
+
their dismissal result; the numeric/capability fields are seeded to inert
|
|
124
|
+
defaults since the picker never consults them (the TS ``refToModel``
|
|
125
|
+
seeded the same way). The card carries the provider-scoped ``modelId`` as
|
|
126
|
+
its ``id`` so the framework row label renders ``provider/modelId`` —
|
|
127
|
+
:func:`card_catalog_id` reassembles the canonical catalog key.
|
|
128
|
+
"""
|
|
129
|
+
return ModelCard(
|
|
130
|
+
id=ref.modelId,
|
|
131
|
+
provider=cast(Any, ref.provider),
|
|
132
|
+
api=cast(Any, ""),
|
|
133
|
+
display_name=ref.name,
|
|
134
|
+
context_window=0,
|
|
135
|
+
max_output_tokens=0,
|
|
136
|
+
modalities=("text",),
|
|
137
|
+
reasoning=ref.reasoning,
|
|
138
|
+
cost=CostSheet(input_per_mtok=0.0, output_per_mtok=0.0),
|
|
139
|
+
# An empty wire_id would be normalized to ``id``; seed it explicitly
|
|
140
|
+
# so the inert card stays inert.
|
|
141
|
+
wire_id=ref.modelId,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def card_catalog_id(card: ModelCard) -> str:
|
|
146
|
+
"""The canonical ``provider/modelId`` catalog key for a picker card —
|
|
147
|
+
exactly how the conductor catalog builds :attr:`ModelCardRef.id`."""
|
|
148
|
+
return f"{card.provider}/{card.id}"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def list_model_refs(services: OverlayServices) -> list["ModelCardRef"]:
|
|
152
|
+
"""Read the catalog as :class:`ModelCardRef` rows, tolerant of any fault
|
|
153
|
+
(the TS ``listModels`` try/catch)."""
|
|
154
|
+
try:
|
|
155
|
+
return list(services.conductor.available_models())
|
|
156
|
+
except Exception:
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def authenticated_providers(services: OverlayServices) -> set[str]:
|
|
161
|
+
"""The set of provider ids the user has authenticated (any saved account
|
|
162
|
+
in the vault), probed live so a mid-session ``/login`` is reflected.
|
|
163
|
+
|
|
164
|
+
Port delta (risk-1 redesign): the TS hook returned ``null`` until the
|
|
165
|
+
async probe settled and the picker showed the whole catalog meanwhile;
|
|
166
|
+
here the probe is simply awaited before the dialog opens, so the race
|
|
167
|
+
disappears. A provider that cannot be read is not counted; a directory
|
|
168
|
+
fault yields the set gathered so far.
|
|
169
|
+
"""
|
|
170
|
+
found: set[str] = set()
|
|
171
|
+
try:
|
|
172
|
+
providers = services.list_login_providers()
|
|
173
|
+
except Exception:
|
|
174
|
+
return found
|
|
175
|
+
for provider in providers:
|
|
176
|
+
try:
|
|
177
|
+
if len(await services.vault.list_accounts(provider.id)) > 0:
|
|
178
|
+
found.add(provider.id)
|
|
179
|
+
except Exception:
|
|
180
|
+
# A provider that cannot be read is simply not counted.
|
|
181
|
+
continue
|
|
182
|
+
return found
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
# models — the single-model picker
|
|
187
|
+
# ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def run_model_picker(
|
|
191
|
+
app: "App[Any]",
|
|
192
|
+
payload: object | None,
|
|
193
|
+
services: OverlayServices | None,
|
|
194
|
+
) -> tuple[ConsoleEvent, ...]:
|
|
195
|
+
"""The single-model picker flow.
|
|
196
|
+
|
|
197
|
+
Restricted to models from providers the user has signed into (so the
|
|
198
|
+
list shows only callable models); if none are authenticated, the whole
|
|
199
|
+
catalog is offered rather than an empty picker. Selecting a card binds it
|
|
200
|
+
on the conductor; Esc leaves the session untouched.
|
|
201
|
+
"""
|
|
202
|
+
if services is None:
|
|
203
|
+
return ()
|
|
204
|
+
refs = list_model_refs(services)
|
|
205
|
+
authed = await authenticated_providers(services)
|
|
206
|
+
pool = [ref for ref in refs if ref.provider in authed] if authed else refs
|
|
207
|
+
chosen = await app.push_screen_wait(ModelDialog([ref_to_card(ref) for ref in pool]))
|
|
208
|
+
if chosen is not None:
|
|
209
|
+
try:
|
|
210
|
+
services.conductor.select_model(card_catalog_id(chosen))
|
|
211
|
+
except Exception:
|
|
212
|
+
# Selection faults must not crash the overlay.
|
|
213
|
+
pass
|
|
214
|
+
return ()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
# scopedModels — the per-scope model picker
|
|
219
|
+
# ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def read_scoped_payload(payload: object | None) -> dict[str, str]:
|
|
223
|
+
"""Narrow the opaque modal payload to the per-scope picker's known fields.
|
|
224
|
+
|
|
225
|
+
The ``/scoped-models`` sub-verbs raise the picker with: ``show`` →
|
|
226
|
+
``{"focus": "summary"}``; ``reset`` → ``{"intent": "reset"}``; ``edit``
|
|
227
|
+
(and a bare chord) → nothing. Unknown shapes narrow to ``{}``.
|
|
228
|
+
"""
|
|
229
|
+
if not isinstance(payload, dict):
|
|
230
|
+
return {}
|
|
231
|
+
out: dict[str, str] = {}
|
|
232
|
+
if payload.get("focus") == "summary":
|
|
233
|
+
out["focus"] = "summary"
|
|
234
|
+
if payload.get("intent") == "reset":
|
|
235
|
+
out["intent"] = "reset"
|
|
236
|
+
return out
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
async def run_scoped_models(
|
|
240
|
+
app: "App[Any]",
|
|
241
|
+
payload: object | None,
|
|
242
|
+
services: OverlayServices | None,
|
|
243
|
+
) -> tuple[ConsoleEvent, ...]:
|
|
244
|
+
"""The per-scope model picker flow.
|
|
245
|
+
|
|
246
|
+
The current enabled-models preference seeds the selection (empty means
|
|
247
|
+
every model); saving persists the chosen ids. The ``{"intent": "reset"}``
|
|
248
|
+
payload clears the override, persists, reports, and returns *without*
|
|
249
|
+
presenting the list (the TS fire-once ``useEffect`` becomes this early
|
|
250
|
+
return); ``{"focus": "summary"}`` opens the picker normally (parity: the
|
|
251
|
+
dialog exposes no read-only summary affordance to target).
|
|
252
|
+
"""
|
|
253
|
+
if services is None:
|
|
254
|
+
return ()
|
|
255
|
+
request = read_scoped_payload(payload)
|
|
256
|
+
|
|
257
|
+
if request.get("intent") == "reset":
|
|
258
|
+
try:
|
|
259
|
+
services.settings.set("enabledModels", [])
|
|
260
|
+
services.settings.save()
|
|
261
|
+
status = StatusMessage(kind="info", text="Per-scope model overrides cleared.")
|
|
262
|
+
except Exception:
|
|
263
|
+
status = StatusMessage(
|
|
264
|
+
kind="error", text="Could not clear per-scope model overrides."
|
|
265
|
+
)
|
|
266
|
+
return (StatusSet(status=status),)
|
|
267
|
+
|
|
268
|
+
refs = list_model_refs(services)
|
|
269
|
+
try:
|
|
270
|
+
enabled = list(services.settings.get("enabledModels"))
|
|
271
|
+
except Exception:
|
|
272
|
+
enabled = []
|
|
273
|
+
selected = enabled if len(enabled) > 0 else [ref.id for ref in refs]
|
|
274
|
+
|
|
275
|
+
ids = await app.push_screen_wait(
|
|
276
|
+
ScopedModelsDialog([ref_to_card(ref) for ref in refs], selected_ids=selected)
|
|
277
|
+
)
|
|
278
|
+
if ids is not None:
|
|
279
|
+
try:
|
|
280
|
+
# Everything selected means "no restriction" — store empty.
|
|
281
|
+
next_ids = [] if len(ids) == len(refs) else list(ids)
|
|
282
|
+
services.settings.set("enabledModels", next_ids)
|
|
283
|
+
services.settings.save()
|
|
284
|
+
except Exception:
|
|
285
|
+
# Persistence faults must not crash the overlay.
|
|
286
|
+
pass
|
|
287
|
+
return ()
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# ---------------------------------------------------------------------------
|
|
291
|
+
# theme — the colour-scheme picker with preview-before-commit
|
|
292
|
+
# ---------------------------------------------------------------------------
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
async def run_theme_picker(
|
|
296
|
+
app: "App[Any]",
|
|
297
|
+
payload: object | None,
|
|
298
|
+
services: OverlayServices | None,
|
|
299
|
+
) -> tuple[ConsoleEvent, ...]:
|
|
300
|
+
"""The colour-scheme picker flow, preview-before-commit (TS item #13).
|
|
301
|
+
|
|
302
|
+
The TS select-and-persist split into open/highlight/Enter/Esc moments
|
|
303
|
+
maps onto the framework :class:`ThemeDialog` natively (risk-1 redesign):
|
|
304
|
+
|
|
305
|
+
- **open** — the dialog captures ``app.theme`` as the revert target
|
|
306
|
+
on its first highlight (it, not the settings file, is what was on
|
|
307
|
+
screen — better fidelity than the TS ``currentScheme`` settings read);
|
|
308
|
+
- **highlight** — the dialog sets ``app.theme`` immediately (Textual
|
|
309
|
+
themes re-skin live; only registered schemes are applied);
|
|
310
|
+
- **Enter** — the dialog dismisses with the scheme token; *this flow*
|
|
311
|
+
persists it and returns the ``scheme:set`` event for the reducer;
|
|
312
|
+
- **Esc** — the dialog restores the original ``app.theme`` and
|
|
313
|
+
dismisses ``None``; nothing is written, no event is returned.
|
|
314
|
+
"""
|
|
315
|
+
if services is None:
|
|
316
|
+
return ()
|
|
317
|
+
token = await app.push_screen_wait(ThemeDialog(list(THEME_CHOICES)))
|
|
318
|
+
if isinstance(token, str) and is_theme_scheme(token):
|
|
319
|
+
try:
|
|
320
|
+
services.settings.set("colourScheme", token)
|
|
321
|
+
services.settings.save()
|
|
322
|
+
except Exception:
|
|
323
|
+
# Persistence faults must not crash the overlay.
|
|
324
|
+
pass
|
|
325
|
+
return (SchemeSet(scheme=token),)
|
|
326
|
+
return ()
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# ---------------------------------------------------------------------------
|
|
330
|
+
# settings — the settings list
|
|
331
|
+
# ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def build_settings_items(
|
|
335
|
+
services: OverlayServices,
|
|
336
|
+
events: list[ConsoleEvent],
|
|
337
|
+
app: "App[Any] | None" = None,
|
|
338
|
+
) -> list[SettingsDialogItem]:
|
|
339
|
+
"""The settings rows, mapped onto the framework :class:`SettingsDialogItem`.
|
|
340
|
+
|
|
341
|
+
Each row reads its current value from the preference store and its
|
|
342
|
+
``on_change`` writes the chosen value back, persisting immediately.
|
|
343
|
+
Reducer-backed rows also append their event to ``events`` (the TS live
|
|
344
|
+
``dispatch`` becomes this accumulator, folded by the App after the dialog
|
|
345
|
+
closes); the colour-scheme row additionally re-themes ``app`` live when a
|
|
346
|
+
registered scheme is chosen. Row ids, labels, descriptions, ordering and
|
|
347
|
+
the on/off vocabulary are the TS rows verbatim.
|
|
348
|
+
"""
|
|
349
|
+
settings = services.settings
|
|
350
|
+
|
|
351
|
+
def on_off(flag: object) -> str:
|
|
352
|
+
return "on" if flag else "off"
|
|
353
|
+
|
|
354
|
+
def toggle_row(
|
|
355
|
+
id: str,
|
|
356
|
+
key: str,
|
|
357
|
+
label: str,
|
|
358
|
+
description: str,
|
|
359
|
+
live: ConsoleEvent | None = None,
|
|
360
|
+
) -> SettingsDialogItem:
|
|
361
|
+
def on_change(value: str) -> None:
|
|
362
|
+
settings.set(key, value == "on")
|
|
363
|
+
settings.save()
|
|
364
|
+
if live is not None:
|
|
365
|
+
events.append(live)
|
|
366
|
+
|
|
367
|
+
return SettingsDialogItem(
|
|
368
|
+
id=id,
|
|
369
|
+
label=label,
|
|
370
|
+
description=description,
|
|
371
|
+
value=on_off(settings.get(key)),
|
|
372
|
+
values=list(TOGGLE_VALUES),
|
|
373
|
+
on_change=on_change,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def delivery_row(id: str, key: str, label: str, description: str) -> SettingsDialogItem:
|
|
377
|
+
def on_change(value: str) -> None:
|
|
378
|
+
if is_delivery_mode(value):
|
|
379
|
+
settings.set(key, value)
|
|
380
|
+
settings.save()
|
|
381
|
+
|
|
382
|
+
return SettingsDialogItem(
|
|
383
|
+
id=id,
|
|
384
|
+
label=label,
|
|
385
|
+
description=description,
|
|
386
|
+
value=str(settings.get(key)),
|
|
387
|
+
values=list(DELIVERY_MODES),
|
|
388
|
+
on_change=on_change,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def on_scheme_change(value: str) -> None:
|
|
392
|
+
settings.set("colourScheme", value)
|
|
393
|
+
settings.save()
|
|
394
|
+
if is_theme_scheme(value):
|
|
395
|
+
if app is not None and app.get_theme(value) is not None:
|
|
396
|
+
app.theme = value
|
|
397
|
+
events.append(SchemeSet(scheme=value))
|
|
398
|
+
|
|
399
|
+
def on_padding_change(value: str) -> None:
|
|
400
|
+
try:
|
|
401
|
+
parsed = int(value, 10)
|
|
402
|
+
except ValueError:
|
|
403
|
+
return
|
|
404
|
+
settings.set("editorPaddingX", parsed)
|
|
405
|
+
settings.save()
|
|
406
|
+
|
|
407
|
+
return [
|
|
408
|
+
SettingsDialogItem(
|
|
409
|
+
id="colour-scheme",
|
|
410
|
+
label="Colour scheme",
|
|
411
|
+
description="The terminal colour scheme the console renders in.",
|
|
412
|
+
value=str(settings.get("colourScheme")),
|
|
413
|
+
values=list(THEME_NAMES),
|
|
414
|
+
on_change=on_scheme_change,
|
|
415
|
+
),
|
|
416
|
+
toggle_row(
|
|
417
|
+
"show-images",
|
|
418
|
+
"showImages",
|
|
419
|
+
"Show images",
|
|
420
|
+
"Render inline image content in the transcript.",
|
|
421
|
+
live=ToggleImages(),
|
|
422
|
+
),
|
|
423
|
+
toggle_row(
|
|
424
|
+
"image-auto-resize",
|
|
425
|
+
"imageAutoResize",
|
|
426
|
+
"Auto-resize images",
|
|
427
|
+
"Shrink oversized images to fit before handing them to a provider.",
|
|
428
|
+
),
|
|
429
|
+
toggle_row(
|
|
430
|
+
"block-images",
|
|
431
|
+
"blockImages",
|
|
432
|
+
"Block images",
|
|
433
|
+
"Withhold image content from providers entirely.",
|
|
434
|
+
),
|
|
435
|
+
toggle_row(
|
|
436
|
+
"show-reasoning",
|
|
437
|
+
"showReasoning",
|
|
438
|
+
"Show reasoning",
|
|
439
|
+
"Stream the model's reasoning / thinking text as it arrives.",
|
|
440
|
+
live=ToggleReasoning(),
|
|
441
|
+
),
|
|
442
|
+
toggle_row(
|
|
443
|
+
"skill-commands",
|
|
444
|
+
"enableSkillCommands",
|
|
445
|
+
"Skill commands",
|
|
446
|
+
"Surface discovered skills as their own slash commands.",
|
|
447
|
+
),
|
|
448
|
+
delivery_row(
|
|
449
|
+
"steering-mode",
|
|
450
|
+
"steeringMode",
|
|
451
|
+
"Steering mode",
|
|
452
|
+
"Whether queued steering corrections release one at a time or all at once.",
|
|
453
|
+
),
|
|
454
|
+
delivery_row(
|
|
455
|
+
"follow-up-mode",
|
|
456
|
+
"followUpMode",
|
|
457
|
+
"Follow-up mode",
|
|
458
|
+
"Whether queued follow-up prompts release one at a time or all at once.",
|
|
459
|
+
),
|
|
460
|
+
toggle_row(
|
|
461
|
+
"auto-compact",
|
|
462
|
+
"autoCompact",
|
|
463
|
+
"Auto-compact",
|
|
464
|
+
"Condense the transcript automatically as it nears the window.",
|
|
465
|
+
),
|
|
466
|
+
toggle_row(
|
|
467
|
+
"collapse-changelog",
|
|
468
|
+
"collapseChangelog",
|
|
469
|
+
"Collapse changelog",
|
|
470
|
+
"Prefer a condensed changelog after the console updates itself.",
|
|
471
|
+
),
|
|
472
|
+
toggle_row(
|
|
473
|
+
"quiet-startup",
|
|
474
|
+
"quietStartup",
|
|
475
|
+
"Quiet startup",
|
|
476
|
+
"Suppress the banner and tips shown on a normal interactive launch.",
|
|
477
|
+
),
|
|
478
|
+
toggle_row(
|
|
479
|
+
"logo-sweep",
|
|
480
|
+
"logoSweep",
|
|
481
|
+
"Logo sweep",
|
|
482
|
+
"Tint the startup wordmark and emblem along a static colour gradient.",
|
|
483
|
+
),
|
|
484
|
+
toggle_row(
|
|
485
|
+
"reduced-motion",
|
|
486
|
+
"reducedMotion",
|
|
487
|
+
"Reduced motion",
|
|
488
|
+
"Suppress motion-flavoured flourishes such as the logo colour sweep.",
|
|
489
|
+
),
|
|
490
|
+
toggle_row(
|
|
491
|
+
"hardware-cursor",
|
|
492
|
+
"showHardwareCursor",
|
|
493
|
+
"Hardware cursor",
|
|
494
|
+
"Reveal the terminal's own cursor instead of the software-drawn caret.",
|
|
495
|
+
),
|
|
496
|
+
SettingsDialogItem(
|
|
497
|
+
id="editor-padding",
|
|
498
|
+
label="Editor padding",
|
|
499
|
+
description="Horizontal padding, in columns, around the prompt editor.",
|
|
500
|
+
value=str(settings.get("editorPaddingX")),
|
|
501
|
+
values=["0", "1", "2", "3"],
|
|
502
|
+
on_change=on_padding_change,
|
|
503
|
+
),
|
|
504
|
+
]
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
async def run_settings_picker(
|
|
508
|
+
app: "App[Any]",
|
|
509
|
+
payload: object | None,
|
|
510
|
+
services: OverlayServices | None,
|
|
511
|
+
) -> tuple[ConsoleEvent, ...]:
|
|
512
|
+
"""The settings-list flow: build the rows, present the framework dialog
|
|
513
|
+
(Enter/Left/Right cycle a row, Esc closes), and return the reducer events
|
|
514
|
+
the row edits accumulated while the dialog was up.
|
|
515
|
+
|
|
516
|
+
A row-building fault yields no dialog at all (the TS ``catch → null``).
|
|
517
|
+
"""
|
|
518
|
+
if services is None:
|
|
519
|
+
return ()
|
|
520
|
+
events: list[ConsoleEvent] = []
|
|
521
|
+
try:
|
|
522
|
+
items = build_settings_items(services, events, app)
|
|
523
|
+
except Exception:
|
|
524
|
+
return ()
|
|
525
|
+
await app.push_screen_wait(SettingsDialog(items))
|
|
526
|
+
return tuple(events)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Overlay router — the single dispatch point for every modal overlay.
|
|
2
|
+
|
|
3
|
+
Port of TS ``src/console/overlays/host.tsx`` under the dialog-API inversion
|
|
4
|
+
(port plan analysis 02, risk 1). The TS ``OverlayHost`` mounted three group
|
|
5
|
+
components unconditionally and routed by ``ModalState.kind``, with each
|
|
6
|
+
dialog driving callback props until the host dropped the modal state. The
|
|
7
|
+
Python framework dialogs are ``ModalScreen[Result]`` values, so the router
|
|
8
|
+
becomes a *table of awaited flows*: :func:`open_overlay` looks the requested
|
|
9
|
+
:data:`~induscode.console.contract.ModalKind` up in
|
|
10
|
+
:data:`OVERLAY_HANDLERS`, awaits the matching ``push_screen_wait`` flow, and
|
|
11
|
+
boxes whatever reducer events the flow accumulated into a typed
|
|
12
|
+
:class:`OverlayOutcome`.
|
|
13
|
+
|
|
14
|
+
The reducer stays the modal bookkeeping authority: the App dispatches
|
|
15
|
+
``modal:open`` *before* awaiting :func:`open_overlay` and ``modal:close``
|
|
16
|
+
after folding the outcome's events, so :class:`ConsoleState.modal` mirrors
|
|
17
|
+
the screen stack exactly as the TS reducer mirrored the mounted dialog —
|
|
18
|
+
one open/close pair per user-raised overlay, even for the TS flows that
|
|
19
|
+
re-dispatched mid-dialog (sign-in → oauth is an awaited sub-flow now; see
|
|
20
|
+
``auth.py``).
|
|
21
|
+
|
|
22
|
+
``push_screen_wait`` requires an active Textual worker, so the App awaits
|
|
23
|
+
:func:`open_overlay` from a worker (``app.run_worker``), never from a
|
|
24
|
+
message handler directly.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from typing import TYPE_CHECKING, Any, Awaitable, Callable, Final, Mapping, TypeAlias
|
|
31
|
+
|
|
32
|
+
from induscode.console.contract import ConsoleEvent, ModalKind, OverlayServices
|
|
33
|
+
|
|
34
|
+
from .auth import run_oauth_flow, run_plugin, run_sign_in, run_sign_out
|
|
35
|
+
from .pickers import (
|
|
36
|
+
run_model_picker,
|
|
37
|
+
run_scoped_models,
|
|
38
|
+
run_settings_picker,
|
|
39
|
+
run_theme_picker,
|
|
40
|
+
)
|
|
41
|
+
from .sessions import run_prior_turns, run_session_picker, run_tree_navigator
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from textual.app import App
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"OVERLAY_HANDLERS",
|
|
48
|
+
"OverlayFlow",
|
|
49
|
+
"OverlayOutcome",
|
|
50
|
+
"open_overlay",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True, slots=True)
|
|
55
|
+
class OverlayOutcome:
|
|
56
|
+
"""What one overlay flow settles with — the typed result the App folds.
|
|
57
|
+
|
|
58
|
+
``kind`` names the overlay that ran; ``events`` are the reducer events
|
|
59
|
+
the flow accumulated (a committed ``scheme:set``, settings ``toggle:*``
|
|
60
|
+
flips, sign-in ``status:set`` reports — never ``modal:*``, which stay the
|
|
61
|
+
App's bookkeeping around the await).
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
#: The modal kind this outcome settles.
|
|
65
|
+
kind: ModalKind
|
|
66
|
+
#: Reducer events to fold once the overlay has closed, oldest first.
|
|
67
|
+
events: tuple[ConsoleEvent, ...] = ()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
#: One overlay flow: push the dialog(s) for a kind, await dismissal, apply
|
|
71
|
+
#: side effects, and return the reducer events to fold.
|
|
72
|
+
OverlayFlow: TypeAlias = Callable[
|
|
73
|
+
["App[Any]", object | None, OverlayServices | None],
|
|
74
|
+
Awaitable[tuple[ConsoleEvent, ...]],
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def _run_none(
|
|
79
|
+
app: "App[Any]",
|
|
80
|
+
payload: object | None,
|
|
81
|
+
services: OverlayServices | None,
|
|
82
|
+
) -> tuple[ConsoleEvent, ...]:
|
|
83
|
+
"""The inert ``none`` kind: nothing to raise (the TS host's
|
|
84
|
+
``kind === "none"`` short-circuit)."""
|
|
85
|
+
return ()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
#: The closed kind → flow dispatch table (TS routed by union exhaustiveness;
|
|
89
|
+
#: the key-coverage test pins this table against ``MODAL_KINDS``).
|
|
90
|
+
OVERLAY_HANDLERS: Final[Mapping[ModalKind, OverlayFlow]] = {
|
|
91
|
+
"none": _run_none,
|
|
92
|
+
"settings": run_settings_picker,
|
|
93
|
+
"models": run_model_picker,
|
|
94
|
+
"scopedModels": run_scoped_models,
|
|
95
|
+
"theme": run_theme_picker,
|
|
96
|
+
"sessions": run_session_picker,
|
|
97
|
+
"tree": run_tree_navigator,
|
|
98
|
+
"userTurns": run_prior_turns,
|
|
99
|
+
"signIn": run_sign_in,
|
|
100
|
+
"signOut": run_sign_out,
|
|
101
|
+
"oauth": run_oauth_flow,
|
|
102
|
+
"plugin": run_plugin,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async def open_overlay(
|
|
107
|
+
app: "App[Any]",
|
|
108
|
+
kind: ModalKind,
|
|
109
|
+
payload: object | None = None,
|
|
110
|
+
services: OverlayServices | None = None,
|
|
111
|
+
) -> OverlayOutcome:
|
|
112
|
+
"""Run the overlay flow for ``kind`` and settle with its outcome.
|
|
113
|
+
|
|
114
|
+
The single entry point the App's modal plumbing awaits (from a worker —
|
|
115
|
+
``push_screen_wait`` demands one). An unknown kind, the ``none`` kind,
|
|
116
|
+
and any kind opened without the runtime ``services`` it needs all settle
|
|
117
|
+
inert — the TS groups rendered nothing for exactly those cases.
|
|
118
|
+
|
|
119
|
+
:param app: the Textual app the dialogs are pushed onto
|
|
120
|
+
:param kind: which overlay to raise
|
|
121
|
+
:param payload: the opaque per-modal payload, narrowed by each flow
|
|
122
|
+
:param services: the runtime handles overlays drive; absent on headless
|
|
123
|
+
mount paths
|
|
124
|
+
"""
|
|
125
|
+
handler = OVERLAY_HANDLERS.get(kind)
|
|
126
|
+
if handler is None:
|
|
127
|
+
return OverlayOutcome(kind=kind)
|
|
128
|
+
events = await handler(app, payload, services)
|
|
129
|
+
return OverlayOutcome(kind=kind, events=events)
|