@pellux/goodvibes-tui 1.1.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. package/src/renderer/qr-renderer.ts +0 -120
@@ -0,0 +1,180 @@
1
+ import { infoRow } from './modal-surface-helpers.ts';
2
+ import type {
3
+ ConfigModalActionContext,
4
+ ConfigModalRow,
5
+ ConfigModalSurface,
6
+ ConfigModalTab,
7
+ ConfigModalView,
8
+ } from '../../input/config-modal-types.ts';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Memory → config-modal surface (W6.1 group-B port). Two tabs: 'All Records'
12
+ // (the full record list) and 'Review Queue' (the operator review queue) — the
13
+ // same split the panel toggled with Tab, now real host tabs. Review-state
14
+ // mutations (reviewed/stale/contradicted/fresh) and record delete route to the
15
+ // existing `/recall review` / `/recall remove` command path (charter: no
16
+ // destructive mutation direct-called from a modal). Selection-blind port: the
17
+ // panel's selected-record scope/class/tags/provenance detail is folded into
18
+ // each row label. `memoryRegistry` absent → the retired MemoryPanel's
19
+ // "not configured" copy renders as an honest degraded state.
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /** Minimal read shape of a `MemoryRecord` this modal renders. */
23
+ interface MemoryRecordLike {
24
+ readonly id: string;
25
+ readonly scope: string;
26
+ readonly cls: string;
27
+ readonly summary: string;
28
+ readonly detail?: string | undefined;
29
+ readonly tags: readonly string[];
30
+ readonly reviewState: string;
31
+ readonly confidence: number;
32
+ readonly staleReason?: string | undefined;
33
+ readonly reviewedAt?: number | undefined;
34
+ readonly reviewedBy?: string | undefined;
35
+ readonly createdAt: number;
36
+ readonly provenance: readonly { readonly kind: string; readonly ref: string }[];
37
+ }
38
+
39
+ export interface MemoryModalDeps {
40
+ readonly memoryRegistry?: {
41
+ search(filter?: { limit?: number }): readonly MemoryRecordLike[];
42
+ reviewQueue(limit?: number): readonly MemoryRecordLike[];
43
+ };
44
+ }
45
+
46
+ // Mirrors registerKnowledgePanels's withUnconfiguredFallback copy for the
47
+ // retired MemoryPanel (src/panels/builtin/knowledge.ts) verbatim.
48
+ const NOT_CONFIGURED_TITLE = 'Memory registry not configured for this session.';
49
+ const NOT_CONFIGURED_BODY = 'This runtime was not wired with a project memory registry at bootstrap, so no memory data is available.';
50
+
51
+ function fmtTime(ts: number): string {
52
+ return new Date(ts).toISOString().slice(0, 16).replace('T', ' ');
53
+ }
54
+
55
+ class MemoryModalSurface implements ConfigModalSurface {
56
+ readonly name = 'memory-modal';
57
+ readonly title = 'Memory';
58
+ private allRecords: MemoryRecordLike[] = [];
59
+ private reviewRecords: MemoryRecordLike[] = [];
60
+
61
+ constructor(private readonly deps: MemoryModalDeps) {}
62
+
63
+ private readonly reviewGate = (row: ConfigModalRow | null, tabId: string): boolean => tabId === 'review' && row !== null;
64
+
65
+ readonly actions = [
66
+ { key: 'enter', id: 'markReviewed', label: 'reviewed', enabledFor: this.reviewGate },
67
+ { key: 's', id: 'markStale', label: 'stale', enabledFor: this.reviewGate },
68
+ { key: 'c', id: 'markContradicted', label: 'contradicted', enabledFor: this.reviewGate },
69
+ { key: 'f', id: 'markFresh', label: 'fresh', enabledFor: this.reviewGate },
70
+ { key: 'd', id: 'remove', label: 'delete', enabledFor: (row: ConfigModalRow | null) => row !== null },
71
+ { key: 'r', id: 'refresh', label: 'refresh' },
72
+ ];
73
+
74
+ onOpen(): void { this.refresh(); }
75
+
76
+ private refresh(): void {
77
+ if (!this.deps.memoryRegistry) { this.allRecords = []; this.reviewRecords = []; return; }
78
+ this.allRecords = [...this.deps.memoryRegistry.search({ limit: 100 })];
79
+ this.reviewRecords = [...this.deps.memoryRegistry.reviewQueue(24)];
80
+ }
81
+
82
+ private recordFrom(id: string): MemoryRecordLike | undefined {
83
+ return this.allRecords.find((r) => r.id === id) ?? this.reviewRecords.find((r) => r.id === id);
84
+ }
85
+
86
+ private allTab(): ConfigModalTab {
87
+ const rows: ConfigModalRow[] = this.allRecords.map((record) => ({
88
+ id: record.id,
89
+ label: `[${record.scope.slice(0, 1).toUpperCase()}/${record.cls.slice(0, 3).toUpperCase()}] ${record.id.slice(-8)} ${fmtTime(record.createdAt)} ${record.summary}${record.tags.length > 0 ? ` #${record.tags.join(' #')}` : ''}`,
90
+ }));
91
+ return {
92
+ id: 'all',
93
+ label: 'All Records',
94
+ header: [`records ${this.allRecords.length} review queue ${this.reviewRecords.length}`],
95
+ rows,
96
+ emptyText: 'No memory records.',
97
+ hints: ['d delete'],
98
+ };
99
+ }
100
+
101
+ private reviewTab(): ConfigModalTab {
102
+ const rows: ConfigModalRow[] = this.reviewRecords.map((record) => ({
103
+ id: record.id,
104
+ label: `${record.reviewState.padEnd(13)} ${String(record.confidence).padStart(3)}% ${record.summary}${record.staleReason ? ` (stale: ${record.staleReason})` : ''}`,
105
+ }));
106
+ return {
107
+ id: 'review',
108
+ label: 'Review Queue',
109
+ header: [`records ${this.allRecords.length} review queue ${this.reviewRecords.length}`],
110
+ rows,
111
+ emptyText: 'No records in the review queue.',
112
+ hints: ['enter reviewed', 's stale', 'c contradicted', 'f fresh', 'd delete'],
113
+ };
114
+ }
115
+
116
+ buildView(): ConfigModalView {
117
+ if (!this.deps.memoryRegistry) {
118
+ return {
119
+ title: 'Memory',
120
+ degraded: `${NOT_CONFIGURED_TITLE} ${NOT_CONFIGURED_BODY}`,
121
+ tabs: [{ id: 'all', label: 'All Records', rows: [] }],
122
+ };
123
+ }
124
+ return { title: 'Memory', tabs: [this.allTab(), this.reviewTab()] };
125
+ }
126
+
127
+ onAction(id: string, ctx: ConfigModalActionContext): void {
128
+ if (id === 'refresh') { this.refresh(); ctx.setStatus('Reloaded memory records.'); return; }
129
+ const record = ctx.row ? this.recordFrom(ctx.row.id) : undefined;
130
+ if (!record) return;
131
+ const review = (state: string, confidence: number, reason?: string): void => {
132
+ const args = ['review', record.id, state, '--confidence', String(confidence), '--by', 'operator'];
133
+ if (reason) args.push('--reason', reason);
134
+ void ctx.executeCommand?.('recall', args);
135
+ ctx.setStatus(`Dispatched /recall review ${record.id} ${state}.`);
136
+ };
137
+ switch (id) {
138
+ case 'markReviewed': review('reviewed', Math.max(record.confidence, 85)); break;
139
+ case 'markStale': review('stale', Math.min(record.confidence, 40), 'marked stale from the memory panel'); break;
140
+ case 'markContradicted': review('contradicted', 0, 'marked contradicted from the memory panel'); break;
141
+ case 'markFresh': review('fresh', Math.max(record.confidence, 60)); break;
142
+ case 'remove': void ctx.executeCommand?.('recall', ['remove', record.id]); ctx.setStatus(`Dispatched /recall remove ${record.id}.`); break;
143
+ }
144
+ }
145
+ }
146
+
147
+ export function createMemoryModalSurface(deps: MemoryModalDeps): ConfigModalSurface {
148
+ return new MemoryModalSurface(deps);
149
+ }
150
+
151
+ /**
152
+ * Deterministic golden fixture: fixed memory records with frozen createdAt
153
+ * timestamps — no live registry, no wall-clock, no random ids.
154
+ */
155
+ export function memoryModalGoldenSurface(): ConfigModalSurface {
156
+ const FIXED_CREATED_AT = 1735689600000; // 2025-01-01T00:00:00.000Z
157
+ const records: readonly MemoryRecordLike[] = [
158
+ {
159
+ id: 'mem-0000001a', scope: 'project', cls: 'decision',
160
+ summary: 'Wave-6 batches panel retirements behind modal builders.',
161
+ detail: 'Applies to KNOWLEDGE, MEMORY, WORK-PLAN in WO-B.',
162
+ tags: ['wave-6', 'modals'], reviewState: 'reviewed', confidence: 90,
163
+ reviewedAt: FIXED_CREATED_AT + 3600000, reviewedBy: 'operator', createdAt: FIXED_CREATED_AT,
164
+ provenance: [{ kind: 'session', ref: 'session-fixed-1' }],
165
+ },
166
+ {
167
+ id: 'mem-0000002b', scope: 'session', cls: 'risk',
168
+ summary: 'Modal review actions must not call mutation APIs directly.',
169
+ tags: ['charter'], reviewState: 'stale', confidence: 35,
170
+ staleReason: 'needs re-verification against the charter doc',
171
+ createdAt: FIXED_CREATED_AT + 86400000, provenance: [],
172
+ },
173
+ ];
174
+ return createMemoryModalSurface({
175
+ memoryRegistry: {
176
+ search: () => records,
177
+ reviewQueue: () => records.filter((record) => record.reviewState !== 'reviewed'),
178
+ },
179
+ });
180
+ }
@@ -0,0 +1,54 @@
1
+ import { DEFAULT_PANEL_PALETTE } from '../polish.ts';
2
+ import type { ModalSectionStyle } from '../../renderer/modal-factory.ts';
3
+ import type { ConfigModalRow } from '../../input/config-modal-types.ts';
4
+
5
+ /**
6
+ * Shared formatting helpers for W6.1 config-modal surfaces. Row styling reuses
7
+ * DEFAULT_PANEL_PALETTE (the retired panels' palette) so a migrated modal reads
8
+ * as the same surface it replaced — the goldens stay panel-consistent.
9
+ */
10
+ export const PALETTE = DEFAULT_PANEL_PALETTE;
11
+
12
+ export type Tone = 'good' | 'bad' | 'warn' | 'info' | 'dim' | 'value';
13
+
14
+ /** Map a semantic tone to a ModalSectionStyle (foreground only). */
15
+ export function toneStyle(tone: Tone): ModalSectionStyle {
16
+ return { fg: PALETTE[tone] };
17
+ }
18
+
19
+ /** A single-glyph status dot matching the provider-console convention
20
+ * (● online · ◐ warming · ✕ error · ○ idle). */
21
+ export function statusGlyph(tone: Tone): string {
22
+ switch (tone) {
23
+ case 'good': return '●';
24
+ case 'warn': return '◐';
25
+ case 'bad': return '✕';
26
+ default: return '○';
27
+ }
28
+ }
29
+
30
+ /** Fixed-width left-justify by display columns (plain padEnd is fine for the
31
+ * ASCII/latin labels these surfaces use; wide glyphs are the status dot only,
32
+ * which we place at a fixed column). */
33
+ export function pad(text: string, width: number): string {
34
+ return text.length >= width ? text : text + ' '.repeat(width - text.length);
35
+ }
36
+
37
+ /** Build a `label value` posture cell for the header line. */
38
+ export function kv(label: string, value: string | number): string {
39
+ return `${label} ${value}`;
40
+ }
41
+
42
+ /** Join posture cells into one header line with a consistent separator. */
43
+ export function postureLine(cells: string[]): string {
44
+ return cells.join(' ');
45
+ }
46
+
47
+ /**
48
+ * A stable non-selectable informational row (section title, empty-state copy,
49
+ * or a content line the surface renders but the cursor never lands on). `id`
50
+ * must be unique within its tab so the host's live-value overlay keys off it.
51
+ */
52
+ export function infoRow(id: string, label: string, style?: ModalSectionStyle): ConfigModalRow {
53
+ return { id, label, selectable: false, ...(style ? { style } : {}) };
54
+ }
@@ -0,0 +1,36 @@
1
+ import { resolveUiTones } from '../../renderer/theme.ts';
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Semantic tone tokens for the W6.1 group-B config modals.
5
+ //
6
+ // Derived entirely from the chrome token set (resolveUiTones) so the builders
7
+ // carry NO raw hex color literals (architecture: no-raw-hex-literal-growth).
8
+ // Mode is fixed to 'dark' — the single read path the whole renderer uses until
9
+ // the terminal-bg-probe lands (see renderer/modal-factory.ts, which resolves
10
+ // the same way).
11
+ // ---------------------------------------------------------------------------
12
+
13
+ const T = resolveUiTones('dark');
14
+
15
+ export const MODAL_TONES = {
16
+ /** Informational / accent (blue). */
17
+ info: T.state.info,
18
+ /** Positive / healthy (green). */
19
+ good: T.state.good,
20
+ /** Caution (amber). */
21
+ warn: T.state.warn,
22
+ /** Error / danger (red). */
23
+ bad: T.state.bad,
24
+ /** Reasoning / secondary accent (purple). */
25
+ reasoning: T.state.reasoning,
26
+ /** Muted secondary text. */
27
+ muted: T.fg.muted,
28
+ /** Dim tertiary text. */
29
+ dim: T.fg.dim,
30
+ /** Primary (near-white) text. */
31
+ primary: T.fg.primary,
32
+ /** QR module foreground — high-contrast dark, not pure black. */
33
+ qrDark: T.fg.inverse,
34
+ /** QR field background — high-contrast light, not pure white. */
35
+ qrLight: T.fg.primary,
36
+ } as const;
@@ -0,0 +1,144 @@
1
+ import { MODAL_TONES } from './modal-theme.ts';
2
+ import { infoRow } from './modal-surface-helpers.ts';
3
+ import type {
4
+ ConfigModalActionContext,
5
+ ConfigModalRow,
6
+ ConfigModalSurface,
7
+ ConfigModalView,
8
+ } from '../../input/config-modal-types.ts';
9
+ import { encodeConnectionPayload, generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // QR Code → 'pairing' config-modal surface (W6.1 group-B port). Connection
13
+ // URL/token/username(/password) plus a scannable QR block — read/navigate only.
14
+ // Token regeneration is a destructive mutation (it invalidates any live
15
+ // companion session) that the panel gated behind an in-panel confirm; per
16
+ // charter that confirm is never folded into a modal, so regenerate routes to a
17
+ // command instead. `getConnectionInfo` is a lazy thunk (loaded once on first
18
+ // buildView, cached) so a missing daemon home degrades honestly rather than
19
+ // throwing at modal-registration time.
20
+ //
21
+ // KNOWN GAP (flagged in the report): src/input/commands/qrcode-runtime.ts only
22
+ // registers `/qrcode` (open); it has no `regenerate` subcommand yet, and this
23
+ // surface cannot observe a regenerated token without being re-opened.
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export interface PairingModalConnectionInfo {
27
+ readonly url: string;
28
+ readonly token: string;
29
+ readonly username: string;
30
+ readonly password?: string;
31
+ readonly version?: string;
32
+ readonly surface?: string;
33
+ }
34
+ export interface PairingModalControlPlaneSnapshot { readonly activeClientIds: readonly string[]; }
35
+ export interface PairingModalReadModel<T> { getSnapshot(): T; }
36
+
37
+ export interface PairingModalDeps {
38
+ /** Lazy connection-info provider — returns null when the daemon/companion
39
+ * token cannot be resolved (honest degraded state instead of a throw). */
40
+ readonly getConnectionInfo: () => PairingModalConnectionInfo | null;
41
+ readonly controlPlaneReadModel?: PairingModalReadModel<PairingModalControlPlaneSnapshot>;
42
+ readonly copyToClipboard?: (text: string) => void;
43
+ }
44
+
45
+ // Fixed-width placeholder — deliberately NOT derived from the real secret's
46
+ // length (mirrors QrPanel.SECRET_MASK), so masking doesn't leak length.
47
+ const SECRET_MASK = '••••••••••••';
48
+
49
+ class PairingModalSurface implements ConfigModalSurface {
50
+ readonly name = 'pairing-modal';
51
+ readonly title = 'Companion Pairing';
52
+ private info: PairingModalConnectionInfo | null | undefined = undefined;
53
+ private revealed = false;
54
+ private requestRender: () => void = () => {};
55
+
56
+ constructor(private readonly deps: PairingModalDeps) {}
57
+
58
+ readonly actions = [
59
+ { key: 'v', id: 'toggleReveal', label: 'reveal token' },
60
+ { key: 'c', id: 'copyToken', label: 'copy token', enabledFor: () => Boolean(this.deps.copyToClipboard) },
61
+ { key: 'r', id: 'regenerate', label: 'regenerate token' },
62
+ ];
63
+
64
+ onOpen(requestRender: () => void): void { this.requestRender = requestRender; }
65
+
66
+ private ensureInfo(): PairingModalConnectionInfo | null {
67
+ if (this.info === undefined) {
68
+ try { this.info = this.deps.getConnectionInfo(); } catch { this.info = null; }
69
+ }
70
+ return this.info;
71
+ }
72
+
73
+ buildView(): ConfigModalView {
74
+ const info = this.ensureInfo();
75
+ if (!info) {
76
+ return { title: 'Companion Pairing', degraded: 'Companion pairing is unavailable — the daemon connection info could not be resolved for this session.', tabs: [{ id: 'pairing', label: 'Pairing', rows: [] }] };
77
+ }
78
+ const { url, token, username, password } = info;
79
+ const displayToken = this.revealed ? token : SECRET_MASK;
80
+ const displayPassword = password !== undefined ? (this.revealed ? password : SECRET_MASK) : undefined;
81
+
82
+ const rows: ConfigModalRow[] = [];
83
+ rows.push(infoRow('intro', 'Scan with the GoodVibes companion app to pair this session.', { dim: true }));
84
+ rows.push(infoRow('url', `URL ${url}`));
85
+ rows.push(infoRow('token', `Token ${displayToken}`, { fg: MODAL_TONES.reasoning }));
86
+ rows.push(infoRow('username', `Username ${username}`));
87
+ if (displayPassword !== undefined) rows.push(infoRow('password', `Password ${displayPassword}`));
88
+ if (this.deps.controlPlaneReadModel) {
89
+ const connected = this.deps.controlPlaneReadModel.getSnapshot().activeClientIds.length;
90
+ rows.push(infoRow('companions', `Companions connected: ${connected}`, connected > 0 ? { fg: MODAL_TONES.good } : { dim: true }));
91
+ }
92
+
93
+ // ASCII QR block — same payload shape as QrPanel.render(); the multi-line
94
+ // preformatted string is split into one non-selectable row per block row.
95
+ const payload = encodeConnectionPayload({
96
+ url, token, username,
97
+ ...(password !== undefined ? { password } : {}),
98
+ version: info.version ?? '0.0.0',
99
+ surface: info.surface ?? 'tui',
100
+ });
101
+ const qr = renderQrToString(generateQrMatrix(payload));
102
+ qr.split('\n').forEach((line, i) => rows.push(infoRow(`qr:${i}`, line, { fg: MODAL_TONES.qrDark, bg: MODAL_TONES.qrLight })));
103
+
104
+ return {
105
+ title: 'Companion Pairing',
106
+ tabs: [{ id: 'pairing', label: 'Pairing', rows, hints: [this.revealed ? 'v hide token' : 'v reveal token'] }],
107
+ };
108
+ }
109
+
110
+ onAction(id: string, ctx: ConfigModalActionContext): void {
111
+ const info = this.ensureInfo();
112
+ if (id === 'toggleReveal') { this.revealed = !this.revealed; this.requestRender(); return; }
113
+ if (id === 'copyToken') {
114
+ if (!this.deps.copyToClipboard || !info) { ctx.print('Clipboard not available.'); return; }
115
+ this.deps.copyToClipboard(info.token);
116
+ ctx.print('Token copied to clipboard.');
117
+ return;
118
+ }
119
+ if (id === 'regenerate') {
120
+ void ctx.executeCommand?.('qrcode', ['regenerate']);
121
+ ctx.setStatus('Dispatched /qrcode regenerate (reopen to see the new token).');
122
+ }
123
+ }
124
+ }
125
+
126
+ export function createPairingModalSurface(deps: PairingModalDeps): ConfigModalSurface {
127
+ return new PairingModalSurface(deps);
128
+ }
129
+
130
+ /**
131
+ * Deterministic golden fixture: a frozen connectionInfo literal (no real
132
+ * token/QR generation call — encodeConnectionPayload/generateQrMatrix are pure
133
+ * functions of this literal) and a frozen control-plane snapshot.
134
+ */
135
+ export function pairingModalGoldenSurface(): ConfigModalSurface {
136
+ const connectionInfo: PairingModalConnectionInfo = {
137
+ url: 'http://192.168.1.50:3141', token: 'golden-token-0123456789abcdef', username: 'golden-user', password: 'golden-pass', version: '0.0.0', surface: 'tui',
138
+ };
139
+ return createPairingModalSurface({
140
+ getConnectionInfo: () => connectionInfo,
141
+ controlPlaneReadModel: { getSnapshot: () => ({ activeClientIds: ['golden-client-1'] }) },
142
+ copyToClipboard: () => {},
143
+ });
144
+ }