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,62 @@
|
|
|
1
|
+
"""Console chrome widgets (M5 wave 2) — the masthead and bottom strip.
|
|
2
|
+
|
|
3
|
+
Port of TS ``src/console/components`` (minus the root surface, which lands
|
|
4
|
+
with the wave-3 ``ConsoleApp``). Everything here is purely presentational:
|
|
5
|
+
Static/container widgets rendering prebuilt Rich ``Text`` through the
|
|
6
|
+
framework :class:`~indusagi.react_ink.ThemeAdapter`, plus the pure
|
|
7
|
+
colour-sweep / welcome-line maths they share with the tests.
|
|
8
|
+
|
|
9
|
+
- :mod:`.banner` — :class:`Banner`, the masthead: block-figlet "INDUS CODE"
|
|
10
|
+
wordmark beside the two-tone emblem, brand/version + welcome lines, the
|
|
11
|
+
bordered Session and Startup Map panels, the changelog block (rendered via
|
|
12
|
+
the framework's ``build_changelog_text``), quiet/compact condensed modes,
|
|
13
|
+
and the optional *frozen* colour-sweep (static gradient — no animation
|
|
14
|
+
loop, reduced-motion safe by construction).
|
|
15
|
+
- :mod:`.emblem` — :class:`Emblem`, the two-tone block monogram (box-quadrant
|
|
16
|
+
fill/shadow rows; ``chalk.hex`` spans become ``rich.style.Style`` colours).
|
|
17
|
+
- :mod:`.banner_sweep` — the pure helpers: :func:`welcome_line` and the
|
|
18
|
+
:func:`row_gradient` hex-lerp, ported verbatim.
|
|
19
|
+
- :mod:`.status_bar` — :class:`StatusBar`, the thin Vertical composing the
|
|
20
|
+
framework ``StatusLine`` + ``Footer`` strips.
|
|
21
|
+
|
|
22
|
+
Deleted on port — ``Composer.tsx`` (analysis 02 §7): the TS prompt row with
|
|
23
|
+
its software caret and inline suggestion window is NOT ported. The framework
|
|
24
|
+
``PromptEditor`` (with the console's ``input`` autocomplete providers feeding
|
|
25
|
+
its suggestion overlay) replaces the whole file; the wave-3 ``ConsoleApp``
|
|
26
|
+
wires it. Nothing in this package renders an input row.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from .banner import (
|
|
30
|
+
BRAND,
|
|
31
|
+
WORDMARK_ROWS,
|
|
32
|
+
Banner,
|
|
33
|
+
build_banner_text,
|
|
34
|
+
emblem_sweep_colors,
|
|
35
|
+
wordmark_lines,
|
|
36
|
+
)
|
|
37
|
+
from .banner_sweep import row_gradient, welcome_line
|
|
38
|
+
from .emblem import (
|
|
39
|
+
EMBLEM_HEIGHT,
|
|
40
|
+
Emblem,
|
|
41
|
+
build_emblem_text,
|
|
42
|
+
emblem_row_texts,
|
|
43
|
+
paint_hex,
|
|
44
|
+
)
|
|
45
|
+
from .status_bar import StatusBar
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"BRAND",
|
|
49
|
+
"Banner",
|
|
50
|
+
"EMBLEM_HEIGHT",
|
|
51
|
+
"Emblem",
|
|
52
|
+
"StatusBar",
|
|
53
|
+
"WORDMARK_ROWS",
|
|
54
|
+
"build_banner_text",
|
|
55
|
+
"build_emblem_text",
|
|
56
|
+
"emblem_row_texts",
|
|
57
|
+
"emblem_sweep_colors",
|
|
58
|
+
"paint_hex",
|
|
59
|
+
"row_gradient",
|
|
60
|
+
"welcome_line",
|
|
61
|
+
"wordmark_lines",
|
|
62
|
+
]
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""Banner — the masthead the console renders above the transcript.
|
|
2
|
+
|
|
3
|
+
Port of TS ``src/console/components/Banner.tsx``. The startup chrome the
|
|
4
|
+
surface mounts once at the top of a session: an original block-letter
|
|
5
|
+
wordmark rendered in the box-drawing palette and tinted with the accent
|
|
6
|
+
role, a version + brand line beneath it, the personalized welcome line, a
|
|
7
|
+
compact bordered "Session" panel of at-a-glance facts (the bound model id and
|
|
8
|
+
the working directory), the bordered "Startup Map" of gathered resources,
|
|
9
|
+
and the "What is new" changelog block on a version bump. It reads nothing
|
|
10
|
+
but its inputs, holds no state beyond them, and runs no effects — purely
|
|
11
|
+
presentational, themed through the framework
|
|
12
|
+
:class:`~indusagi.react_ink.ThemeAdapter`.
|
|
13
|
+
|
|
14
|
+
The wordmark is the "INDUS CODE" brand masthead rendered in the ANSI-Shadow
|
|
15
|
+
block-figlet style from the ``█ ║ ═ ╔ ╗ ╚ ╝`` box-drawing family — the same
|
|
16
|
+
masthead the shipped console shows, so both surfaces share one identity. The
|
|
17
|
+
glyph rows are plain data tinted with the accent role at render time.
|
|
18
|
+
|
|
19
|
+
Port shape (analysis 02 §7): the Ink component tree (``Wordmark`` /
|
|
20
|
+
``WelcomeLine`` / ``SessionFact`` / ``StartupNotices`` / ``StartupMapPanel``
|
|
21
|
+
/ ``StartupChangelogBlock``) becomes pure builders assembling one
|
|
22
|
+
:class:`rich.text.Text`, rendered by a single :class:`~textual.widgets
|
|
23
|
+
.Static`. Ink's round-bordered boxes become hand-drawn ``╭─╮│╰─╯`` frames in
|
|
24
|
+
the accent tone; the changelog block reuses the framework's
|
|
25
|
+
``build_changelog_text`` (the exact renderer behind the framework
|
|
26
|
+
``ChangelogBlock`` widget); ``chalk.hex`` becomes ``rich.style.Style
|
|
27
|
+
(color=…)`` via :func:`~.emblem.paint_hex`.
|
|
28
|
+
|
|
29
|
+
The optional colour-sweep is STATIC, exactly as in TS: :func:`row_gradient`
|
|
30
|
+
takes no clock and the gradient is laid down in the one render pass — no
|
|
31
|
+
animation loop exists to suppress, which is what makes it reduced-motion
|
|
32
|
+
safe. The caller resolves the on/off decision (reduced-motion / non-TTY);
|
|
33
|
+
``sweep`` here is simply that resolved flag.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from typing import TYPE_CHECKING, Final, Literal, Sequence
|
|
39
|
+
|
|
40
|
+
from rich.cells import cell_len
|
|
41
|
+
from rich.console import RenderableType
|
|
42
|
+
from rich.text import Text
|
|
43
|
+
from textual.reactive import reactive
|
|
44
|
+
from textual.widgets import Static
|
|
45
|
+
|
|
46
|
+
from indusagi.react_ink import build_changelog_text
|
|
47
|
+
|
|
48
|
+
from ..startup import StartupChangelog, StartupMap, StartupNotice
|
|
49
|
+
from .banner_sweep import row_gradient, welcome_line
|
|
50
|
+
from .emblem import EMBLEM_HEIGHT, emblem_row_texts, paint_hex
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
from indusagi.react_ink import InkThemeAdapter
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"BRAND",
|
|
57
|
+
"Banner",
|
|
58
|
+
"WORDMARK_ROWS",
|
|
59
|
+
"build_banner_text",
|
|
60
|
+
"emblem_sweep_colors",
|
|
61
|
+
"wordmark_lines",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
#: The human-readable product label shown on the version line.
|
|
66
|
+
#:
|
|
67
|
+
#: parity: kept verbatim from the TS Banner, which hardcodes its own masthead
|
|
68
|
+
#: label rather than reading the workspace brand record.
|
|
69
|
+
BRAND: Final[str] = "indus console"
|
|
70
|
+
|
|
71
|
+
#: The compact emblem glyph drawn inline on the condensed one-liner header.
|
|
72
|
+
_COMPACT_EMBLEM: Final[str] = "▛▜▙▟"
|
|
73
|
+
|
|
74
|
+
#: The block-letter brand wordmark, one string per terminal row.
|
|
75
|
+
#:
|
|
76
|
+
#: The product name "INDUS CODE" laid out in the ANSI-Shadow block-figlet
|
|
77
|
+
#: style from the ``█ ║ ═ ╔ ╗ ╚ ╝`` box-drawing family. Every row is the same
|
|
78
|
+
#: display width so the capitals stack into a clean masthead; the render
|
|
79
|
+
#: tints the whole block with the accent role.
|
|
80
|
+
WORDMARK_ROWS: Final[tuple[str, ...]] = (
|
|
81
|
+
"██╗███╗ ██╗██████╗ ██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ ███████╗",
|
|
82
|
+
"██║████╗ ██║██╔══██╗██║ ██║██╔════╝ ██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
83
|
+
"██║██╔██╗ ██║██║ ██║██║ ██║███████╗ ██║ ██║ ██║██║ ██║█████╗ ",
|
|
84
|
+
"██║██║╚██╗██║██║ ██║██║ ██║╚════██║ ██║ ██║ ██║██║ ██║██╔══╝ ",
|
|
85
|
+
"██║██║ ╚████║██████╔╝╚██████╔╝███████║ ╚██████╗╚██████╔╝██████╔╝███████╗",
|
|
86
|
+
"╚═╝╚═╝ ╚═══╝╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
#: The sweep-endpoint fallbacks the TS source used when a scheme leaves the
|
|
90
|
+
#: accent / secondary-accent keys unset.
|
|
91
|
+
_PRIMARY_FALLBACK: Final[str] = "#3ad6b4"
|
|
92
|
+
_SECONDARY_FALLBACK: Final[str] = "#f0a24b"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# Sweep endpoints
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _sweep_endpoints(theme: InkThemeAdapter) -> tuple[str, str]:
|
|
101
|
+
"""The primary→secondary hex pair the frozen gradient lerps between."""
|
|
102
|
+
primary = theme.colors.get("accent") or _PRIMARY_FALLBACK
|
|
103
|
+
secondary = theme.colors.get("customMessage") or theme.colors.get("accent") or _SECONDARY_FALLBACK
|
|
104
|
+
return primary, secondary
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def emblem_sweep_colors(theme: InkThemeAdapter, sweep: bool) -> tuple[str, ...] | None:
|
|
108
|
+
"""The emblem-glyph computed gradient row colours for the colour-sweep,
|
|
109
|
+
or ``None`` when the sweep is off (the emblem keeps its flat accent
|
|
110
|
+
fill).
|
|
111
|
+
|
|
112
|
+
The emblem is :data:`~.emblem.EMBLEM_HEIGHT` rows tall; each is coloured
|
|
113
|
+
over the same primary→secondary lerp so the mark and the wordmark share
|
|
114
|
+
one gradient.
|
|
115
|
+
"""
|
|
116
|
+
if not sweep:
|
|
117
|
+
return None
|
|
118
|
+
primary, secondary = _sweep_endpoints(theme)
|
|
119
|
+
return tuple(
|
|
120
|
+
row_gradient(i, EMBLEM_HEIGHT, primary, secondary) for i in range(EMBLEM_HEIGHT)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Pure piece builders (the TS sub-components, as Text producers)
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def wordmark_lines(theme: InkThemeAdapter, sweep: bool = False) -> tuple[Text, ...]:
|
|
130
|
+
"""The block-letter wordmark, optionally tinted with the frozen
|
|
131
|
+
colour-sweep — one ``Text`` per glyph row.
|
|
132
|
+
|
|
133
|
+
With ``sweep`` off (the default) every row is the flat accent role. With
|
|
134
|
+
it on, row ``i`` is painted with :func:`row_gradient`'s frozen lerp
|
|
135
|
+
between the accent (primary) and ``customMessage`` (secondary) palette
|
|
136
|
+
hues — a single render pass, no timer.
|
|
137
|
+
"""
|
|
138
|
+
rows = len(WORDMARK_ROWS)
|
|
139
|
+
primary, secondary = _sweep_endpoints(theme)
|
|
140
|
+
out: list[Text] = []
|
|
141
|
+
for index, line in enumerate(WORDMARK_ROWS):
|
|
142
|
+
if sweep:
|
|
143
|
+
out.append(paint_hex(row_gradient(index, rows, primary, secondary), line))
|
|
144
|
+
else:
|
|
145
|
+
out.append(theme.color("accent", line))
|
|
146
|
+
return tuple(out)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _notice_lines(theme: InkThemeAdapter, notices: Sequence[StartupNotice]) -> list[Text]:
|
|
150
|
+
"""The notices region: one themed line per gathered startup notice, drawn
|
|
151
|
+
above the wordmark, plus the trailing blank (Ink ``marginBottom={1}``).
|
|
152
|
+
Empty when there are no notices."""
|
|
153
|
+
if len(notices) == 0:
|
|
154
|
+
return []
|
|
155
|
+
lines: list[Text] = []
|
|
156
|
+
for notice in notices:
|
|
157
|
+
if notice.kind == "error":
|
|
158
|
+
lines.append(theme.color("error", notice.text))
|
|
159
|
+
elif notice.kind == "warning":
|
|
160
|
+
lines.append(theme.color("warning", notice.text))
|
|
161
|
+
else:
|
|
162
|
+
lines.append(theme.muted(notice.text))
|
|
163
|
+
lines.append(Text(""))
|
|
164
|
+
return lines
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _welcome_text(
|
|
168
|
+
theme: InkThemeAdapter,
|
|
169
|
+
name: str | None,
|
|
170
|
+
tone: Literal["accent", "muted"] = "accent",
|
|
171
|
+
) -> Text:
|
|
172
|
+
"""The personalized welcome line, themed in the given tone with the name
|
|
173
|
+
in bold."""
|
|
174
|
+
|
|
175
|
+
def paint(text: str) -> Text:
|
|
176
|
+
return theme.muted(text) if tone == "muted" else theme.color("accent", text)
|
|
177
|
+
|
|
178
|
+
line = welcome_line(name)
|
|
179
|
+
# Split out the name so it can be bolded between the framing words; the
|
|
180
|
+
# formatter guarantees the "Welcome back, {name}!" / "Welcome back!"
|
|
181
|
+
# shapes.
|
|
182
|
+
if name is not None and line != "Welcome back!":
|
|
183
|
+
out = Text()
|
|
184
|
+
out.append_text(paint("Welcome back, "))
|
|
185
|
+
out.append(name.strip(), style="bold")
|
|
186
|
+
out.append_text(paint("!"))
|
|
187
|
+
return out
|
|
188
|
+
return paint(line)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _space_between(left: Text, right: Text, width: int) -> Text:
|
|
192
|
+
"""Lay ``left`` and ``right`` on one row with the slack between them —
|
|
193
|
+
Ink's ``justifyContent="space-between"`` for a fixed-width panel row."""
|
|
194
|
+
gap = max(1, width - left.cell_len - right.cell_len)
|
|
195
|
+
out = Text()
|
|
196
|
+
out.append_text(left)
|
|
197
|
+
out.append(" " * gap)
|
|
198
|
+
out.append_text(right)
|
|
199
|
+
return out
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _panel_lines(
|
|
203
|
+
theme: InkThemeAdapter,
|
|
204
|
+
content: Sequence[Text],
|
|
205
|
+
columns: int,
|
|
206
|
+
) -> list[Text]:
|
|
207
|
+
"""Frame ``content`` rows in a round accent border with one cell of
|
|
208
|
+
horizontal padding — Ink's ``borderStyle="round"`` ``borderColor=accent``
|
|
209
|
+
``paddingX={1}`` box, drawn by hand into text rows."""
|
|
210
|
+
inner = max(10, columns - 4)
|
|
211
|
+
border = theme.color_style("accent")
|
|
212
|
+
lines: list[Text] = [Text("╭" + "─" * (inner + 2) + "╮", style=border)]
|
|
213
|
+
for row in content:
|
|
214
|
+
clipped = row.copy()
|
|
215
|
+
clipped.truncate(inner, overflow="ellipsis", pad=True)
|
|
216
|
+
line = Text()
|
|
217
|
+
line.append("│ ", style=border)
|
|
218
|
+
line.append_text(clipped)
|
|
219
|
+
line.append(" │", style=border)
|
|
220
|
+
lines.append(line)
|
|
221
|
+
lines.append(Text("╰" + "─" * (inner + 2) + "╯", style=border))
|
|
222
|
+
return lines
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _session_fact(theme: InkThemeAdapter, label: str, value: str) -> Text:
|
|
226
|
+
"""One labelled row inside the Session panel: a dim caption and its
|
|
227
|
+
value."""
|
|
228
|
+
out = Text()
|
|
229
|
+
out.append_text(theme.dim(f"{label:<7} "))
|
|
230
|
+
out.append_text(theme.muted(value))
|
|
231
|
+
return out
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _session_panel_lines(
|
|
235
|
+
theme: InkThemeAdapter,
|
|
236
|
+
model_id: str,
|
|
237
|
+
workspace: str,
|
|
238
|
+
verbose: bool,
|
|
239
|
+
columns: int,
|
|
240
|
+
) -> list[Text]:
|
|
241
|
+
"""The bordered Session panel: the at-a-glance facts of the session."""
|
|
242
|
+
rows: list[Text] = [
|
|
243
|
+
theme.color("accent", "Session"),
|
|
244
|
+
_session_fact(theme, "model", model_id),
|
|
245
|
+
_session_fact(theme, "cwd", workspace),
|
|
246
|
+
]
|
|
247
|
+
if verbose:
|
|
248
|
+
rows.append(_session_fact(theme, "mode", "verbose diagnostics on"))
|
|
249
|
+
return _panel_lines(theme, rows, columns)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _startup_map_lines(
|
|
253
|
+
theme: InkThemeAdapter,
|
|
254
|
+
startup: StartupMap,
|
|
255
|
+
columns: int,
|
|
256
|
+
) -> list[Text]:
|
|
257
|
+
"""The Startup Map panel: a bordered list of the gathered session
|
|
258
|
+
resources, one titled group per section with its entry lines indented
|
|
259
|
+
beneath it. Empty when the map has no sections."""
|
|
260
|
+
if len(startup.sections) == 0:
|
|
261
|
+
return []
|
|
262
|
+
inner = max(10, columns - 4)
|
|
263
|
+
total = sum(section.count for section in startup.sections)
|
|
264
|
+
plural = "" if total == 1 else "s"
|
|
265
|
+
rows: list[Text] = [
|
|
266
|
+
_space_between(
|
|
267
|
+
theme.color("accent", "Startup Map"),
|
|
268
|
+
theme.dim(f"{total} item{plural}"),
|
|
269
|
+
inner,
|
|
270
|
+
)
|
|
271
|
+
]
|
|
272
|
+
for section in startup.sections:
|
|
273
|
+
rows.append(Text("")) # Ink marginTop={1} per section group.
|
|
274
|
+
rows.append(
|
|
275
|
+
_space_between(theme.muted(section.title), theme.dim(str(section.count)), inner)
|
|
276
|
+
)
|
|
277
|
+
for line in section.lines:
|
|
278
|
+
entry = Text(" ") # Ink paddingLeft={2}.
|
|
279
|
+
entry.append_text(theme.dim("› "))
|
|
280
|
+
entry.append_text(theme.muted(line))
|
|
281
|
+
rows.append(entry)
|
|
282
|
+
return _panel_lines(theme, rows, columns)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _changelog_lines(
|
|
286
|
+
theme: InkThemeAdapter,
|
|
287
|
+
changelog: StartupChangelog,
|
|
288
|
+
columns: int,
|
|
289
|
+
) -> list[Text]:
|
|
290
|
+
"""The changelog region. On a full survey it renders the framework
|
|
291
|
+
changelog block (the exact ``ChangelogBlock`` renderable); on a condensed
|
|
292
|
+
survey a single "updated to" line preceded by its ``marginTop`` blank;
|
|
293
|
+
otherwise nothing."""
|
|
294
|
+
if changelog.mode == "full" and changelog.markdown is not None:
|
|
295
|
+
return [build_changelog_text("What is new", changelog.markdown, theme, columns)]
|
|
296
|
+
if changelog.mode == "condensed":
|
|
297
|
+
line = Text()
|
|
298
|
+
line.append_text(theme.muted(f"Updated to v{changelog.version}. Use "))
|
|
299
|
+
line.append_text(theme.color("accent", "/changelog"))
|
|
300
|
+
line.append_text(theme.muted(" to view the full changelog."))
|
|
301
|
+
return [Text(""), line]
|
|
302
|
+
return []
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# ---------------------------------------------------------------------------
|
|
306
|
+
# The assembled masthead
|
|
307
|
+
# ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def build_banner_text(
|
|
311
|
+
theme: InkThemeAdapter,
|
|
312
|
+
*,
|
|
313
|
+
model_id: str,
|
|
314
|
+
workspace: str,
|
|
315
|
+
version: str,
|
|
316
|
+
verbose: bool = False,
|
|
317
|
+
quiet: bool = False,
|
|
318
|
+
compact: bool = False,
|
|
319
|
+
account: str | None = None,
|
|
320
|
+
sweep: bool = False,
|
|
321
|
+
startup: StartupMap | None = None,
|
|
322
|
+
notices: Sequence[StartupNotice] | None = None,
|
|
323
|
+
changelog: StartupChangelog | None = None,
|
|
324
|
+
columns: int = 80,
|
|
325
|
+
) -> Text:
|
|
326
|
+
"""Assemble the console masthead as one prebuilt Rich ``Text``.
|
|
327
|
+
|
|
328
|
+
In the default (loud) mode this is the two-tone emblem beside the
|
|
329
|
+
block-letter wordmark, the brand / version line, the personalized welcome
|
|
330
|
+
line, the optional notices region, the bordered Session and Startup Map
|
|
331
|
+
panels, and the changelog block. The ``quiet`` or ``compact`` flags
|
|
332
|
+
collapse all of that to a single compact header line (emblem glyph +
|
|
333
|
+
brand + version + model), still carrying the welcome line, the notices,
|
|
334
|
+
and the changelog so nothing important is silently dropped — ``compact``
|
|
335
|
+
keeps the inline emblem glyph (the repeat-launch presentation), ``quiet``
|
|
336
|
+
is the legacy strip.
|
|
337
|
+
|
|
338
|
+
:param theme: the framework adapter that turns roles into colours
|
|
339
|
+
:param model_id: canonical id of the model bound to the session
|
|
340
|
+
:param workspace: working directory the session is scoped to
|
|
341
|
+
:param version: the product version shown on the brand line
|
|
342
|
+
:param verbose: whether to render the extra diagnostics fact
|
|
343
|
+
:param quiet: suppress the big wordmark + panels (manual toggle)
|
|
344
|
+
:param compact: auto-condensed repeat-launch one-liner
|
|
345
|
+
:param account: the signed-in account label for the welcome line (the TS
|
|
346
|
+
``name`` prop; renamed — ``name`` is the Textual widget identity)
|
|
347
|
+
:param sweep: the resolved static colour-sweep on/off decision
|
|
348
|
+
:param startup: the gathered session resources (the Startup Map panel)
|
|
349
|
+
:param notices: out-of-band lines drawn above the wordmark
|
|
350
|
+
:param changelog: the changelog survey shown on a version bump
|
|
351
|
+
:param columns: the laid-out width panels and dividers size against
|
|
352
|
+
"""
|
|
353
|
+
notice_list = tuple(notices) if notices is not None else ()
|
|
354
|
+
lines: list[Text] = list(_notice_lines(theme, notice_list))
|
|
355
|
+
|
|
356
|
+
# The condensed one-liner: the manual quiet toggle *or* an auto-condensed
|
|
357
|
+
# repeat launch. Both collapse to the same compact header.
|
|
358
|
+
if quiet or compact:
|
|
359
|
+
header = Text()
|
|
360
|
+
if compact:
|
|
361
|
+
header.append_text(theme.color("accent", _COMPACT_EMBLEM[:2]))
|
|
362
|
+
header.append_text(theme.color("customMessage", _COMPACT_EMBLEM[2:]))
|
|
363
|
+
header.append(" ")
|
|
364
|
+
brand = theme.color("accent", BRAND)
|
|
365
|
+
brand.stylize("bold")
|
|
366
|
+
header.append_text(brand)
|
|
367
|
+
header.append(" ")
|
|
368
|
+
header.append_text(theme.muted(f"v{version}"))
|
|
369
|
+
header.append(" ")
|
|
370
|
+
header.append_text(theme.dim(model_id))
|
|
371
|
+
lines.append(header)
|
|
372
|
+
lines.append(_welcome_text(theme, account, tone="muted"))
|
|
373
|
+
if changelog is not None:
|
|
374
|
+
lines.extend(_changelog_lines(theme, changelog, columns))
|
|
375
|
+
return Text("\n").join(lines)
|
|
376
|
+
|
|
377
|
+
# Emblem beside wordmark: both blocks are the same height, so each
|
|
378
|
+
# terminal row is `emblem row + two spaces + wordmark row` (the Ink row
|
|
379
|
+
# Box of two columns).
|
|
380
|
+
emblem_rows = emblem_row_texts(theme, emblem_sweep_colors(theme, sweep))
|
|
381
|
+
wordmark_rows = wordmark_lines(theme, sweep)
|
|
382
|
+
for emblem_row, wordmark_row in zip(emblem_rows, wordmark_rows):
|
|
383
|
+
row = Text()
|
|
384
|
+
row.append_text(emblem_row)
|
|
385
|
+
row.append(" ")
|
|
386
|
+
row.append_text(wordmark_row)
|
|
387
|
+
lines.append(row)
|
|
388
|
+
|
|
389
|
+
# Brand / version line + welcome line (Ink marginTop={1} block).
|
|
390
|
+
lines.append(Text(""))
|
|
391
|
+
brand_line = Text()
|
|
392
|
+
brand = theme.color("accent", BRAND)
|
|
393
|
+
brand.stylize("bold")
|
|
394
|
+
brand_line.append_text(brand)
|
|
395
|
+
brand_line.append(" ")
|
|
396
|
+
brand_line.append_text(theme.muted(f"v{version}"))
|
|
397
|
+
lines.append(brand_line)
|
|
398
|
+
lines.append(_welcome_text(theme, account, tone="accent"))
|
|
399
|
+
|
|
400
|
+
# Session panel (marginTop={1}).
|
|
401
|
+
lines.append(Text(""))
|
|
402
|
+
lines.extend(_session_panel_lines(theme, model_id, workspace, verbose, columns))
|
|
403
|
+
|
|
404
|
+
# Startup Map panel (marginTop={1}; omitted when the map is empty).
|
|
405
|
+
if startup is not None and len(startup.sections) > 0:
|
|
406
|
+
lines.append(Text(""))
|
|
407
|
+
lines.extend(_startup_map_lines(theme, startup, columns))
|
|
408
|
+
|
|
409
|
+
if changelog is not None:
|
|
410
|
+
lines.extend(_changelog_lines(theme, changelog, columns))
|
|
411
|
+
|
|
412
|
+
return Text("\n").join(lines)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class Banner(Static):
|
|
416
|
+
"""The console masthead as one persistent ``Static``.
|
|
417
|
+
|
|
418
|
+
All inputs are reactive so the surface can re-skin the banner on a scheme
|
|
419
|
+
switch (live theme preview) or late-arriving startup data; the widget
|
|
420
|
+
re-renders its prebuilt ``Text`` in place. The Ink root's
|
|
421
|
+
``marginBottom={1}`` becomes widget margin.
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
DEFAULT_CSS = """
|
|
425
|
+
Banner {
|
|
426
|
+
width: 1fr;
|
|
427
|
+
height: auto;
|
|
428
|
+
}
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
theme: reactive[InkThemeAdapter | None] = reactive(None, layout=True)
|
|
432
|
+
model_id: reactive[str] = reactive("", layout=True)
|
|
433
|
+
workspace: reactive[str] = reactive("", layout=True)
|
|
434
|
+
version: reactive[str] = reactive("", layout=True)
|
|
435
|
+
verbose: reactive[bool] = reactive(False, layout=True)
|
|
436
|
+
quiet: reactive[bool] = reactive(False, layout=True)
|
|
437
|
+
compact: reactive[bool] = reactive(False, layout=True)
|
|
438
|
+
account: reactive[str | None] = reactive(None, layout=True)
|
|
439
|
+
sweep: reactive[bool] = reactive(False, layout=True)
|
|
440
|
+
startup: reactive[StartupMap | None] = reactive(None, layout=True)
|
|
441
|
+
notices: reactive[tuple[StartupNotice, ...]] = reactive((), layout=True)
|
|
442
|
+
changelog: reactive[StartupChangelog | None] = reactive(None, layout=True)
|
|
443
|
+
|
|
444
|
+
def __init__(
|
|
445
|
+
self,
|
|
446
|
+
theme: InkThemeAdapter,
|
|
447
|
+
*,
|
|
448
|
+
model_id: str,
|
|
449
|
+
workspace: str,
|
|
450
|
+
version: str,
|
|
451
|
+
verbose: bool = False,
|
|
452
|
+
quiet: bool = False,
|
|
453
|
+
compact: bool = False,
|
|
454
|
+
account: str | None = None,
|
|
455
|
+
sweep: bool = False,
|
|
456
|
+
startup: StartupMap | None = None,
|
|
457
|
+
notices: Sequence[StartupNotice] | None = None,
|
|
458
|
+
changelog: StartupChangelog | None = None,
|
|
459
|
+
name: str | None = None,
|
|
460
|
+
id: str | None = None,
|
|
461
|
+
classes: str | None = None,
|
|
462
|
+
disabled: bool = False,
|
|
463
|
+
) -> None:
|
|
464
|
+
super().__init__("", name=name, id=id, classes=classes, disabled=disabled)
|
|
465
|
+
self.set_reactive(Banner.theme, theme)
|
|
466
|
+
self.set_reactive(Banner.model_id, model_id)
|
|
467
|
+
self.set_reactive(Banner.workspace, workspace)
|
|
468
|
+
self.set_reactive(Banner.version, version)
|
|
469
|
+
self.set_reactive(Banner.verbose, verbose)
|
|
470
|
+
self.set_reactive(Banner.quiet, quiet)
|
|
471
|
+
self.set_reactive(Banner.compact, compact)
|
|
472
|
+
self.set_reactive(Banner.account, account)
|
|
473
|
+
self.set_reactive(Banner.sweep, sweep)
|
|
474
|
+
self.set_reactive(Banner.startup, startup)
|
|
475
|
+
self.set_reactive(Banner.notices, tuple(notices) if notices is not None else ())
|
|
476
|
+
self.set_reactive(Banner.changelog, changelog)
|
|
477
|
+
# Ink: <Box flexDirection="column" marginBottom={1}>
|
|
478
|
+
self.styles.margin = (0, 0, 1, 0)
|
|
479
|
+
|
|
480
|
+
def render(self) -> RenderableType:
|
|
481
|
+
theme = self.theme
|
|
482
|
+
if theme is None:
|
|
483
|
+
return Text("")
|
|
484
|
+
columns = self.size.width or 80
|
|
485
|
+
return build_banner_text(
|
|
486
|
+
theme,
|
|
487
|
+
model_id=self.model_id,
|
|
488
|
+
workspace=self.workspace,
|
|
489
|
+
version=self.version,
|
|
490
|
+
verbose=self.verbose,
|
|
491
|
+
quiet=self.quiet,
|
|
492
|
+
compact=self.compact,
|
|
493
|
+
account=self.account,
|
|
494
|
+
sweep=self.sweep,
|
|
495
|
+
startup=self.startup,
|
|
496
|
+
notices=self.notices,
|
|
497
|
+
changelog=self.changelog,
|
|
498
|
+
columns=columns,
|
|
499
|
+
)
|