@pellux/goodvibes-agent 1.5.5 → 1.5.6
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.
- package/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/dist/package/main.js +94130 -79335
- package/docs/tools-and-commands.md +2 -1
- package/package.json +6 -3
- package/src/agent/autonomy-schedule-format.ts +1 -1
- package/src/agent/autonomy-schedule.ts +3 -3
- package/src/agent/calendar/calendar-oauth-service.ts +324 -0
- package/src/agent/calendar-subscription-registry.ts +497 -0
- package/src/agent/email/email-service.ts +57 -0
- package/src/agent/email/smtp-client.ts +30 -10
- package/src/agent/memory-prompt.ts +143 -12
- package/src/agent/operator-actions.ts +26 -22
- package/src/agent/prompt-context-receipts.ts +45 -18
- package/src/agent/reminder-schedule-format.ts +1 -1
- package/src/agent/reminder-schedule.ts +3 -3
- package/src/agent/routine-schedule-format.ts +2 -2
- package/src/agent/routine-schedule-promotion.ts +5 -5
- package/src/agent/routine-schedule-receipts.ts +2 -2
- package/src/agent/schedule-edit-format.ts +1 -1
- package/src/agent/schedule-edit.ts +5 -5
- package/src/agent/session-registration.ts +281 -0
- package/src/agent/vibe-file.ts +50 -0
- package/src/cli/local-library-command-shared.ts +146 -0
- package/src/cli/management.ts +2 -1
- package/src/cli/personas-command.ts +252 -0
- package/src/cli/resume-relaunch-notice.ts +151 -0
- package/src/cli/routines-command.ts +25 -2
- package/src/cli/service-posture.ts +3 -2
- package/src/cli/skill-bundle-command.ts +175 -0
- package/src/cli/skills-command.ts +309 -0
- package/src/cli/status.ts +43 -7
- package/src/cli/tui-startup.ts +24 -1
- package/src/config/agent-settings-policy.ts +0 -1
- package/src/config/secret-config.ts +4 -0
- package/src/core/conversation-rendering.ts +20 -11
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/system-message-noise.ts +108 -0
- package/src/core/system-message-router.ts +54 -1
- package/src/core/thinking-overlay.ts +83 -0
- package/src/input/agent-workspace-access-command-editor-submission.ts +75 -108
- package/src/input/agent-workspace-access-command-editors.ts +108 -128
- package/src/input/agent-workspace-activation.ts +15 -0
- package/src/input/agent-workspace-basic-command-editor-submission.ts +292 -513
- package/src/input/agent-workspace-basic-command-editors.ts +433 -564
- package/src/input/agent-workspace-calendar-connect-editor.ts +116 -0
- package/src/input/agent-workspace-calendar-oauth-editor.ts +152 -0
- package/src/input/agent-workspace-calendar-subscribe-editor.ts +128 -0
- package/src/input/agent-workspace-categories.ts +7 -2
- package/src/input/agent-workspace-channel-command-editor-submission.ts +38 -52
- package/src/input/agent-workspace-channel-command-editors.ts +42 -46
- package/src/input/agent-workspace-command-editor-engine.ts +133 -0
- package/src/input/agent-workspace-command-editor.ts +4 -0
- package/src/input/agent-workspace-direct-editor-submission.ts +59 -0
- package/src/input/agent-workspace-email-connect-editor.ts +162 -0
- package/src/input/agent-workspace-knowledge-command-editor-submission.ts +52 -81
- package/src/input/agent-workspace-knowledge-command-editors.ts +71 -71
- package/src/input/agent-workspace-library-command-editor-submission.ts +10 -28
- package/src/input/agent-workspace-library-command-editors.ts +16 -2
- package/src/input/agent-workspace-live-counters.ts +55 -0
- package/src/input/agent-workspace-mcp-command-editor-submission.ts +60 -82
- package/src/input/agent-workspace-mcp-command-editors.ts +52 -0
- package/src/input/agent-workspace-media-command-editor-submission.ts +12 -39
- package/src/input/agent-workspace-media-command-editors.ts +10 -8
- package/src/input/agent-workspace-memory-command-editor-submission.ts +76 -151
- package/src/input/agent-workspace-memory-command-editors.ts +116 -141
- package/src/input/agent-workspace-operations-command-editor-submission.ts +131 -184
- package/src/input/agent-workspace-operations-command-editors.ts +150 -162
- package/src/input/agent-workspace-provider-command-editor-submission.ts +60 -106
- package/src/input/agent-workspace-provider-command-editors.ts +58 -68
- package/src/input/agent-workspace-search.ts +8 -1
- package/src/input/agent-workspace-session-command-editor-submission.ts +104 -132
- package/src/input/agent-workspace-session-command-editors.ts +160 -195
- package/src/input/agent-workspace-settings.ts +40 -1
- package/src/input/agent-workspace-skill-bundle-command-editor-submission.ts +55 -71
- package/src/input/agent-workspace-skill-bundle-command-editors.ts +65 -69
- package/src/input/agent-workspace-snapshot-builders.ts +431 -0
- package/src/input/agent-workspace-snapshot-config.ts +43 -0
- package/src/input/agent-workspace-snapshot.ts +198 -432
- package/src/input/agent-workspace-task-command-editor-submission.ts +34 -44
- package/src/input/agent-workspace-task-command-editors.ts +27 -28
- package/src/input/agent-workspace-types.ts +35 -1
- package/src/input/agent-workspace.ts +5 -14
- package/src/input/command-registry.ts +23 -1
- package/src/input/commands/calendar-connect-runtime.ts +226 -0
- package/src/input/commands/calendar-runtime.ts +110 -7
- package/src/input/commands/calendar-subscription-runtime.ts +225 -0
- package/src/input/commands/email-runtime.ts +100 -40
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/network-scan-runtime.ts +75 -0
- package/src/input/commands/operator-actions-runtime.ts +9 -6
- package/src/input/commands/personas-runtime.ts +1 -1
- package/src/input/commands/schedule-runtime.ts +4 -4
- package/src/input/commands/session-content.ts +13 -1
- package/src/input/commands/session-workflow.ts +15 -20
- package/src/input/commands/session.ts +3 -1
- package/src/input/commands/shell-core.ts +20 -6
- package/src/input/commands.ts +2 -2
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +10 -0
- package/src/input/handler-feed.ts +87 -0
- package/src/input/handler-modal-routes.ts +6 -1
- package/src/input/handler.ts +5 -0
- package/src/input/panel-paste-flood-guard.ts +94 -0
- package/src/input/settings-modal-types.ts +5 -3
- package/src/input/settings-modal.ts +21 -0
- package/src/main.ts +36 -36
- package/src/permissions/approval-posture.ts +141 -0
- package/src/renderer/agent-workspace-context-lines.ts +10 -2
- package/src/renderer/compositor.ts +27 -4
- package/src/renderer/diff.ts +61 -18
- package/src/renderer/fullscreen-primitives.ts +37 -18
- package/src/renderer/markdown.ts +20 -10
- package/src/renderer/modal-factory.ts +25 -15
- package/src/renderer/overlay-box.ts +23 -12
- package/src/renderer/process-indicator.ts +8 -3
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +2 -0
- package/src/renderer/startup-theme-probe.ts +35 -0
- package/src/renderer/status-glyphs.ts +11 -15
- package/src/renderer/system-message.ts +17 -2
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/terminal-bg-probe.ts +373 -0
- package/src/renderer/terminal-escapes.ts +24 -0
- package/src/renderer/theme-mode-config.ts +87 -0
- package/src/renderer/theme.ts +241 -0
- package/src/renderer/thinking.ts +12 -3
- package/src/renderer/tool-call.ts +7 -3
- package/src/renderer/ui-factory.ts +92 -36
- package/src/renderer/ui-primitives.ts +33 -93
- package/src/runtime/bootstrap-core.ts +15 -0
- package/src/runtime/bootstrap-hook-bridge.ts +6 -0
- package/src/runtime/bootstrap-shell.ts +2 -0
- package/src/runtime/bootstrap.ts +68 -5
- package/src/runtime/calendar-boot-refresh.ts +105 -0
- package/src/runtime/lan-scan-consent.ts +253 -0
- package/src/runtime/services.ts +127 -2
- package/src/runtime/session-spine-rest-transport.ts +160 -0
- package/src/runtime/terminal-output-guard.ts +6 -1
- package/src/runtime/ui-services.ts +3 -0
- package/src/shell/agent-workspace-fullscreen.ts +5 -0
- package/src/shell/terminal-focus-mode.ts +120 -0
- package/src/shell/ui-openers.ts +13 -4
- package/src/tools/agent-harness-autonomy-intake.ts +5 -5
- package/src/tools/agent-harness-autonomy-queue.ts +1 -1
- package/src/tools/agent-harness-background-processes.ts +2 -1
- package/src/tools/agent-harness-metadata.ts +16 -8
- package/src/tools/agent-harness-operator-methods.ts +44 -8
- package/src/tools/agent-harness-personal-ops-discovery.ts +57 -10
- package/src/tools/agent-harness-personal-ops-lanes.ts +17 -5
- package/src/tools/agent-harness-personal-ops-operations.ts +7 -7
- package/src/tools/agent-harness-personal-ops-types.ts +6 -0
- package/src/tools/agent-harness-prompt-context.ts +26 -12
- package/src/tools/agent-harness-workspace-actions.ts +4 -0
- package/src/tools/agent-local-registry-args.ts +117 -0
- package/src/tools/agent-local-registry-memory.ts +227 -0
- package/src/tools/agent-local-registry-tool.ts +19 -237
- package/src/tools/agent-operator-briefing-tool.ts +2 -2
- package/src/tools/agent-operator-method-tool.ts +13 -0
- package/src/tools/agent-policy-explanation.ts +39 -4
- package/src/tools/agent-schedule-tool.ts +5 -5
- package/src/utils/terminal-width.ts +98 -1
- package/src/version.ts +1 -1
- package/src/cli/local-library-command.ts +0 -825
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* terminal-escapes — the raw control sequences main.ts writes to enter/leave the
|
|
3
|
+
* TUI's terminal mode (alt screen, mouse, cursor, keyboard-extension, paste, and
|
|
4
|
+
* focus reporting). Extracted as plain constants so callers share one definition
|
|
5
|
+
* and the entry file stays within the source-file line-count gate.
|
|
6
|
+
*
|
|
7
|
+
* FOCUS_ENABLE / FOCUS_DISABLE (DECSET ?1004) are the shared home for the OS
|
|
8
|
+
* window-focus reporting mode consumed by the focus-tracker (W4-R3): R3 imports
|
|
9
|
+
* FOCUS_ENABLE from here rather than redefining it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const ALT_SCREEN_ENTER = '\x1b[?1049h';
|
|
13
|
+
export const ALT_SCREEN_EXIT = '\x1b[?1049l';
|
|
14
|
+
export const MOUSE_ENABLE = '\x1b[?1000h\x1b[?1002h\x1b[?1006h';
|
|
15
|
+
export const MOUSE_DISABLE = '\x1b[?1006l\x1b[?1002l\x1b[?1000l';
|
|
16
|
+
export const CURSOR_HIDE = '\x1b[?25l';
|
|
17
|
+
export const CURSOR_SHOW = '\x1b[?25h';
|
|
18
|
+
export const CLEAR_SCREEN = '\x1b[2J\x1b[3J\x1b[H';
|
|
19
|
+
export const KEYBOARD_EXT_ENABLE = '\x1b[>4;2m' + '\x1b[?1u';
|
|
20
|
+
export const KEYBOARD_EXT_DISABLE = '\x1b[>4;0m' + '\x1b[?1l';
|
|
21
|
+
export const PASTE_ENABLE = '\x1b[?2004h';
|
|
22
|
+
export const PASTE_DISABLE = '\x1b[?2004l';
|
|
23
|
+
export const FOCUS_ENABLE = '\x1b[?1004h';
|
|
24
|
+
export const FOCUS_DISABLE = '\x1b[?1004l';
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* theme-mode-config — the appearance/theme-mode preference surface (W4-R4).
|
|
3
|
+
*
|
|
4
|
+
* The preference lives at the config key `display.themeMode` (auto | dark |
|
|
5
|
+
* light, default auto). It is stored under the existing SDK `display` section
|
|
6
|
+
* (which exists in DEFAULT_CONFIG) as a TUI-local synthetic setting — no SDK
|
|
7
|
+
* schema change — the same pattern the config already uses for other
|
|
8
|
+
* display.* toggles.
|
|
9
|
+
*
|
|
10
|
+
* DIVERGENCE FROM THE W4 BRIEF (recorded): the brief listed theme-mode-config.ts
|
|
11
|
+
* as the NEW home for the config-read helpers, but W4-R2 landed FIRST and
|
|
12
|
+
* inlined resolveConfiguredThemeMode / coerceThemeModeSetting /
|
|
13
|
+
* THEME_MODE_CONFIG_KEY / ThemeModeSetting into terminal-bg-probe.ts (so R2
|
|
14
|
+
* could ship the probe before this file existed). Rather than duplicate those
|
|
15
|
+
* (which would let the two copies drift — the exact failure S1 exists to
|
|
16
|
+
* prevent), this module RE-EXPORTS them from terminal-bg-probe as the single
|
|
17
|
+
* source, and adds only the settings-modal-facing metadata (the value list,
|
|
18
|
+
* default, and human description) that R2 did not need.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately free of terminal/probe state so the settings-modal data layer
|
|
21
|
+
* can import it without pulling in the stateful probe class.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { setActiveThemeMode, type ThemeModeSetting } from './theme.ts';
|
|
25
|
+
import { coerceThemeModeSetting, THEME_MODE_CONFIG_KEY, THEME_MODE_DEFAULT } from './terminal-bg-probe.ts';
|
|
26
|
+
import type { ConfigKey, ConfigSetting } from '@pellux/goodvibes-sdk/platform/config';
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
THEME_MODE_CONFIG_KEY,
|
|
30
|
+
THEME_MODE_DEFAULT,
|
|
31
|
+
coerceThemeModeSetting,
|
|
32
|
+
resolveConfiguredThemeMode,
|
|
33
|
+
} from './terminal-bg-probe.ts';
|
|
34
|
+
|
|
35
|
+
/** The three valid preference values, in settings-cycle order. */
|
|
36
|
+
export const THEME_MODE_VALUES: readonly ThemeModeSetting[] = ['auto', 'dark', 'light'];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Human-facing description for the settings-modal entry. States the honest
|
|
40
|
+
* timing contract: forced modes apply on the next full paint immediately;
|
|
41
|
+
* `auto` only re-probes at startup, so switching TO auto takes effect next
|
|
42
|
+
* launch.
|
|
43
|
+
*/
|
|
44
|
+
export const THEME_MODE_DESCRIPTION =
|
|
45
|
+
'Terminal background theme. auto probes the terminal background colour once at '
|
|
46
|
+
+ 'startup (OSC 11) and picks light or dark; dark/light force a fixed theme. '
|
|
47
|
+
+ 'Forced modes take effect immediately; auto is only evaluated at startup, so '
|
|
48
|
+
+ 'selecting auto takes effect on the next launch. Unreadable/unsupported '
|
|
49
|
+
+ 'terminals fall back to dark. Scope (honest): transcript markdown, tool-call '
|
|
50
|
+
+ 'and system-message accents, and the header/footer/thinking chrome all flip '
|
|
51
|
+
+ 'with this setting; only the background colour itself follows your terminal.';
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The synthetic ConfigSetting descriptor for display.themeMode (the TUI's
|
|
55
|
+
* settings-modal-data.ts pattern). Agent-local — the key is NOT in the SDK
|
|
56
|
+
* ConfigKey union, so it is cast; safe because the `display` section exists in
|
|
57
|
+
* DEFAULT_CONFIG and ConfigManager.setDynamic/get round-trip unknown fields
|
|
58
|
+
* under an existing section. Injected into the settings modal's display group
|
|
59
|
+
* so the preference is discoverable and cycles like any other enum setting.
|
|
60
|
+
*/
|
|
61
|
+
export const THEME_MODE_SYNTHETIC_SETTING: ConfigSetting = {
|
|
62
|
+
key: THEME_MODE_CONFIG_KEY as ConfigKey,
|
|
63
|
+
type: 'enum',
|
|
64
|
+
default: THEME_MODE_DEFAULT,
|
|
65
|
+
enumValues: [...THEME_MODE_VALUES],
|
|
66
|
+
description: THEME_MODE_DESCRIPTION,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Apply a display.themeMode settings change with the honest timing contract:
|
|
71
|
+
* forced dark/light flip the active mode NOW (caller passes its full-repaint
|
|
72
|
+
* hook); auto is only evaluated by the startup probe, so it takes effect on the
|
|
73
|
+
* next launch — the returned message states which happened. Called by the
|
|
74
|
+
* settings-modal onSettingApplied hook (ui-openers).
|
|
75
|
+
*/
|
|
76
|
+
export function applyThemeModeSettingChange(
|
|
77
|
+
value: unknown,
|
|
78
|
+
requestFullRepaint?: () => void,
|
|
79
|
+
): { message: string } {
|
|
80
|
+
const next = coerceThemeModeSetting(value);
|
|
81
|
+
if (next === 'dark' || next === 'light') {
|
|
82
|
+
setActiveThemeMode(next);
|
|
83
|
+
requestFullRepaint?.();
|
|
84
|
+
return { message: `Theme mode: ${next} (applied now)` };
|
|
85
|
+
}
|
|
86
|
+
return { message: 'Theme mode: auto (probes terminal on next startup)' };
|
|
87
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* theme.ts — Semantic colour token layer (W4-R4 port of the TUI theme system).
|
|
3
|
+
*
|
|
4
|
+
* Two token layers, resolved per background mode:
|
|
5
|
+
*
|
|
6
|
+
* - ThemeTokens (transcript): every colour decision in the markdown /
|
|
7
|
+
* conversation-rendering / tool-call / system-message pipeline. Agent-local
|
|
8
|
+
* (the SDK presentation contract hoisted only the CHROME tone table, not
|
|
9
|
+
* these transcript-semantic tokens). Read live per render via activeTheme()
|
|
10
|
+
* so a dark→light repaint re-resolves with no module reload.
|
|
11
|
+
*
|
|
12
|
+
* - UiToneTokens (chrome): the panel/modal/overlay/header/footer/thinking tone
|
|
13
|
+
* table. This IS the SDK presentation contract — resolveUiTones() / and
|
|
14
|
+
* activeUiTones() compose the ThemeMode dimension over the SDK's
|
|
15
|
+
* resolveTones(), so the agent never mints its own light chrome variant.
|
|
16
|
+
*
|
|
17
|
+
* Dark mode values are the historically used colours. Light mode values exist
|
|
18
|
+
* for correctness parity and are consumed once the terminal-bg-probe
|
|
19
|
+
* (terminal-escapes / OSC 11, W4-R2) resolves the mode and setActiveThemeMode
|
|
20
|
+
* is called. Callers that do not yet have mode detection get 'dark' — the safe
|
|
21
|
+
* default (activeMode starts dark).
|
|
22
|
+
*
|
|
23
|
+
* IMPORTANT: inline code has NO background token. The bg:#1a1a1a hardcode that
|
|
24
|
+
* previously existed caused a near-black box on light terminals. Differentiate
|
|
25
|
+
* inline code via inlineCodeFg + bold only; bg inherits the terminal.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { UI_TONES } from './ui-primitives.ts';
|
|
29
|
+
import { resolveTones, type ThemeMode, type ToneTokens } from '@pellux/goodvibes-sdk/platform/presentation';
|
|
30
|
+
|
|
31
|
+
/** Background mode — dark is the safe default until the bg-probe resolves. */
|
|
32
|
+
export type { ThemeMode };
|
|
33
|
+
|
|
34
|
+
/** Resolved semantic colour tokens (concrete hex strings or ANSI-256 indices). */
|
|
35
|
+
export interface ThemeTokens {
|
|
36
|
+
/** H1 heading foreground + table header accent */
|
|
37
|
+
heading1: string;
|
|
38
|
+
/** H2 heading foreground */
|
|
39
|
+
heading2: string;
|
|
40
|
+
/** H3 heading foreground (ANSI-256 — falls back to nearest on ansi256 terminals) */
|
|
41
|
+
heading3: string;
|
|
42
|
+
/** Inline code foreground (bold is applied separately by caller) */
|
|
43
|
+
inlineCodeFg: string;
|
|
44
|
+
/** Hyperlink and bare-URL foreground */
|
|
45
|
+
link: string;
|
|
46
|
+
/** Non-current search match background */
|
|
47
|
+
searchMatchBg: string;
|
|
48
|
+
/** Non-current search match foreground */
|
|
49
|
+
searchMatchFg: string;
|
|
50
|
+
/** Current (focused) search match background */
|
|
51
|
+
searchCurrentBg: string;
|
|
52
|
+
/** Current (focused) search match foreground */
|
|
53
|
+
searchCurrentFg: string;
|
|
54
|
+
/** Strikethrough / muted text foreground */
|
|
55
|
+
strikethrough: string;
|
|
56
|
+
/** Blockquote / dim text foreground */
|
|
57
|
+
blockquote: string;
|
|
58
|
+
/** Assistant event-line marker + label accent */
|
|
59
|
+
assistantHeader: string;
|
|
60
|
+
/** Reasoning / thinking block accent */
|
|
61
|
+
reasoningAccent: string;
|
|
62
|
+
/** Tool call / active status accent (also diff/tool result label) */
|
|
63
|
+
toolAccent: string;
|
|
64
|
+
/** Collapsed-fragment body background (tool result preview bg) */
|
|
65
|
+
collapsedBodyBg: string;
|
|
66
|
+
/** Checked task-list checkbox foreground (✓ in green) */
|
|
67
|
+
checkboxChecked: string;
|
|
68
|
+
/** Error / cancelled message bar background */
|
|
69
|
+
errorBarBg: string;
|
|
70
|
+
/** Model name / provider dim label foreground */
|
|
71
|
+
modelNameDim: string;
|
|
72
|
+
/** Tool name foreground in tool-result event line */
|
|
73
|
+
toolNameFg: string;
|
|
74
|
+
/** Diff block accent — marker, label, and collapsed-prefix foreground */
|
|
75
|
+
diffAccent: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Dark palette (byte-identical to the agent's prior static reads).
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
const DARK: ThemeTokens = {
|
|
82
|
+
heading1: '#00ffff',
|
|
83
|
+
heading2: '#00ffff',
|
|
84
|
+
heading3: '111',
|
|
85
|
+
inlineCodeFg: '#ffcc00',
|
|
86
|
+
link: '#00aaff',
|
|
87
|
+
searchMatchBg: '#806600',
|
|
88
|
+
searchMatchFg: '#ffffff',
|
|
89
|
+
searchCurrentBg: '#ffff00',
|
|
90
|
+
searchCurrentFg: '#000000',
|
|
91
|
+
strikethrough: '244',
|
|
92
|
+
blockquote: '244',
|
|
93
|
+
assistantHeader: UI_TONES.accent.control,
|
|
94
|
+
reasoningAccent: UI_TONES.state.reasoning,
|
|
95
|
+
toolAccent: UI_TONES.state.info,
|
|
96
|
+
collapsedBodyBg: '#1a1a1a',
|
|
97
|
+
checkboxChecked: '#22c55e',
|
|
98
|
+
errorBarBg: '#3a1a1a',
|
|
99
|
+
modelNameDim: '#94a3b8',
|
|
100
|
+
toolNameFg: '#e2e8f0',
|
|
101
|
+
diffAccent: '#f59e0b',
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Light palette — dark-on-light legibility (contrast ratios against #ffffff):
|
|
106
|
+
// heading1/2: Deep teal (#0077aa) — readable on white/cream terminals
|
|
107
|
+
// heading3: ANSI-256 24 (dark cyan) — the light equivalent of 111
|
|
108
|
+
// inlineCodeFg: Dark orange (#b45309) — distinguishable without a box bg
|
|
109
|
+
// link: Standard blue (#0055cc) — browser-link convention
|
|
110
|
+
// searchMatchBg: Muted yellow (#ffe066) on black fg — visible on light bg
|
|
111
|
+
// searchCurrentBg: Strong amber (#f59e0b) — current match is more vivid
|
|
112
|
+
// blockquote: Dim blue-gray (ANSI-256 67)
|
|
113
|
+
// assistantHeader: Dark cyan (#0e7490)
|
|
114
|
+
// reasoningAccent: Dark purple (#7c3aed)
|
|
115
|
+
// toolAccent: Dark sky (#0369a1)
|
|
116
|
+
// collapsedBodyBg: Very light gray (#f3f4f6)
|
|
117
|
+
// checkboxChecked: Forest green (#15803d) — ~5.2:1 on #fff
|
|
118
|
+
// errorBarBg: Soft rose (#fee2e2) — legible text on top
|
|
119
|
+
// modelNameDim: Slate-500 (#64748b) — ~4.6:1 on #fff
|
|
120
|
+
// toolNameFg: Slate-800 (#334155) — strong enough for tool names
|
|
121
|
+
// diffAccent: Amber-700 (#b45309) — ~4.7:1 on #fff
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
const LIGHT: ThemeTokens = {
|
|
124
|
+
heading1: '#0077aa',
|
|
125
|
+
heading2: '#0077aa',
|
|
126
|
+
heading3: '24',
|
|
127
|
+
inlineCodeFg: '#b45309',
|
|
128
|
+
link: '#0055cc',
|
|
129
|
+
searchMatchBg: '#ffe066',
|
|
130
|
+
searchMatchFg: '#000000',
|
|
131
|
+
searchCurrentBg: '#f59e0b',
|
|
132
|
+
searchCurrentFg: '#000000',
|
|
133
|
+
strikethrough: '244',
|
|
134
|
+
blockquote: '67',
|
|
135
|
+
assistantHeader: '#0e7490',
|
|
136
|
+
reasoningAccent: '#7c3aed',
|
|
137
|
+
toolAccent: '#0369a1',
|
|
138
|
+
collapsedBodyBg: '#f3f4f6',
|
|
139
|
+
checkboxChecked: '#15803d',
|
|
140
|
+
errorBarBg: '#fee2e2',
|
|
141
|
+
modelNameDim: '#64748b',
|
|
142
|
+
toolNameFg: '#334155',
|
|
143
|
+
diffAccent: '#b45309',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
Object.freeze(DARK);
|
|
147
|
+
Object.freeze(LIGHT);
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* resolveTheme — Return the transcript token set for the given background mode.
|
|
151
|
+
* The returned object is frozen; callers should not mutate it.
|
|
152
|
+
*/
|
|
153
|
+
export function resolveTheme(mode: ThemeMode): Readonly<ThemeTokens> {
|
|
154
|
+
return mode === 'light' ? LIGHT : DARK;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Default dark-mode token set, exported for convenience. Frozen. */
|
|
158
|
+
export const DARK_THEME: Readonly<ThemeTokens> = DARK;
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// Chrome tokens (UiToneTokens) — the SDK presentation contract, mode-resolved.
|
|
162
|
+
//
|
|
163
|
+
// The dark table is the SDK's TONE_TOKENS (re-exported here as UI_TONES); the
|
|
164
|
+
// light variant is the SDK's resolveTones('light'). The agent does NOT mint its
|
|
165
|
+
// own light chrome variant — that duplication is exactly what W4-S1 ended.
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
/** The chrome tone-token shape (the SDK ToneTokens contract). */
|
|
169
|
+
export type UiToneTokens = ToneTokens;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* resolveUiTones — chrome token set for the given mode. Single read path;
|
|
173
|
+
* 'dark' is byte-identical to the UI_TONES constant (same SDK object).
|
|
174
|
+
* Prefer activeUiTones() at call sites.
|
|
175
|
+
*/
|
|
176
|
+
export function resolveUiTones(mode: ThemeMode): Readonly<UiToneTokens> {
|
|
177
|
+
return resolveTones(mode);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ===========================================================================
|
|
181
|
+
// Active-mode runtime.
|
|
182
|
+
//
|
|
183
|
+
// The mode is decided ONCE at startup — from appearance config
|
|
184
|
+
// (display.themeMode forced dark/light) or the terminal-background probe
|
|
185
|
+
// (auto) — and is then stable for the session. Transcript tokens (activeTheme)
|
|
186
|
+
// and chrome tokens (activeUiTones) are both read live per render, so a
|
|
187
|
+
// dark→light repaint (auto mode, light wins within the probe window)
|
|
188
|
+
// re-resolves without any module reload.
|
|
189
|
+
//
|
|
190
|
+
// registerThemeRefresh exists for owners that BAKE tone values into
|
|
191
|
+
// module-level constants (which cannot be re-resolved per call). Nothing
|
|
192
|
+
// registers this wave — the agent's opaque panel palettes (polish.ts
|
|
193
|
+
// DEFAULT_PANEL_PALETTE and the modal/overlay/fullscreen surfaces built from
|
|
194
|
+
// it) paint OPAQUE dark boxes whose fg/state tokens stay dark in both modes, so
|
|
195
|
+
// they need no rebuild for dark parity (the opaque-surface trio deferral). The
|
|
196
|
+
// hook is kept at parity with the TUI so those surfaces can register when full
|
|
197
|
+
// light-chrome coherence lands.
|
|
198
|
+
// ===========================================================================
|
|
199
|
+
|
|
200
|
+
/** User-facing appearance preference: auto probes the terminal; dark/light force. */
|
|
201
|
+
export type ThemeModeSetting = 'auto' | ThemeMode;
|
|
202
|
+
|
|
203
|
+
/** The resolved mode in effect for the current session. Dark is the safe default. */
|
|
204
|
+
let activeMode: ThemeMode = 'dark';
|
|
205
|
+
|
|
206
|
+
/** In-place palette rebuilders, run (in registration order) on every mode flip. */
|
|
207
|
+
const themeRefreshers: Array<() => void> = [];
|
|
208
|
+
|
|
209
|
+
/** The active mode, for diagnostics / tests. */
|
|
210
|
+
export function getActiveThemeMode(): ThemeMode {
|
|
211
|
+
return activeMode;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Register an in-place palette rebuild to run whenever the active mode changes.
|
|
216
|
+
* Base-palette owners register at their own module-eval time (before any
|
|
217
|
+
* derived palette), so refreshers run base-first.
|
|
218
|
+
*/
|
|
219
|
+
export function registerThemeRefresh(rebuild: () => void): void {
|
|
220
|
+
themeRefreshers.push(rebuild);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Set the active background mode and rebuild every registered chrome palette in
|
|
225
|
+
* place. Idempotent and reversible. Callers: startup (forced mode or probe
|
|
226
|
+
* result, wired via installBackgroundThemeProbe's applyThemeMode injection).
|
|
227
|
+
*/
|
|
228
|
+
export function setActiveThemeMode(mode: ThemeMode): void {
|
|
229
|
+
activeMode = mode;
|
|
230
|
+
for (const rebuild of themeRefreshers) rebuild();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Transcript tokens for the active mode — read live, per render. */
|
|
234
|
+
export function activeTheme(): Readonly<ThemeTokens> {
|
|
235
|
+
return resolveTheme(activeMode);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Chrome tokens for the active mode — read live, per render. */
|
|
239
|
+
export function activeUiTones(): Readonly<UiToneTokens> {
|
|
240
|
+
return resolveUiTones(activeMode);
|
|
241
|
+
}
|
package/src/renderer/thinking.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import { type Line } from '../types/grid.ts';
|
|
2
|
-
import { BORDERS
|
|
2
|
+
import { BORDERS } from './layout.ts';
|
|
3
3
|
import { renderConversationNotice } from './conversation-surface.ts';
|
|
4
|
+
import { activeUiTones } from './theme.ts';
|
|
4
5
|
|
|
5
6
|
export function renderThinkingBlock(text: string, width: number): Line[] {
|
|
7
|
+
// Thinking notices paint the ▌ marker and italic body on the TRANSPARENT
|
|
8
|
+
// terminal background (renderConversationNotice passes no bodyBg), so both
|
|
9
|
+
// colours resolve per-render through activeUiTones() to stay legible on a
|
|
10
|
+
// light terminal. In dark mode the accent adopts the shared reasoning purple
|
|
11
|
+
// (state.reasoning) and the body adopts chrome.faint (== fg.dim) — a small,
|
|
12
|
+
// deliberate convergence to the reference tokens from the agent's prior local
|
|
13
|
+
// BORDERS.THINKING.color / COLORS.DIM_TEXT (see the W4-R4 visible-changes note).
|
|
14
|
+
const t = activeUiTones();
|
|
6
15
|
return renderConversationNotice(
|
|
7
16
|
text,
|
|
8
17
|
width,
|
|
9
18
|
{
|
|
10
|
-
accent:
|
|
11
|
-
text:
|
|
19
|
+
accent: t.state.reasoning,
|
|
20
|
+
text: t.chrome.faint,
|
|
12
21
|
dim: true,
|
|
13
22
|
italic: true,
|
|
14
23
|
},
|
|
@@ -4,6 +4,7 @@ import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
|
4
4
|
import type { ToolCall } from '@pellux/goodvibes-sdk/platform/types';
|
|
5
5
|
import { stripDangerousAnsi } from './ansi-sanitize.ts';
|
|
6
6
|
import { friendlyToolLabel } from './tool-labels.ts';
|
|
7
|
+
import { activeUiTones } from './theme.ts';
|
|
7
8
|
|
|
8
9
|
const TOOL_NAME_MIN_WIDTH = 8;
|
|
9
10
|
const TOOL_NAME_MAX_WIDTH = 30;
|
|
@@ -55,6 +56,9 @@ function buildLeftSegments(
|
|
|
55
56
|
): Array<{ text: string; fg: string; bold?: boolean; dim?: boolean }> {
|
|
56
57
|
if (leftBudget <= 0) return [];
|
|
57
58
|
|
|
59
|
+
// Read live tones so the tool-call row is legible in light mode. Dark values
|
|
60
|
+
// are byte-identical (t.fg.primary == #e2e8f0, t.chrome.bad == #ef4444).
|
|
61
|
+
const t = activeUiTones();
|
|
58
62
|
const segments: Array<{ text: string; fg: string; bold?: boolean; dim?: boolean }> = [];
|
|
59
63
|
const suffixBudget = suffixText ? Math.min(Math.max(12, Math.floor(leftBudget * 0.3)), 20) : 0;
|
|
60
64
|
const suffixDisplay = suffixBudget > 0 ? truncateDisplay(suffixText, suffixBudget) : '';
|
|
@@ -83,14 +87,14 @@ function buildLeftSegments(
|
|
|
83
87
|
segments.push({ text: toolNameDisplay, fg: '#00ffcc', bold: true });
|
|
84
88
|
}
|
|
85
89
|
if (keyArgDisplay) {
|
|
86
|
-
segments.push({ text: ' ', fg:
|
|
90
|
+
segments.push({ text: ' ', fg: t.fg.primary });
|
|
87
91
|
segments.push({ text: keyArgDisplay, fg: '252' });
|
|
88
92
|
}
|
|
89
93
|
if (suffixDisplay) {
|
|
90
|
-
segments.push({ text: ' ', fg:
|
|
94
|
+
segments.push({ text: ' ', fg: t.fg.primary });
|
|
91
95
|
segments.push({
|
|
92
96
|
text: suffixDisplay,
|
|
93
|
-
fg: suffixText.startsWith('- ') ?
|
|
97
|
+
fg: suffixText.startsWith('- ') ? t.chrome.bad : '244',
|
|
94
98
|
dim: true,
|
|
95
99
|
});
|
|
96
100
|
}
|
|
@@ -4,11 +4,34 @@ import { VERSION } from '../version.ts';
|
|
|
4
4
|
import { fitDisplay, getDisplayWidth, truncateDisplay, wrapText, interpolateColor } from '../utils/terminal-width.ts';
|
|
5
5
|
import { renderConversationFragment, renderConversationStatusLine, type ConversationStatusSegment } from './conversation-surface.ts';
|
|
6
6
|
import { GLYPHS } from './ui-primitives.ts';
|
|
7
|
+
import { activeUiTones } from './theme.ts';
|
|
8
|
+
import {
|
|
9
|
+
THINKING_PHRASES,
|
|
10
|
+
waitingPhrase,
|
|
11
|
+
type WaitingState,
|
|
12
|
+
} from '@pellux/goodvibes-sdk/platform/presentation';
|
|
7
13
|
|
|
8
14
|
/** Number of frames before the animated gradient completes one full cycle. */
|
|
9
15
|
const GRADIENT_CYCLE_FRAMES = 50;
|
|
10
16
|
/** Number of frames before rotating to the next thinking phrase (~30 seconds at 80ms/frame). */
|
|
11
17
|
const PHRASE_ROTATION_FRAMES = 375;
|
|
18
|
+
/**
|
|
19
|
+
* Silence threshold before the whimsical phrase rotation freezes and an honest
|
|
20
|
+
* label (Waiting for model Ns / Stalled Ns) takes over. Matches the TUI.
|
|
21
|
+
*/
|
|
22
|
+
const THINKING_STALL_FREEZE_MS = 2_500;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Per-turn stall signal derived from stream metrics — computed from the last
|
|
26
|
+
* delta clock every render (not from any event), so it degrades gracefully with
|
|
27
|
+
* zero new SDK events. `reconnect` is set only when the transport surfaces retry
|
|
28
|
+
* counters (the agent's SDK orchestrator does not today — see computeStallInfo).
|
|
29
|
+
*/
|
|
30
|
+
export interface ThinkingStallInfo {
|
|
31
|
+
/** Ms since the last output-token advance (or the turn start if none yet). */
|
|
32
|
+
readonly msSinceLastDelta: number;
|
|
33
|
+
readonly reconnect?: { readonly attempt: number; readonly maxAttempts: number };
|
|
34
|
+
}
|
|
12
35
|
|
|
13
36
|
/** Format a number: up to 999, then 1.0k, 1.0M, 1.0B, 1.0T */
|
|
14
37
|
function fmtNum(n: number): string {
|
|
@@ -236,9 +259,12 @@ export class UIFactory {
|
|
|
236
259
|
if (inp > 0 || out > 0) {
|
|
237
260
|
statusTokens.push({ text: `↑${fmtNum(inp)} ↓${fmtNum(out)}`, fg: '240' });
|
|
238
261
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
262
|
+
// W4-R4: the disconnected footer 'waiting for your approval' token is retired
|
|
263
|
+
// here — the approval-wait truth now lives in the unified waiting state of the
|
|
264
|
+
// thinking indicator (createThinkingFragment's approvalPending path) and in the
|
|
265
|
+
// permission prompt itself, so the footer no longer carries a separate,
|
|
266
|
+
// easily-desynced copy. composerPendingRisk stays in the signature for the
|
|
267
|
+
// composer flags row; it just no longer mints its own status token.
|
|
242
268
|
const rightNotice = isRecentlyCopied
|
|
243
269
|
? { text: `copied ${GLYPHS.status.success} `, fg: '81', bold: true }
|
|
244
270
|
: dangerMode
|
|
@@ -277,39 +303,69 @@ export class UIFactory {
|
|
|
277
303
|
return lines;
|
|
278
304
|
}
|
|
279
305
|
|
|
280
|
-
/**
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
'Harmonizing...',
|
|
300
|
-
'Feeling it...',
|
|
301
|
-
'In the zone...',
|
|
302
|
-
];
|
|
306
|
+
/**
|
|
307
|
+
* Per-frame stall info from stream metrics — computed from a last-delta clock
|
|
308
|
+
* every render (not from any event) so it degrades gracefully with zero new
|
|
309
|
+
* SDK events. Undefined until a delta clock exists this turn. Renderer-local
|
|
310
|
+
* by design (per the S1 decision record: the SDK owns state->wording; deriving
|
|
311
|
+
* WHICH state applies stays with each renderer's own stream-metrics shape).
|
|
312
|
+
*/
|
|
313
|
+
public static computeStallInfo(
|
|
314
|
+
lastDeltaAtMs: number | undefined,
|
|
315
|
+
reconnectAttempt: number | undefined,
|
|
316
|
+
reconnectMaxAttempts: number | undefined,
|
|
317
|
+
nowMs: number,
|
|
318
|
+
): ThinkingStallInfo | undefined {
|
|
319
|
+
if (lastDeltaAtMs === undefined) return undefined;
|
|
320
|
+
const reconnect = reconnectAttempt !== undefined && reconnectMaxAttempts !== undefined
|
|
321
|
+
? { attempt: reconnectAttempt, maxAttempts: reconnectMaxAttempts }
|
|
322
|
+
: undefined;
|
|
323
|
+
return { msSinceLastDelta: nowMs - lastDeltaAtMs, reconnect };
|
|
324
|
+
}
|
|
303
325
|
|
|
304
|
-
/**
|
|
305
|
-
|
|
306
|
-
|
|
326
|
+
/**
|
|
327
|
+
* Render-loop stall decision: suppress stall detection entirely while a tool
|
|
328
|
+
* is actively executing. The last-delta clock only advances on output-token
|
|
329
|
+
* deltas and is never advanced during tool execution (the model isn't
|
|
330
|
+
* producing tokens then), so without this gate any tool call longer than
|
|
331
|
+
* THINKING_STALL_FREEZE_MS would print "Stalled Ns..." above a ticking tool
|
|
332
|
+
* row — a false positive. Genuine no-delta silence while waiting on the
|
|
333
|
+
* provider (including pre-first-token) still stall-detects here, since no tool
|
|
334
|
+
* is active then — the honest stall case this indicator exists for.
|
|
335
|
+
*/
|
|
336
|
+
public static computeRenderStallInfo(
|
|
337
|
+
metrics: { toolActive: boolean; lastDeltaAtMs: number | undefined; nowMs: number },
|
|
338
|
+
): ThinkingStallInfo | undefined {
|
|
339
|
+
return metrics.toolActive
|
|
340
|
+
? undefined
|
|
341
|
+
: this.computeStallInfo(metrics.lastDeltaAtMs, undefined, undefined, metrics.nowMs);
|
|
342
|
+
}
|
|
307
343
|
|
|
308
|
-
public static createThinkingFragment(width: number, spinner: string, frame: number = 0, tokenSpeed?: number, toolPreview?: string, inputTokens?: number, outputTokens?: number): Line[] {
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
const
|
|
312
|
-
|
|
344
|
+
public static createThinkingFragment(width: number, spinner: string, frame: number = 0, tokenSpeed?: number, toolPreview?: string, inputTokens?: number, outputTokens?: number, stallInfo?: ThinkingStallInfo, approvalPending?: boolean): Line[] {
|
|
345
|
+
// Live thinking row paints on the transparent terminal bg → read the
|
|
346
|
+
// mode-resolved chrome tones per render (gradient/brand flip in light mode).
|
|
347
|
+
const tones = activeUiTones();
|
|
348
|
+
// Decide WHICH honest waiting state applies (renderer-local), then defer the
|
|
349
|
+
// exact wording to the SDK presentation contract's waitingPhrase(). Precedence
|
|
350
|
+
// matches the contract: approval > reconnecting > pre-first-token > stalled >
|
|
351
|
+
// thinking. Pre-first-token silence reads "Waiting for model Ns..." (blame-free
|
|
352
|
+
// for reasoning models that think before emitting), NOT "Stalled".
|
|
353
|
+
const isStalled = stallInfo !== undefined && stallInfo.msSinceLastDelta >= THINKING_STALL_FREEZE_MS;
|
|
354
|
+
let state: WaitingState;
|
|
355
|
+
if (approvalPending) state = 'approval';
|
|
356
|
+
else if (stallInfo?.reconnect) state = 'reconnecting';
|
|
357
|
+
else if (isStalled && (outputTokens ?? 0) === 0) state = 'pre-first-token';
|
|
358
|
+
else if (isStalled) state = 'stalled';
|
|
359
|
+
else state = 'thinking';
|
|
360
|
+
const phrase = waitingPhrase(state, {
|
|
361
|
+
reconnectAttempt: stallInfo?.reconnect?.attempt,
|
|
362
|
+
reconnectMaxAttempts: stallInfo?.reconnect?.maxAttempts,
|
|
363
|
+
msSinceLastDelta: stallInfo?.msSinceLastDelta,
|
|
364
|
+
frame,
|
|
365
|
+
});
|
|
366
|
+
// A tok/s figure next to an approval prompt reads as if the model were still
|
|
367
|
+
// working — suppress it while waiting on the user.
|
|
368
|
+
const speedSuffix = (!approvalPending && tokenSpeed !== undefined && tokenSpeed > 0) ? ` (${Math.round(tokenSpeed)} tok/s)` : '';
|
|
313
369
|
const text = ` ${spinner} ${phrase}${speedSuffix} `;
|
|
314
370
|
|
|
315
371
|
const textWidth = Math.max(1, getDisplayWidth(text) - 1);
|
|
@@ -319,7 +375,7 @@ export class UIFactory {
|
|
|
319
375
|
const gradientPos = raw <= 0.5 ? raw * 2 : (1 - raw) * 2;
|
|
320
376
|
return {
|
|
321
377
|
text: char,
|
|
322
|
-
fg: interpolateColor(
|
|
378
|
+
fg: interpolateColor(tones.accent.gradientStart, tones.accent.gradientEnd, gradientPos),
|
|
323
379
|
bold: true,
|
|
324
380
|
};
|
|
325
381
|
});
|
|
@@ -327,7 +383,7 @@ export class UIFactory {
|
|
|
327
383
|
const inTok = inputTokens ?? 0;
|
|
328
384
|
const outTok = outputTokens ?? 0;
|
|
329
385
|
segments.push({ text: ` in ${fmtNum(inTok)} `, fg: '243', dim: true });
|
|
330
|
-
segments.push({ text: `out ${fmtNum(outTok)}`, fg:
|
|
386
|
+
segments.push({ text: `out ${fmtNum(outTok)}`, fg: tones.accent.brand });
|
|
331
387
|
}
|
|
332
388
|
const line = createEmptyLine(width);
|
|
333
389
|
let col = 1;
|