@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.
- package/CHANGELOG.md +85 -21
- package/README.md +20 -11
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/conversation-rendering.ts +19 -0
- package/src/core/conversation.ts +18 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/system-message-router.ts +37 -51
- package/src/core/turn-cancellation.ts +20 -0
- package/src/core/turn-event-wiring.ts +64 -3
- package/src/export/cost-utils.ts +36 -0
- package/src/input/command-registry.ts +38 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +1 -1
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +6 -1
- package/src/input/commands/operator-panel-runtime.ts +40 -24
- package/src/input/commands/planning-runtime.ts +26 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +3 -3
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +15 -7
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +338 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +161 -0
- package/src/input/config-modal.ts +377 -0
- package/src/input/feed-context-factory.ts +7 -8
- package/src/input/handler-command-route.ts +27 -17
- package/src/input/handler-feed-routes.ts +61 -23
- package/src/input/handler-feed.ts +47 -23
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +65 -0
- package/src/input/handler-modal-stack.ts +3 -5
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-picker-routes.ts +11 -94
- package/src/input/handler-shortcuts.ts +24 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +6 -28
- package/src/input/keybindings.ts +22 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +58 -0
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/settings-modal-data.ts +95 -0
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/main.ts +24 -27
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +9 -43
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +36 -10
- package/src/panels/diff-panel.ts +12 -43
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-panel.ts +800 -0
- package/src/panels/fleet-read-model.ts +477 -0
- package/src/panels/fleet-steer.ts +92 -0
- package/src/panels/fleet-stop.ts +125 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/index.ts +7 -31
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +151 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +36 -0
- package/src/panels/modals/pairing-modal.ts +144 -0
- package/src/panels/modals/planning-modal.ts +282 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +149 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +108 -5
- package/src/panels/project-planning-answer-actions.ts +4 -2
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +13 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +60 -4
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/config-modal.ts +81 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +10 -2
- package/src/renderer/git-status.ts +40 -0
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/model-workspace.ts +44 -1
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/turn-injection.ts +114 -0
- package/src/runtime/bootstrap-command-context.ts +21 -1
- package/src/runtime/bootstrap-command-parts.ts +34 -7
- package/src/runtime/bootstrap-core.ts +14 -4
- package/src/runtime/bootstrap-shell.ts +29 -16
- package/src/runtime/bootstrap.ts +11 -17
- package/src/runtime/code-index-services.ts +112 -0
- package/src/runtime/orchestrator-core-services.ts +49 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +91 -43
- package/src/runtime/ui-services.ts +8 -0
- package/src/runtime/workstream-services.ts +195 -0
- package/src/shell/blocking-input.ts +22 -1
- package/src/shell/ui-openers.ts +129 -17
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -515
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -566
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -496
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- 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
|
+
}
|