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,188 @@
|
|
|
1
|
+
"""Banner colour-sweep helpers — the pure, render-agnostic maths behind the
|
|
2
|
+
optional startup flourish and the personalized welcome line.
|
|
3
|
+
|
|
4
|
+
Port of TS ``src/console/components/banner-sweep.ts``. Two concerns, no
|
|
5
|
+
Textual or Rich, no I/O, no state:
|
|
6
|
+
|
|
7
|
+
- :func:`row_gradient` computes a *frozen* (single-pass, no animation timer)
|
|
8
|
+
colour for one wordmark row by lerping between two palette hexes across the
|
|
9
|
+
block's rows. The banner paints row ``i`` of ``n`` with this colour when the
|
|
10
|
+
opt-in sweep is on; off, the wordmark stays a flat accent. Because it takes
|
|
11
|
+
no clock and never schedules a frame, it is reduced-motion safe by
|
|
12
|
+
construction — the suppression decision lives at the call site, which simply
|
|
13
|
+
does not ask for a gradient when motion is reduced.
|
|
14
|
+
|
|
15
|
+
- :func:`welcome_line` formats the ``"Welcome back{, name}!"`` greeting from
|
|
16
|
+
the signed-in account label, with a length guard and a name-less fallback,
|
|
17
|
+
so the banner and its tests share one formatter rather than re-deriving the
|
|
18
|
+
string.
|
|
19
|
+
|
|
20
|
+
Everything here is referentially transparent: same inputs → same output.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import re
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Final
|
|
28
|
+
|
|
29
|
+
__all__ = ["row_gradient", "welcome_line"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Welcome line
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
#: The greeting shown when no signed-in account label is available.
|
|
37
|
+
_WELCOME_FALLBACK: Final[str] = "Welcome back!"
|
|
38
|
+
|
|
39
|
+
#: The longest account label the welcome line embeds before it is dropped.
|
|
40
|
+
#:
|
|
41
|
+
#: An account key can be an arbitrary string (an email, a display name, a
|
|
42
|
+
#: pasted token); past this width it stops reading as a name and starts
|
|
43
|
+
#: wrapping the banner, so an over-long label is treated as "no usable name"
|
|
44
|
+
#: and the fallback greeting is shown instead.
|
|
45
|
+
_MAX_NAME: Final[int] = 40
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def welcome_line(name: str | None = None) -> str:
|
|
49
|
+
"""Format the personalized welcome greeting.
|
|
50
|
+
|
|
51
|
+
Returns ``"Welcome back, {name}!"`` for a usable name, and the plain
|
|
52
|
+
``"Welcome back!"`` fallback when the name is absent, blank, or too long
|
|
53
|
+
to read as a name. The name is trimmed of surrounding whitespace before
|
|
54
|
+
the checks so a padded label does not slip past the guards.
|
|
55
|
+
|
|
56
|
+
:param name: the signed-in account label, if one is known
|
|
57
|
+
:returns: the greeting string (plain text — never markup)
|
|
58
|
+
"""
|
|
59
|
+
if name is None:
|
|
60
|
+
return _WELCOME_FALLBACK
|
|
61
|
+
trimmed = name.strip()
|
|
62
|
+
if len(trimmed) == 0 or len(trimmed) > _MAX_NAME:
|
|
63
|
+
return _WELCOME_FALLBACK
|
|
64
|
+
return f"Welcome back, {trimmed}!"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Hex colour maths
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True, slots=True)
|
|
73
|
+
class _Rgb:
|
|
74
|
+
"""One parsed colour channel triple, each component in the 0–255 range."""
|
|
75
|
+
|
|
76
|
+
r: int
|
|
77
|
+
g: int
|
|
78
|
+
b: int
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
_SIX_HEX_RE: Final[re.Pattern[str]] = re.compile(r"^[0-9a-fA-F]{6}$")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _parse_hex(hex_color: str) -> _Rgb | None:
|
|
85
|
+
"""Parse a ``#rrggbb`` (or ``#rgb``) hex string into its channel triple.
|
|
86
|
+
|
|
87
|
+
Tolerant of a missing leading ``#`` and of the short three-digit form
|
|
88
|
+
(each nibble is doubled). Returns ``None`` for anything it cannot read as
|
|
89
|
+
a colour, so the caller can fall back to the raw hex rather than emit
|
|
90
|
+
garbage.
|
|
91
|
+
"""
|
|
92
|
+
body = hex_color[1:] if hex_color.startswith("#") else hex_color
|
|
93
|
+
if len(body) == 3:
|
|
94
|
+
six = "".join(ch + ch for ch in body)
|
|
95
|
+
elif len(body) == 6:
|
|
96
|
+
six = body
|
|
97
|
+
else:
|
|
98
|
+
return None
|
|
99
|
+
if _SIX_HEX_RE.match(six) is None:
|
|
100
|
+
return None
|
|
101
|
+
return _Rgb(
|
|
102
|
+
r=int(six[0:2], 16),
|
|
103
|
+
g=int(six[2:4], 16),
|
|
104
|
+
b=int(six[4:6], 16),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _clamp_channel(value: float) -> int:
|
|
109
|
+
"""Clamp to the byte range and round so a lerp result is a legal channel.
|
|
110
|
+
|
|
111
|
+
Rounding is JS ``Math.round`` (half away from zero on the positive axis,
|
|
112
|
+
i.e. half-up) rather than Python's banker's rounding, so the produced
|
|
113
|
+
hexes match the TS build digit for digit.
|
|
114
|
+
"""
|
|
115
|
+
if value < 0:
|
|
116
|
+
return 0
|
|
117
|
+
if value > 255:
|
|
118
|
+
return 255
|
|
119
|
+
return int(value + 0.5)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _lerp_rgb(start: _Rgb, end: _Rgb, t: float) -> _Rgb:
|
|
123
|
+
"""Linear-interpolate between two channel triples by ``t`` in ``[0, 1]``.
|
|
124
|
+
|
|
125
|
+
``t = 0`` returns ``start``, ``t = 1`` returns ``end``, anything between
|
|
126
|
+
mixes the two channel-wise. ``t`` is clamped so an out-of-range fraction
|
|
127
|
+
cannot overshoot.
|
|
128
|
+
"""
|
|
129
|
+
k = 0.0 if t < 0 else 1.0 if t > 1 else t
|
|
130
|
+
return _Rgb(
|
|
131
|
+
r=_clamp_channel(start.r + (end.r - start.r) * k),
|
|
132
|
+
g=_clamp_channel(start.g + (end.g - start.g) * k),
|
|
133
|
+
b=_clamp_channel(start.b + (end.b - start.b) * k),
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _channel_hex(value: int) -> str:
|
|
138
|
+
"""Render one byte channel as a two-digit lowercase hex pair."""
|
|
139
|
+
return format(_clamp_channel(value), "02x")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _to_hex(rgb: _Rgb) -> str:
|
|
143
|
+
"""Render a channel triple as a ``#rrggbb`` hex string (the form Rich's
|
|
144
|
+
colour parser accepts)."""
|
|
145
|
+
return f"#{_channel_hex(rgb.r)}{_channel_hex(rgb.g)}{_channel_hex(rgb.b)}"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def row_gradient(
|
|
149
|
+
row_index: int,
|
|
150
|
+
row_count: int,
|
|
151
|
+
primary_hex: str,
|
|
152
|
+
secondary_hex: str,
|
|
153
|
+
) -> str:
|
|
154
|
+
"""The frozen sweep colour for one wordmark row.
|
|
155
|
+
|
|
156
|
+
Lerps from ``primary_hex`` (the first row) to ``secondary_hex`` (the last
|
|
157
|
+
row) across the ``row_count`` rows of the block, returning the colour for
|
|
158
|
+
row ``row_index`` as a ``#rrggbb`` hex string. A single-row block pins to
|
|
159
|
+
the primary; the endpoints are exact (row 0 = primary, row n-1 =
|
|
160
|
+
secondary). If either hex cannot be parsed, the corresponding raw input
|
|
161
|
+
hex is returned so the banner still gets a usable colour string rather
|
|
162
|
+
than nothing.
|
|
163
|
+
|
|
164
|
+
This computes a *static* gradient — there is no timer, no frame loop, no
|
|
165
|
+
clock. The whole sweep is laid down in the one render pass, which is
|
|
166
|
+
exactly why it is safe under reduced-motion: the flourish is colour, not
|
|
167
|
+
movement.
|
|
168
|
+
|
|
169
|
+
:param row_index: the zero-based row being painted
|
|
170
|
+
:param row_count: the total number of rows in the wordmark block
|
|
171
|
+
:param primary_hex: the colour of the first row (e.g. the accent)
|
|
172
|
+
:param secondary_hex: the colour of the last row (e.g. the secondary
|
|
173
|
+
accent)
|
|
174
|
+
:returns: a ``#rrggbb`` hex string (or a raw input hex when one is
|
|
175
|
+
unparseable)
|
|
176
|
+
"""
|
|
177
|
+
start = _parse_hex(primary_hex)
|
|
178
|
+
end = _parse_hex(secondary_hex)
|
|
179
|
+
if start is None:
|
|
180
|
+
return secondary_hex
|
|
181
|
+
if end is None:
|
|
182
|
+
return primary_hex
|
|
183
|
+
if row_index <= 0 or row_count <= 1:
|
|
184
|
+
return _to_hex(start)
|
|
185
|
+
if row_index >= row_count - 1:
|
|
186
|
+
return _to_hex(end)
|
|
187
|
+
t = row_index / (row_count - 1)
|
|
188
|
+
return _to_hex(_lerp_rgb(start, end, t))
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Emblem — the two-tone block monogram that sits to the left of the wordmark.
|
|
2
|
+
|
|
3
|
+
Port of TS ``src/console/components/Emblem.tsx``. Before this, the masthead
|
|
4
|
+
was lettering alone — the "INDUS CODE" wordmark *was* the whole identity,
|
|
5
|
+
painted in one flat accent. The emblem gives the brand a *mark*: a small
|
|
6
|
+
fixed-width block glyph built from the box quadrant family
|
|
7
|
+
(``▛ ▜ ▙ ▟ ▝ ▘ ▗ ▖ █ ▓``), split per row into a bright "fill" span and a dim
|
|
8
|
+
"shadow" span so it reads as a single solid shape lit from one corner. The
|
|
9
|
+
fill is the primary accent; the shadow is the secondary accent — the two
|
|
10
|
+
distinct palette hues the scheme already carries — so the mark is genuinely
|
|
11
|
+
two-tone rather than a single colour with a darker edge.
|
|
12
|
+
|
|
13
|
+
It is purely presentational: a fixed glyph grid tinted through the framework
|
|
14
|
+
:class:`~indusagi.react_ink.ThemeAdapter` at render time, no state beyond the
|
|
15
|
+
reactive inputs, no side effects, no props beyond the theme and an optional
|
|
16
|
+
row-colour override the banner uses to fold the emblem's fill into a frozen
|
|
17
|
+
colour-sweep.
|
|
18
|
+
|
|
19
|
+
Port shape (analysis 02 §7): the Ink ``Box``-per-row column becomes one
|
|
20
|
+
:class:`~textual.widgets.Static` whose ``render()`` returns a prebuilt
|
|
21
|
+
:class:`rich.text.Text`; the per-row fill/shadow ``<Text>`` spans become Rich
|
|
22
|
+
style spans on that one renderable (``chalk.hex`` → ``rich.style.Style
|
|
23
|
+
(color=…)``). The pure builders are exported so the banner can lay the emblem
|
|
24
|
+
beside the wordmark row-by-row without mounting this widget.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from typing import TYPE_CHECKING, Final, Sequence
|
|
31
|
+
|
|
32
|
+
from rich.color import ColorParseError
|
|
33
|
+
from rich.console import RenderableType
|
|
34
|
+
from rich.style import Style
|
|
35
|
+
from rich.text import Text
|
|
36
|
+
from textual.reactive import reactive
|
|
37
|
+
from textual.widgets import Static
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from indusagi.react_ink import InkThemeAdapter
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"EMBLEM_HEIGHT",
|
|
44
|
+
"Emblem",
|
|
45
|
+
"build_emblem_text",
|
|
46
|
+
"emblem_row_texts",
|
|
47
|
+
"paint_hex",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class _EmblemRow:
|
|
53
|
+
"""One emblem row, split into the lit "fill" portion and the shadowed
|
|
54
|
+
portion.
|
|
55
|
+
|
|
56
|
+
Every row carries the same total glyph width so the column is
|
|
57
|
+
rectangular; either span may be empty when a row is wholly lit or wholly
|
|
58
|
+
in shadow.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
# The brightly-lit portion of the row, painted in the primary accent.
|
|
62
|
+
fill: str
|
|
63
|
+
# The shadowed portion of the row, painted in the secondary accent.
|
|
64
|
+
shadow: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
#: The emblem glyph grid: a six-row block monogram from the box quadrant
|
|
68
|
+
#: family.
|
|
69
|
+
#:
|
|
70
|
+
#: Read top-to-bottom it is a solid rounded block with a lit upper-left face
|
|
71
|
+
#: and a shadowed lower-right face — the fill/shadow split per row is what
|
|
72
|
+
#: gives the two-tone, lit-from-a-corner look. Five glyphs wide on every row
|
|
73
|
+
#: so the column is rectangular and aligns with the wordmark beside it.
|
|
74
|
+
_EMBLEM_ROWS: Final[tuple[_EmblemRow, ...]] = (
|
|
75
|
+
_EmblemRow(fill="▛▀▀▜", shadow="▖"),
|
|
76
|
+
_EmblemRow(fill="▌█▓", shadow="▝▌"),
|
|
77
|
+
_EmblemRow(fill="▌█▓", shadow=" ▌"),
|
|
78
|
+
_EmblemRow(fill="▌█▓", shadow="▗▌"),
|
|
79
|
+
_EmblemRow(fill="▌█", shadow="▓▟▘"),
|
|
80
|
+
_EmblemRow(fill="▙▄", shadow="▄▟▘"),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
#: How many terminal rows the emblem occupies (its glyph-grid height).
|
|
84
|
+
EMBLEM_HEIGHT: Final[int] = len(_EMBLEM_ROWS)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def paint_hex(hex_color: str, text: str) -> Text:
|
|
88
|
+
"""Paint ``text`` in a raw ``#rrggbb`` hex, falling back to the unpainted
|
|
89
|
+
text if Rich rejects the colour — used for the optional colour-sweep,
|
|
90
|
+
whose per-row hexes do not exist as theme colour-keys.
|
|
91
|
+
|
|
92
|
+
The TS source wrapped ``chalk.hex`` in a try/catch; Rich parses colours
|
|
93
|
+
eagerly in ``Style.__init__`` and raises :class:`ColorParseError` for
|
|
94
|
+
garbage, which degrades to unstyled text exactly like chalk did.
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
return Text(text, style=Style(color=hex_color))
|
|
98
|
+
except ColorParseError:
|
|
99
|
+
return Text(text)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def emblem_row_texts(
|
|
103
|
+
theme: InkThemeAdapter,
|
|
104
|
+
row_colors: Sequence[str] | None = None,
|
|
105
|
+
) -> tuple[Text, ...]:
|
|
106
|
+
"""The emblem as one prebuilt :class:`rich.text.Text` per glyph row.
|
|
107
|
+
|
|
108
|
+
Each row paints its fill span in the accent role (or the supplied sweep
|
|
109
|
+
colour for that row) and its shadow span in the secondary
|
|
110
|
+
``customMessage`` role, so the monogram reads as one solid mark with a
|
|
111
|
+
lit and a shadowed face. The banner consumes these to lay the emblem
|
|
112
|
+
beside the wordmark row-by-row.
|
|
113
|
+
|
|
114
|
+
:param theme: the framework adapter that turns roles into colours
|
|
115
|
+
:param row_colors: optional per-row fill colour (a ``#rrggbb`` hex),
|
|
116
|
+
indexed by row — the banner's frozen colour-sweep. Absent rows fall
|
|
117
|
+
back to the accent role; the shadow keeps the secondary accent so
|
|
118
|
+
the mark stays two-tone.
|
|
119
|
+
"""
|
|
120
|
+
rows: list[Text] = []
|
|
121
|
+
for index, row in enumerate(_EMBLEM_ROWS):
|
|
122
|
+
sweep = row_colors[index] if row_colors is not None and index < len(row_colors) else None
|
|
123
|
+
fill = paint_hex(sweep, row.fill) if sweep is not None else theme.color("accent", row.fill)
|
|
124
|
+
line = Text()
|
|
125
|
+
line.append_text(fill)
|
|
126
|
+
line.append_text(theme.color("customMessage", row.shadow))
|
|
127
|
+
rows.append(line)
|
|
128
|
+
return tuple(rows)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def build_emblem_text(
|
|
132
|
+
theme: InkThemeAdapter,
|
|
133
|
+
row_colors: Sequence[str] | None = None,
|
|
134
|
+
) -> Text:
|
|
135
|
+
"""The whole two-tone emblem as a single prebuilt Rich ``Text``.
|
|
136
|
+
|
|
137
|
+
The Ink column-of-rows becomes the rows of :func:`emblem_row_texts`
|
|
138
|
+
joined by newlines — what the :class:`Emblem` widget renders.
|
|
139
|
+
"""
|
|
140
|
+
return Text("\n").join(emblem_row_texts(theme, row_colors))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class Emblem(Static):
|
|
144
|
+
"""The two-tone block emblem as one persistent ``Static``.
|
|
145
|
+
|
|
146
|
+
``theme`` and ``row_colors`` are reactive, so a scheme switch (or the
|
|
147
|
+
banner toggling the sweep) re-renders the mark in place; the glyph grid
|
|
148
|
+
itself never changes.
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
DEFAULT_CSS = """
|
|
152
|
+
Emblem {
|
|
153
|
+
width: auto;
|
|
154
|
+
height: auto;
|
|
155
|
+
}
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
theme: reactive[InkThemeAdapter | None] = reactive(None, layout=True)
|
|
159
|
+
row_colors: reactive[tuple[str, ...] | None] = reactive(None, layout=True)
|
|
160
|
+
|
|
161
|
+
def __init__(
|
|
162
|
+
self,
|
|
163
|
+
theme: InkThemeAdapter,
|
|
164
|
+
*,
|
|
165
|
+
row_colors: Sequence[str] | None = None,
|
|
166
|
+
name: str | None = None,
|
|
167
|
+
id: str | None = None,
|
|
168
|
+
classes: str | None = None,
|
|
169
|
+
disabled: bool = False,
|
|
170
|
+
) -> None:
|
|
171
|
+
super().__init__("", name=name, id=id, classes=classes, disabled=disabled)
|
|
172
|
+
self.set_reactive(Emblem.theme, theme)
|
|
173
|
+
self.set_reactive(
|
|
174
|
+
Emblem.row_colors, tuple(row_colors) if row_colors is not None else None
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def render(self) -> RenderableType:
|
|
178
|
+
theme = self.theme
|
|
179
|
+
if theme is None:
|
|
180
|
+
return Text("")
|
|
181
|
+
return build_emblem_text(theme, self.row_colors)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""StatusBar — the bottom chrome strip of the console.
|
|
2
|
+
|
|
3
|
+
Port of TS ``src/console/components/StatusBar.tsx``. Composes the framework's
|
|
4
|
+
:class:`~indusagi.react_ink.StatusLine` (the transient toast row) above its
|
|
5
|
+
:class:`~indusagi.react_ink.Footer` (the persistent session/usage strip),
|
|
6
|
+
both fed from a projected :class:`~indusagi.react_ink.SessionSnapshot`. The
|
|
7
|
+
console owns no rendering of its own here — it threads the resolved theme
|
|
8
|
+
adapter and the current snapshot/status into the framework widgets, which do
|
|
9
|
+
the layout. Purely presentational.
|
|
10
|
+
|
|
11
|
+
Port shape (analysis 02 §7 — "mostly collapses"): the Ink column ``<Box>``
|
|
12
|
+
becomes a :class:`~textual.containers.Vertical` holding the two framework
|
|
13
|
+
widgets, which the ``ConsoleApp`` docks at the bottom. Where TS re-rendered
|
|
14
|
+
with fresh props every frame, the Textual widgets are retained and reactive:
|
|
15
|
+
:meth:`StatusBar.update_state` threads one TS-shaped prop set (snapshot /
|
|
16
|
+
branch / provider count / status) into the children's reactives exactly as
|
|
17
|
+
the framework expects (``StatusLine.snapshot``/``.status``;
|
|
18
|
+
``Footer.snapshot``/``.branch``/``.available_provider_count``).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
from textual.containers import Vertical
|
|
26
|
+
|
|
27
|
+
from indusagi.react_ink import Footer, StatusLine
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from indusagi.react_ink import InkThemeAdapter, SessionSnapshot, StatusMessage
|
|
31
|
+
|
|
32
|
+
__all__ = ["StatusBar"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class StatusBar(Vertical):
|
|
36
|
+
"""The toast row above the footer strip.
|
|
37
|
+
|
|
38
|
+
Constructor inputs mirror the TS props (``theme`` / ``snapshot`` /
|
|
39
|
+
``branch`` / ``provider_count`` / ``status``); the children are created
|
|
40
|
+
eagerly and exposed as :attr:`status_line` / :attr:`footer` so the host
|
|
41
|
+
app can also reach them directly. Both framework widgets hide themselves
|
|
42
|
+
until they have something to show, so an empty bar takes no rows.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
DEFAULT_CSS = """
|
|
46
|
+
StatusBar {
|
|
47
|
+
width: 1fr;
|
|
48
|
+
height: auto;
|
|
49
|
+
}
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
theme: InkThemeAdapter,
|
|
55
|
+
*,
|
|
56
|
+
snapshot: SessionSnapshot | None = None,
|
|
57
|
+
branch: str | None = None,
|
|
58
|
+
provider_count: int = 0,
|
|
59
|
+
status: StatusMessage | None = None,
|
|
60
|
+
name: str | None = None,
|
|
61
|
+
id: str | None = None,
|
|
62
|
+
classes: str | None = None,
|
|
63
|
+
disabled: bool = False,
|
|
64
|
+
) -> None:
|
|
65
|
+
# The framework strips, threaded exactly the props the TS StatusBar
|
|
66
|
+
# forwarded: StatusLine gets snapshot + status, Footer gets snapshot
|
|
67
|
+
# + branch + availableProviderCount.
|
|
68
|
+
self.status_line = StatusLine(theme, snapshot=snapshot, status=status)
|
|
69
|
+
self.footer = Footer(
|
|
70
|
+
theme,
|
|
71
|
+
snapshot=snapshot,
|
|
72
|
+
branch=branch,
|
|
73
|
+
available_provider_count=provider_count,
|
|
74
|
+
)
|
|
75
|
+
super().__init__(
|
|
76
|
+
self.status_line,
|
|
77
|
+
self.footer,
|
|
78
|
+
name=name,
|
|
79
|
+
id=id,
|
|
80
|
+
classes=classes,
|
|
81
|
+
disabled=disabled,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def update_state(
|
|
85
|
+
self,
|
|
86
|
+
snapshot: SessionSnapshot | None,
|
|
87
|
+
*,
|
|
88
|
+
branch: str | None = None,
|
|
89
|
+
provider_count: int = 0,
|
|
90
|
+
status: StatusMessage | None = None,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Thread a fresh TS-shaped prop set into the framework widgets.
|
|
93
|
+
|
|
94
|
+
The console calls this once per projected snapshot (an agent event,
|
|
95
|
+
a toast change, a branch move); the children's reactives re-render
|
|
96
|
+
the strips in place.
|
|
97
|
+
"""
|
|
98
|
+
self.status_line.snapshot = snapshot
|
|
99
|
+
self.status_line.status = status
|
|
100
|
+
self.footer.snapshot = snapshot
|
|
101
|
+
self.footer.branch = branch
|
|
102
|
+
self.footer.available_provider_count = provider_count
|