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,836 @@
1
+ """Console contract — the FROZEN type surface of the interactive terminal shell.
2
+
3
+ Port of TS ``src/console/contract.ts``. This module is the single typed seam
4
+ between the coding-agent *product* and the Textual front-end that drives a
5
+ session from a live terminal. It declares *only* shapes plus a handful of tiny
6
+ inert helpers (a scheme-name guard, a fresh empty-state constant, and a pure
7
+ modal transition) — no rendering, no key handlers, no I/O. Every later console
8
+ module (the reducer, the theme engine, the startup gatherer, and the M5-wave-2
9
+ ``ConsoleApp`` surface) is written against the names declared here, so the
10
+ file is intentionally small, append-mostly, and stable.
11
+
12
+ Design stance — the shell is *wiring*, not widgets:
13
+
14
+ 1. **One immutable reducer state.** The terminal surface's UI-local state is
15
+ reduced from a single :class:`ConsoleState` value mutated only through a
16
+ :data:`ConsoleEvent` action union dispatched into
17
+ :func:`induscode.console.reducer.console_reducer`. The discriminants are
18
+ the console's own ``domain:verb`` vocabulary, kept verbatim from TS.
19
+ 2. **A data-driven slash registry.** Slash commands are rows in a
20
+ :class:`SlashRegistry`, never an ``if``-ladder — the whole slash framework
21
+ already landed as :mod:`induscode.console_slash` (M1) and is re-exported
22
+ here so console consumers import one surface.
23
+ 3. **Theme behind the framework adapter.** The console speaks in semantic
24
+ :class:`ThemeTokens`; the concrete terminal colours are produced once, at
25
+ the config-load boundary, by mapping a :class:`ThemePalette` through the
26
+ framework ``create_theme_bundle``. The accent ramp is the console's own.
27
+ 4. **The console drives a conductor.** :class:`ConsoleProps` carries a
28
+ ``SessionConductor``; the surface subscribes to its ``SessionSignal``
29
+ stream and renders its ``ConductorState``. The conductor surface is the
30
+ runtime contract and is never re-declared here.
31
+
32
+ Port deltas (locked by the port plan, analysis 02 §7):
33
+
34
+ - **Composer buffer / caret / history are GONE.** The TS reducer owned a
35
+ hand-rolled single-line editor (``buffer``/``caret``/``history``/
36
+ ``historyAt``/``stash`` state; ``buffer:*``/``caret:*``/``history:*``
37
+ events). The Python build delegates all of that to the framework's
38
+ ``EditorCore``-backed ``PromptEditor``, so :class:`ConsoleState` keeps only
39
+ the rows/blocks/modal/status/scheme/toggles/busy slices and the event union
40
+ drops the editor families.
41
+ - **``Themed`` / ``RenderSlot`` are dropped** — Textual CSS variables and
42
+ ``compose()`` replace the per-component theme prop and the ReactNode slot.
43
+ - ``ConsoleEventOf`` (TS type-level extraction) is replaced by the
44
+ :data:`CONSOLE_EVENT_TYPES` tuple plus a reducer key-coverage test, per the
45
+ cross-cutting exhaustiveness rule.
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ from collections.abc import Awaitable, Callable
51
+ from dataclasses import dataclass
52
+ from typing import ClassVar, Final, Literal, Protocol, TypeAlias, TypeGuard
53
+
54
+ from indusagi.react_ink import (
55
+ InkThemeAdapter,
56
+ SessionSnapshot,
57
+ StatusMessage,
58
+ ThemeBundle,
59
+ ToolExecutionState,
60
+ UiDisplayBlock,
61
+ )
62
+
63
+ from induscode.conductor import ConductorState, SessionConductor, SessionSignal
64
+ from induscode.console_slash import (
65
+ Handled,
66
+ OpenModal,
67
+ Prompt,
68
+ SlashCommand,
69
+ SlashContext,
70
+ SlashOutcome,
71
+ SlashRegistry,
72
+ SlashRun,
73
+ Unknown,
74
+ )
75
+ from induscode.launch import (
76
+ AuthVault,
77
+ LoginProvider,
78
+ OAuthLoginCallbacks,
79
+ OAuthLoginResult,
80
+ )
81
+ from induscode.sessions import SessionLibrary
82
+ from induscode.settings import PreferenceStore
83
+
84
+ __all__ = [
85
+ "BlockAppend",
86
+ "BlocksClear",
87
+ "BusySet",
88
+ "CONSOLE_EVENT_TYPES",
89
+ "ConductorState",
90
+ "ConsoleDispatch",
91
+ "ConsoleEvent",
92
+ "ConsoleEventType",
93
+ "ConsoleHost",
94
+ "ConsoleProps",
95
+ "ConsoleReducer",
96
+ "ConsoleState",
97
+ "ConsoleTheme",
98
+ "DEFAULT_SCHEME",
99
+ "EMPTY_CONSOLE_STATE",
100
+ "Handled",
101
+ "InkThemeAdapter",
102
+ "MODAL_KINDS",
103
+ "ModalClose",
104
+ "ModalKind",
105
+ "ModalOpen",
106
+ "ModalState",
107
+ "NO_MODAL",
108
+ "OpenModal",
109
+ "OverlayServices",
110
+ "Prompt",
111
+ "RowsAppend",
112
+ "RowsPatch",
113
+ "RowsSet",
114
+ "SchemeSet",
115
+ "SessionConductor",
116
+ "SessionSignal",
117
+ "SessionSnapshot",
118
+ "SlashCommand",
119
+ "SlashContext",
120
+ "SlashOutcome",
121
+ "SlashRegistry",
122
+ "SlashRun",
123
+ "StartOAuthLogin",
124
+ "StatusClear",
125
+ "StatusMessage",
126
+ "StatusSet",
127
+ "THEME_TOKEN_ROLES",
128
+ "ThemePalette",
129
+ "ThemeScheme",
130
+ "ThemeToken",
131
+ "ThemeTokens",
132
+ "Tick",
133
+ "ToggleImages",
134
+ "ToggleReasoning",
135
+ "ToolExecutionState",
136
+ "UiDisplayBlock",
137
+ "Unknown",
138
+ "ViewRow",
139
+ "ViewRowKind",
140
+ "is_theme_scheme",
141
+ "transition_modal",
142
+ ]
143
+
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # Theme — schemes, semantic tokens, and the accent palette
147
+ # ---------------------------------------------------------------------------
148
+
149
+ #: The built-in colour schemes the console ships with.
150
+ #:
151
+ #: Named for the time-of-day they evoke rather than a bare light/dark axis,
152
+ #: with a daltonized (color-blind-friendly) variant of each:
153
+ #:
154
+ #: - ``midnight`` — a low-luminance scheme for dark terminals.
155
+ #: - ``daylight`` — a high-luminance scheme for light terminals.
156
+ #: - ``midnight-cb`` — the dark scheme re-derived so success vs failure
157
+ #: separates off the red-green axis (success → blue), deuteran/protan-safe.
158
+ #: - ``daylight-cb`` — the light scheme's color-blind-safe counterpart.
159
+ ThemeScheme: TypeAlias = Literal["midnight", "daylight", "midnight-cb", "daylight-cb"]
160
+
161
+ #: The default scheme applied before any user preference is loaded.
162
+ DEFAULT_SCHEME: Final[ThemeScheme] = "midnight"
163
+
164
+ #: The closed scheme-name set the guard validates against.
165
+ _SCHEME_NAMES: Final[frozenset[str]] = frozenset(
166
+ {"midnight", "daylight", "midnight-cb", "daylight-cb"}
167
+ )
168
+
169
+
170
+ def is_theme_scheme(value: str) -> TypeGuard[ThemeScheme]:
171
+ """Narrow an arbitrary string to a known :data:`ThemeScheme`.
172
+
173
+ The single sanctioned guard for scheme names, so every loader validates
174
+ the same way. Returns ``True`` only for a recognised scheme literal.
175
+
176
+ :param value: any candidate string read from settings or a slash argument
177
+ """
178
+ return value in _SCHEME_NAMES
179
+
180
+
181
+ @dataclass(frozen=True, slots=True)
182
+ class ThemeTokens:
183
+ """The semantic colour/style tokens the console renders in.
184
+
185
+ These are *roles*, not literals: a component asks for ``frame`` or
186
+ ``signal``, never a hex code. Each token is resolved to a concrete colour
187
+ by the framework :class:`~indusagi.react_ink.InkThemeAdapter` at the
188
+ boundary; the console body never touches a hex value directly. The token
189
+ set is closed (a frozen dataclass), so a scheme that omits one fails at
190
+ construction rather than rendering an undefined colour.
191
+ """
192
+
193
+ # The primary accent (active prompt, selection, focus).
194
+ signal: str
195
+ # Default panel/border lines and structural chrome.
196
+ frame: str
197
+ # De-emphasised borders and separators.
198
+ quiet_frame: str
199
+ # The composer's active background tint.
200
+ prompt_surface: str
201
+ # Accent applied to message-row gutters and headers.
202
+ card_accent: str
203
+ # Default foreground for assistant/answer text.
204
+ body_text: str
205
+ # Secondary/metadata text (timestamps, hints).
206
+ muted_text: str
207
+ # High-contrast text on an accented surface.
208
+ ink_text: str
209
+ # Informational status tone.
210
+ notice: str
211
+ # Success status tone.
212
+ affirm: str
213
+ # Warning status tone.
214
+ caution: str
215
+ # Error/fault status tone.
216
+ alarm: str
217
+ # Busy/in-flight status tone.
218
+ pending: str
219
+ # Rich-render roles (markdown / diff / syntax highlighting). These feed
220
+ # the framework adapter's markdown/diff/highlight accessors so the styled
221
+ # transcript, colored diffs, and fenced-code highlighting recolor for
222
+ # free with each scheme.
223
+ # Inline ``code`` foreground.
224
+ code_inline: str
225
+ # Markdown heading foreground.
226
+ heading: str
227
+ # The dim bar drawn beside a blockquote.
228
+ blockquote_bar: str
229
+ # Background tint for an added (``+``) diff line.
230
+ diff_added_bg: str
231
+ # Background tint for a removed (``-``) diff line.
232
+ diff_removed_bg: str
233
+ # Foreground for added-line content / ``+`` marker.
234
+ diff_added_text: str
235
+ # Foreground for removed-line content / ``-`` marker.
236
+ diff_removed_text: str
237
+ # Syntax scope: keywords.
238
+ syn_keyword: str
239
+ # Syntax scope: string literals.
240
+ syn_string: str
241
+ # Syntax scope: numeric literals.
242
+ syn_number: str
243
+ # Syntax scope: comments.
244
+ syn_comment: str
245
+ # Syntax scope: types / classes / built-ins.
246
+ syn_type: str
247
+
248
+
249
+ #: One token role name (TS ``ThemeToken = keyof ThemeTokens``).
250
+ ThemeToken: TypeAlias = Literal[
251
+ "signal",
252
+ "frame",
253
+ "quiet_frame",
254
+ "prompt_surface",
255
+ "card_accent",
256
+ "body_text",
257
+ "muted_text",
258
+ "ink_text",
259
+ "notice",
260
+ "affirm",
261
+ "caution",
262
+ "alarm",
263
+ "pending",
264
+ "code_inline",
265
+ "heading",
266
+ "blockquote_bar",
267
+ "diff_added_bg",
268
+ "diff_removed_bg",
269
+ "diff_added_text",
270
+ "diff_removed_text",
271
+ "syn_keyword",
272
+ "syn_string",
273
+ "syn_number",
274
+ "syn_comment",
275
+ "syn_type",
276
+ ]
277
+
278
+ #: The closed set of token role names, for iteration and validation (the
279
+ #: Python stand-in for TS ``keyof`` — tests fold over this).
280
+ THEME_TOKEN_ROLES: Final[tuple[ThemeToken, ...]] = (
281
+ "signal",
282
+ "frame",
283
+ "quiet_frame",
284
+ "prompt_surface",
285
+ "card_accent",
286
+ "body_text",
287
+ "muted_text",
288
+ "ink_text",
289
+ "notice",
290
+ "affirm",
291
+ "caution",
292
+ "alarm",
293
+ "pending",
294
+ "code_inline",
295
+ "heading",
296
+ "blockquote_bar",
297
+ "diff_added_bg",
298
+ "diff_removed_bg",
299
+ "diff_added_text",
300
+ "diff_removed_text",
301
+ "syn_keyword",
302
+ "syn_string",
303
+ "syn_number",
304
+ "syn_comment",
305
+ "syn_type",
306
+ )
307
+
308
+
309
+ @dataclass(frozen=True, slots=True)
310
+ class ThemePalette:
311
+ """The accent ramp a scheme is derived from — the console's own hex values.
312
+
313
+ A palette is the small raw-colour source from which the full
314
+ :class:`ThemeTokens` map is computed at load time. These nine stops are an
315
+ original ramp (re-derived for this console; they intentionally do not
316
+ reuse any upstream accent values) spanning a cool primary, a warm
317
+ secondary, and a neutral text gradient. The boundary adapter expands them
318
+ into the semantic token map and hands the result to the framework
319
+ ``create_theme_bundle``.
320
+ """
321
+
322
+ # Primary accent — the dominant cool hue.
323
+ primary: str
324
+ # Secondary accent — a complementary warm hue.
325
+ secondary: str
326
+ # Tertiary accent — a muted support hue.
327
+ tertiary: str
328
+ # Brightest neutral — high-contrast text.
329
+ ink: str
330
+ # Mid neutral — default body text.
331
+ body: str
332
+ # Dim neutral — secondary/metadata text.
333
+ muted: str
334
+ # Success hue.
335
+ affirm: str
336
+ # Warning hue.
337
+ caution: str
338
+ # Error hue.
339
+ alarm: str
340
+
341
+
342
+ @dataclass(frozen=True, slots=True)
343
+ class ConsoleTheme:
344
+ """A fully resolved scheme: identity, raw ramp, and the semantic token map
345
+ the console renders in.
346
+
347
+ The ``tokens`` are derived from ``palette`` at load time; ``bundle`` is
348
+ the framework projection produced from those tokens — the Rich-painting
349
+ adapter **plus** the registered Textual ``Theme`` and the Pygments style
350
+ (the Python boundary is ``create_theme_bundle``, not the bare adapter).
351
+ A scheme is the unit the theme picker selects.
352
+ """
353
+
354
+ # Which scheme this resolves.
355
+ scheme: ThemeScheme
356
+ # The raw accent ramp this scheme was derived from.
357
+ palette: ThemePalette
358
+ # The semantic token map components render against.
359
+ tokens: ThemeTokens
360
+ # The framework adapter that turns token roles into terminal colours.
361
+ adapter: InkThemeAdapter
362
+ # The full framework bundle: adapter + Textual Theme + Pygments style.
363
+ bundle: ThemeBundle
364
+
365
+
366
+ # ---------------------------------------------------------------------------
367
+ # Transcript view rows
368
+ # ---------------------------------------------------------------------------
369
+
370
+ #: The discriminant of a single rendered transcript row.
371
+ #:
372
+ #: The console renders its scrollback as a flat list of typed rows rather
373
+ #: than a single message blob, so each kind can be width-packed and themed
374
+ #: independently:
375
+ #:
376
+ #: - ``prompt`` — a user-submitted turn.
377
+ #: - ``answer`` — streamed assistant answer text.
378
+ #: - ``reason`` — streamed reasoning/thinking text (shown only when enabled).
379
+ #: - ``toolRun`` — a tool invocation row, correlated by ``run_id``.
380
+ #: - ``notice`` — an out-of-band notice (changelog, status, system note).
381
+ ViewRowKind: TypeAlias = Literal["prompt", "answer", "reason", "toolRun", "notice"]
382
+
383
+
384
+ @dataclass(frozen=True, slots=True)
385
+ class ViewRow:
386
+ """One immutable row in the transcript view.
387
+
388
+ Rows are append-mostly and identity-stable: a streaming
389
+ ``answer``/``reason`` row keeps its ``id`` while its ``text`` grows, so
390
+ the renderer can reconcile in place. ``run_id`` correlates a ``toolRun``
391
+ row with its :class:`~indusagi.react_ink.ToolExecutionState` entry.
392
+ """
393
+
394
+ # Stable unique row id.
395
+ id: str
396
+ # Which kind of row this is.
397
+ kind: ViewRowKind
398
+ # The row's display text (may be partial while streaming).
399
+ text: str
400
+ # Tool-run correlation id, present only for ``toolRun`` rows.
401
+ run_id: str | None = None
402
+
403
+
404
+ # ---------------------------------------------------------------------------
405
+ # Modal / overlay state
406
+ # ---------------------------------------------------------------------------
407
+
408
+ #: The closed set of modal overlays the console can raise over the transcript.
409
+ #:
410
+ #: Exactly one modal is active at a time (or ``none``). The surface renders
411
+ #: the matching framework dialog from a table keyed by this discriminant
412
+ #: rather than a hand-written switch:
413
+ #:
414
+ #: - ``none`` — no overlay; the composer has focus.
415
+ #: - ``settings`` — the settings list.
416
+ #: - ``models`` — the single-model picker.
417
+ #: - ``scopedModels`` — the per-scope model picker.
418
+ #: - ``theme`` — the scheme picker.
419
+ #: - ``sessions`` — the session resume/list picker.
420
+ #: - ``tree`` — the transcript-tree navigator.
421
+ #: - ``userTurns`` — the prior-user-turn picker (for branching/forking).
422
+ #: - ``signIn`` — the provider sign-in launcher.
423
+ #: - ``signOut`` — the provider sign-out confirmation.
424
+ #: - ``oauth`` — the in-flight OAuth device/redirect flow.
425
+ #: - ``plugin`` — a plugin-supplied select/confirm/input/custom overlay.
426
+ ModalKind: TypeAlias = Literal[
427
+ "none",
428
+ "settings",
429
+ "models",
430
+ "scopedModels",
431
+ "theme",
432
+ "sessions",
433
+ "tree",
434
+ "userTurns",
435
+ "signIn",
436
+ "signOut",
437
+ "oauth",
438
+ "plugin",
439
+ ]
440
+
441
+ #: The closed modal-kind set, for the overlay dispatch table and its
442
+ #: key-coverage test (TS relied on union exhaustiveness).
443
+ MODAL_KINDS: Final[tuple[ModalKind, ...]] = (
444
+ "none",
445
+ "settings",
446
+ "models",
447
+ "scopedModels",
448
+ "theme",
449
+ "sessions",
450
+ "tree",
451
+ "userTurns",
452
+ "signIn",
453
+ "signOut",
454
+ "oauth",
455
+ "plugin",
456
+ )
457
+
458
+
459
+ @dataclass(frozen=True, slots=True)
460
+ class ModalState:
461
+ """The active modal plus any opaque payload the matching dialog needs.
462
+
463
+ ``payload`` is deliberately untyped at the contract layer: each dialog
464
+ narrows it at its own boundary (e.g. the OAuth flow state, a plugin
465
+ overlay request). ``none`` carries no payload.
466
+ """
467
+
468
+ # Which overlay is currently raised.
469
+ kind: ModalKind
470
+ # Opaque per-modal payload, narrowed by the rendering dialog.
471
+ payload: object | None = None
472
+
473
+
474
+ #: The inert "no overlay" modal state.
475
+ NO_MODAL: Final[ModalState] = ModalState(kind="none")
476
+
477
+
478
+ def transition_modal(next_kind: ModalKind, payload: object | None = None) -> ModalState:
479
+ """Compute the next :class:`ModalState` for a requested transition.
480
+
481
+ A pure, inert helper so the open/close/replace logic lives in one place
482
+ rather than being re-derived per call site. Opening any kind other than
483
+ ``none`` raises that overlay (carrying the optional payload); opening
484
+ ``none`` (or closing) returns :data:`NO_MODAL`.
485
+
486
+ :param next_kind: the modal kind to transition to
487
+ :param payload: optional payload for the target overlay
488
+ """
489
+ if next_kind == "none":
490
+ return NO_MODAL
491
+ return ModalState(kind=next_kind, payload=payload)
492
+
493
+
494
+ # ---------------------------------------------------------------------------
495
+ # The reducer state
496
+ # ---------------------------------------------------------------------------
497
+
498
+
499
+ @dataclass(frozen=True, slots=True)
500
+ class ConsoleState:
501
+ """The immutable UI-local state the terminal surface is reduced from.
502
+
503
+ Every observable UI-local property of the console — what is on screen,
504
+ which overlay is up, and the display toggles — is one read-only field
505
+ here. The surface holds exactly one of these and never mutates it in
506
+ place; a :data:`ConsoleEvent` produces a fresh value. Live session data
507
+ (messages, usage, model) is *not* stored here — it is projected from the
508
+ conductor snapshot — and the composer text/caret/history live in the
509
+ framework editor (``EditorCore``), not in this state (port delta; see the
510
+ module docstring).
511
+ """
512
+
513
+ # The rendered transcript, newest row last.
514
+ rows: tuple[ViewRow, ...] = ()
515
+ # Out-of-band display blocks (changelog, etc.) interleaved with the rows.
516
+ blocks: tuple[UiDisplayBlock, ...] = ()
517
+ # The active modal overlay.
518
+ modal: ModalState = NO_MODAL
519
+ # The transient status line, when one is showing.
520
+ status: StatusMessage | None = None
521
+ # The active colour scheme.
522
+ scheme: ThemeScheme = DEFAULT_SCHEME
523
+ # Whether reasoning/thinking rows are shown.
524
+ show_reasoning: bool = False
525
+ # Whether inline images are rendered.
526
+ show_images: bool = True
527
+ # Whether a turn is in flight (input disabled / spinner shown).
528
+ busy: bool = False
529
+ # Monotonic tick that forces a status-bar re-render on data change.
530
+ tick: int = 0
531
+
532
+
533
+ #: The freshly-seeded :class:`ConsoleState` a session opens with.
534
+ #:
535
+ #: A pure constant (no session data baked in) so the initial render is
536
+ #: deterministic and the store can be re-seeded without consulting any
537
+ #: runtime. Loaders overlay user preferences (scheme, toggles) on top of this
538
+ #: base via :func:`induscode.console.reducer.init_console_state`.
539
+ EMPTY_CONSOLE_STATE: Final[ConsoleState] = ConsoleState()
540
+
541
+
542
+ # ---------------------------------------------------------------------------
543
+ # The reducer event union
544
+ # ---------------------------------------------------------------------------
545
+ #
546
+ # The closed action union the console reducer folds over ConsoleState. The
547
+ # discriminants are the console's own ``domain:verb`` vocabulary, kept
548
+ # verbatim from TS. The TS composer families (``buffer:*``, ``caret:*``,
549
+ # ``history:*``) are dropped — the framework EditorCore owns those (port
550
+ # delta; see the module docstring). Grouped by the slice each touches:
551
+ #
552
+ # Transcript view
553
+ # - ``rows:set`` — replace the whole row list.
554
+ # - ``rows:append`` — append one row.
555
+ # - ``rows:patch`` — replace the text of the row matching an id.
556
+ # - ``block:append`` — append a display block.
557
+ # - ``blocks:clear`` — drop every display block (e.g. on a new session).
558
+ #
559
+ # Overlays, status, theme, toggles, busy
560
+ # - ``modal:open`` — raise an overlay (with optional payload).
561
+ # - ``modal:close`` — drop back to the composer.
562
+ # - ``status:set`` — show a transient status message.
563
+ # - ``status:clear`` — clear the status line.
564
+ # - ``scheme:set`` — switch the active colour scheme.
565
+ # - ``toggle:reasoning`` — flip reasoning-row visibility.
566
+ # - ``toggle:images`` — flip inline-image rendering.
567
+ # - ``busy:set`` — set the in-flight flag.
568
+ # - ``tick`` — bump the status-bar re-render tick.
569
+
570
+
571
+ @dataclass(frozen=True, slots=True)
572
+ class RowsSet:
573
+ """Replace the whole row list."""
574
+
575
+ type: ClassVar[Literal["rows:set"]] = "rows:set"
576
+ rows: tuple[ViewRow, ...]
577
+
578
+
579
+ @dataclass(frozen=True, slots=True)
580
+ class RowsAppend:
581
+ """Append one row."""
582
+
583
+ type: ClassVar[Literal["rows:append"]] = "rows:append"
584
+ row: ViewRow
585
+
586
+
587
+ @dataclass(frozen=True, slots=True)
588
+ class RowsPatch:
589
+ """Replace the text of the row matching ``id``."""
590
+
591
+ type: ClassVar[Literal["rows:patch"]] = "rows:patch"
592
+ id: str
593
+ text: str
594
+
595
+
596
+ @dataclass(frozen=True, slots=True)
597
+ class BlockAppend:
598
+ """Append a display block."""
599
+
600
+ type: ClassVar[Literal["block:append"]] = "block:append"
601
+ block: UiDisplayBlock
602
+
603
+
604
+ @dataclass(frozen=True, slots=True)
605
+ class BlocksClear:
606
+ """Drop every display block (e.g. on a new session)."""
607
+
608
+ type: ClassVar[Literal["blocks:clear"]] = "blocks:clear"
609
+
610
+
611
+ @dataclass(frozen=True, slots=True)
612
+ class ModalOpen:
613
+ """Raise an overlay (with optional payload)."""
614
+
615
+ type: ClassVar[Literal["modal:open"]] = "modal:open"
616
+ kind: ModalKind
617
+ payload: object | None = None
618
+
619
+
620
+ @dataclass(frozen=True, slots=True)
621
+ class ModalClose:
622
+ """Drop back to the composer."""
623
+
624
+ type: ClassVar[Literal["modal:close"]] = "modal:close"
625
+
626
+
627
+ @dataclass(frozen=True, slots=True)
628
+ class StatusSet:
629
+ """Show a transient status message."""
630
+
631
+ type: ClassVar[Literal["status:set"]] = "status:set"
632
+ status: StatusMessage
633
+
634
+
635
+ @dataclass(frozen=True, slots=True)
636
+ class StatusClear:
637
+ """Clear the status line."""
638
+
639
+ type: ClassVar[Literal["status:clear"]] = "status:clear"
640
+
641
+
642
+ @dataclass(frozen=True, slots=True)
643
+ class SchemeSet:
644
+ """Switch the active colour scheme."""
645
+
646
+ type: ClassVar[Literal["scheme:set"]] = "scheme:set"
647
+ scheme: ThemeScheme
648
+
649
+
650
+ @dataclass(frozen=True, slots=True)
651
+ class ToggleReasoning:
652
+ """Flip reasoning-row visibility."""
653
+
654
+ type: ClassVar[Literal["toggle:reasoning"]] = "toggle:reasoning"
655
+
656
+
657
+ @dataclass(frozen=True, slots=True)
658
+ class ToggleImages:
659
+ """Flip inline-image rendering."""
660
+
661
+ type: ClassVar[Literal["toggle:images"]] = "toggle:images"
662
+
663
+
664
+ @dataclass(frozen=True, slots=True)
665
+ class BusySet:
666
+ """Set the in-flight flag."""
667
+
668
+ type: ClassVar[Literal["busy:set"]] = "busy:set"
669
+ busy: bool
670
+
671
+
672
+ @dataclass(frozen=True, slots=True)
673
+ class Tick:
674
+ """Bump the status-bar re-render tick."""
675
+
676
+ type: ClassVar[Literal["tick"]] = "tick"
677
+
678
+
679
+ #: The closed action union the console reducer folds over ConsoleState.
680
+ ConsoleEvent: TypeAlias = (
681
+ RowsSet
682
+ | RowsAppend
683
+ | RowsPatch
684
+ | BlockAppend
685
+ | BlocksClear
686
+ | ModalOpen
687
+ | ModalClose
688
+ | StatusSet
689
+ | StatusClear
690
+ | SchemeSet
691
+ | ToggleReasoning
692
+ | ToggleImages
693
+ | BusySet
694
+ | Tick
695
+ )
696
+
697
+ #: The discriminant literals of :data:`ConsoleEvent`, for filtering/logging.
698
+ ConsoleEventType: TypeAlias = Literal[
699
+ "rows:set",
700
+ "rows:append",
701
+ "rows:patch",
702
+ "block:append",
703
+ "blocks:clear",
704
+ "modal:open",
705
+ "modal:close",
706
+ "status:set",
707
+ "status:clear",
708
+ "scheme:set",
709
+ "toggle:reasoning",
710
+ "toggle:images",
711
+ "busy:set",
712
+ "tick",
713
+ ]
714
+
715
+ #: Every event discriminant, in declaration order — the reducer key-coverage
716
+ #: test folds one event of each tag through the reducer (the Python analogue
717
+ #: of TS union exhaustiveness; cross-cutting rule 1).
718
+ CONSOLE_EVENT_TYPES: Final[tuple[ConsoleEventType, ...]] = (
719
+ "rows:set",
720
+ "rows:append",
721
+ "rows:patch",
722
+ "block:append",
723
+ "blocks:clear",
724
+ "modal:open",
725
+ "modal:close",
726
+ "status:set",
727
+ "status:clear",
728
+ "scheme:set",
729
+ "toggle:reasoning",
730
+ "toggle:images",
731
+ "busy:set",
732
+ "tick",
733
+ )
734
+
735
+ #: The reducer signature the console store is built from.
736
+ ConsoleReducer: TypeAlias = Callable[[ConsoleState, ConsoleEvent], ConsoleState]
737
+
738
+ #: The bound dispatch function the surface threads to its children.
739
+ ConsoleDispatch: TypeAlias = Callable[[ConsoleEvent], None]
740
+
741
+
742
+ # ---------------------------------------------------------------------------
743
+ # The console host props
744
+ # ---------------------------------------------------------------------------
745
+
746
+
747
+ class StartOAuthLogin(Protocol):
748
+ """Drive a provider's browser sign-in and persist the result through the
749
+ vault: ``await start_oauth_login(provider_id, callbacks, vault, account)``.
750
+ """
751
+
752
+ def __call__(
753
+ self,
754
+ provider_id: str,
755
+ callbacks: OAuthLoginCallbacks,
756
+ vault: AuthVault,
757
+ account: str | None = None,
758
+ ) -> Awaitable[OAuthLoginResult]: ...
759
+
760
+
761
+ @dataclass(frozen=True, slots=True)
762
+ class OverlayServices:
763
+ """The runtime handles the modal overlays reach for when they open.
764
+
765
+ The console surface itself stays UI-local — it knows nothing about
766
+ settings files, the session catalog, or the credential store. Each
767
+ overlay that needs to drive one of those is handed this bundle (threaded
768
+ through :attr:`ConsoleProps.services`), so a dialog body can read/write a
769
+ preference, list and open saved sessions, enumerate sign-in providers,
770
+ run a browser login, and persist credentials — all without prop-drilling
771
+ individual dependencies through the surface. It is optional on
772
+ :class:`ConsoleProps`: the headless and test mount paths run without it,
773
+ and an overlay group that finds it absent renders nothing.
774
+ """
775
+
776
+ # The session this console drives (model selection, branching, forking).
777
+ conductor: SessionConductor
778
+ # The two-tier preference reader/writer behind the settings overlays.
779
+ settings: PreferenceStore
780
+ # The catalog-and-navigation layer over persisted transcripts.
781
+ sessions: SessionLibrary
782
+ # Enumerate the merged sign-in directory (browser + api-key providers).
783
+ list_login_providers: Callable[[], list[LoginProvider]]
784
+ # Drive a provider's browser sign-in and persist the result via the vault.
785
+ start_oauth_login: StartOAuthLogin
786
+ # Open a sign-in url in the user's browser (Chrome-first per platform).
787
+ # Resolves True when a browser was launched, False otherwise — the OAuth
788
+ # overlay calls this so the consent page opens automatically, matching
789
+ # the CLI sign-in path.
790
+ open_login_url: Callable[[str], Awaitable[bool]]
791
+ # The credential store sign-in / sign-out overlays read and write.
792
+ vault: AuthVault
793
+
794
+
795
+ @dataclass(frozen=True, slots=True)
796
+ class ConsoleProps:
797
+ """What the root console surface receives at mount.
798
+
799
+ The console is a consumer: it is handed a fully-assembled
800
+ ``SessionConductor`` to drive, the resolved :class:`ConsoleTheme` to
801
+ render in, the :class:`SlashRegistry` to dispatch against, and a small
802
+ set of optional seeds (a first message, preloaded images, a request-exit
803
+ hook). It owns no runtime of its own.
804
+ """
805
+
806
+ # The session this console drives and renders.
807
+ conductor: SessionConductor
808
+ # The resolved colour scheme to render in.
809
+ theme: ConsoleTheme
810
+ # The slash-command registry the composer dispatches against.
811
+ slash: SlashRegistry
812
+ # An optional first user turn to submit on mount.
813
+ initial_input: str | None = None
814
+ # Optional local image paths to attach to the first turn.
815
+ initial_images: tuple[str, ...] = ()
816
+ # Whether to render verbose diagnostics in the banner.
817
+ verbose: bool = False
818
+ # Invoked when the console asks the host process to exit.
819
+ on_exit: Callable[[], None] | None = None
820
+ # The runtime handles the modal overlays drive; absent on headless paths.
821
+ services: OverlayServices | None = None
822
+
823
+
824
+ @dataclass(frozen=True, slots=True)
825
+ class ConsoleHost:
826
+ """The ambient context the surface threads to its descendants.
827
+
828
+ A narrow read-only handle on the things deep children need without prop
829
+ drilling: the conductor to drive and the exit request. Larger state flows
830
+ through the reducer, not this object.
831
+ """
832
+
833
+ # The session this console drives.
834
+ conductor: SessionConductor
835
+ # Ask the host process to exit the interactive console.
836
+ request_exit: Callable[[], None]