@pellux/goodvibes-tui 0.18.13 → 0.18.18

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 (71) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +3 -2
  5. package/src/daemon/cli.ts +82 -6
  6. package/src/input/command-registry.ts +2 -0
  7. package/src/input/commands/control-room-runtime.ts +1 -1
  8. package/src/input/commands/health-runtime.ts +1 -1
  9. package/src/input/commands/local-setup-review.ts +1 -1
  10. package/src/input/commands/platform-access-runtime.ts +1 -1
  11. package/src/input/commands/qrcode-runtime.ts +20 -0
  12. package/src/input/commands/subscription-runtime.ts +1 -1
  13. package/src/input/commands.ts +2 -0
  14. package/src/input/handler-feed.ts +6 -0
  15. package/src/input/handler-modal-routes.ts +19 -2
  16. package/src/input/handler-modal-token-routes.ts +3 -0
  17. package/src/input/handler-picker-routes.ts +4 -2
  18. package/src/input/model-picker.ts +11 -0
  19. package/src/input/settings-modal.ts +31 -3
  20. package/src/panels/agent-logs-panel.ts +23 -24
  21. package/src/panels/base-panel.ts +6 -0
  22. package/src/panels/builtin/session.ts +66 -0
  23. package/src/panels/builtin/shared.ts +1 -1
  24. package/src/panels/provider-account-snapshot.ts +1 -1
  25. package/src/panels/provider-accounts-panel.ts +23 -27
  26. package/src/panels/qr-panel.ts +182 -0
  27. package/src/panels/scrollable-list-panel.ts +407 -0
  28. package/src/panels/services-panel.ts +1 -1
  29. package/src/panels/subscription-panel.ts +1 -1
  30. package/src/panels/types.ts +6 -0
  31. package/src/panels/worktree-panel.ts +20 -19
  32. package/src/renderer/buffer.ts +19 -0
  33. package/src/renderer/compositor.ts +19 -6
  34. package/src/renderer/panel-composite.ts +24 -3
  35. package/src/renderer/qr-renderer.ts +117 -0
  36. package/src/renderer/settings-modal-helpers.ts +122 -0
  37. package/src/renderer/settings-modal.ts +147 -111
  38. package/src/runtime/bootstrap-command-context.ts +1 -1
  39. package/src/runtime/bootstrap-command-parts.ts +31 -15
  40. package/src/runtime/bootstrap-core.ts +23 -1
  41. package/src/runtime/bootstrap.ts +6 -1
  42. package/src/runtime/diagnostics/panels/index.ts +5 -5
  43. package/src/runtime/services.ts +1 -1
  44. package/src/runtime/store/domains/domain-read-matrix.ts +0 -2
  45. package/src/runtime/ui-events.ts +1 -46
  46. package/src/runtime/ui-read-model-helpers.ts +1 -32
  47. package/src/runtime/ui-read-models-observability-maintenance.ts +1 -81
  48. package/src/runtime/ui-read-models-observability-options.ts +1 -5
  49. package/src/runtime/ui-read-models-observability-remote.ts +1 -73
  50. package/src/runtime/ui-read-models-observability-security.ts +1 -172
  51. package/src/runtime/ui-read-models-observability-system.ts +1 -217
  52. package/src/runtime/ui-read-models-observability.ts +1 -59
  53. package/src/runtime/ui-service-queries.ts +1 -114
  54. package/src/version.ts +1 -1
  55. package/src/config/service-registry.ts +0 -1
  56. package/src/config/subscription-providers.ts +0 -1
  57. package/src/runtime/diagnostics/actions.ts +0 -776
  58. package/src/runtime/diagnostics/index.ts +0 -99
  59. package/src/runtime/diagnostics/panels/agents.ts +0 -252
  60. package/src/runtime/diagnostics/panels/events.ts +0 -188
  61. package/src/runtime/diagnostics/panels/health.ts +0 -242
  62. package/src/runtime/diagnostics/panels/tasks.ts +0 -251
  63. package/src/runtime/diagnostics/panels/tool-calls.ts +0 -267
  64. package/src/runtime/diagnostics/provider.ts +0 -262
  65. package/src/runtime/store/domains/conversation.ts +0 -1
  66. package/src/runtime/store/domains/permissions.ts +0 -1
  67. package/src/runtime/store/helpers/reducers/conversation.ts +0 -1
  68. package/src/runtime/store/helpers/reducers/lifecycle.ts +0 -1
  69. package/src/runtime/store/helpers/reducers/shared.ts +0 -60
  70. package/src/runtime/store/helpers/reducers/sync.ts +0 -555
  71. package/src/runtime/store/helpers/reducers.ts +0 -30
@@ -0,0 +1,117 @@
1
+ import type { Line } from '../types/grid.ts';
2
+ import { createEmptyLine, createStyledCell } from '../types/grid.ts';
3
+ import { getDisplayWidth } from '../utils/terminal-width.ts';
4
+ import { generateQrMatrix } from '@pellux/goodvibes-sdk/platform/pairing/qr-generator';
5
+
6
+ export { generateQrMatrix };
7
+
8
+ /**
9
+ * Render a QR boolean matrix to terminal Lines using Unicode half-block characters.
10
+ *
11
+ * Two matrix rows map to one terminal row:
12
+ * top=dark, bottom=dark → '█' (FULL BLOCK)
13
+ * top=dark, bottom=light → '▀' (UPPER HALF BLOCK)
14
+ * top=light, bottom=dark → '▄' (LOWER HALF BLOCK)
15
+ * top=light, bottom=light → ' ' (SPACE)
16
+ *
17
+ * @param modules - 2D boolean matrix where true = dark module
18
+ * @param width - Terminal width available for centering
19
+ * @param options - Optional fg/bg overrides
20
+ */
21
+ export function renderQrMatrix(
22
+ modules: readonly boolean[][],
23
+ width: number,
24
+ options?: { fg?: string; bg?: string },
25
+ ): Line[] {
26
+ const fg = options?.fg ?? '#000000';
27
+ const bg = options?.bg ?? '#ffffff';
28
+
29
+ const rows = modules.length;
30
+ const cols = modules[0]?.length ?? 0;
31
+ if (rows === 0 || cols === 0) return [];
32
+
33
+ // Each terminal row covers two matrix rows
34
+ const terminalRows = Math.ceil(rows / 2);
35
+ // Left-align with a single-cell indent. Visually aligns with the text above
36
+ // the QR when rendered with half-block characters; bumping higher
37
+ // mis-registers the finder patterns by a visible unit.
38
+ const leftPad = 1;
39
+
40
+ const lines: Line[] = [];
41
+
42
+ // Prepend a single-row top quiet band of bg so the QR's first module row
43
+ // does not butt up against whatever chrome precedes it. Combined with the
44
+ // leftPad=1 on the horizontal axis, this keeps the finder-pattern square
45
+ // margin consistent on both axes.
46
+ {
47
+ const topBand = createEmptyLine(width);
48
+ const endCol = Math.min(leftPad + cols + 1, width);
49
+ for (let col = 0; col < endCol; col++) {
50
+ topBand[col] = createStyledCell(' ', { fg, bg });
51
+ }
52
+ lines.push(topBand);
53
+ }
54
+
55
+ for (let termRow = 0; termRow < terminalRows; termRow++) {
56
+ const matrixRowTop = termRow * 2;
57
+ const matrixRowBot = termRow * 2 + 1;
58
+ const topRow = modules[matrixRowTop];
59
+ const botRow = matrixRowBot < rows ? modules[matrixRowBot] : null;
60
+
61
+ const line = createEmptyLine(width);
62
+
63
+ // Fill leading padding with bg
64
+ for (let col = 0; col < leftPad && col < width; col++) {
65
+ line[col] = createStyledCell(' ', { fg, bg });
66
+ }
67
+
68
+ // Render QR columns
69
+ for (let col = 0; col < cols; col++) {
70
+ const termCol = leftPad + col;
71
+ if (termCol >= width) break;
72
+
73
+ const topDark = topRow ? (topRow[col] ?? false) : false;
74
+ const botDark = botRow ? (botRow[col] ?? false) : false;
75
+
76
+ let char: string;
77
+ let cellFg: string;
78
+ let cellBg: string;
79
+
80
+ if (topDark && botDark) {
81
+ char = '█';
82
+ cellFg = fg;
83
+ cellBg = bg;
84
+ } else if (topDark && !botDark) {
85
+ char = '▀';
86
+ cellFg = fg;
87
+ cellBg = bg;
88
+ } else if (!topDark && botDark) {
89
+ char = '▄';
90
+ cellFg = fg;
91
+ cellBg = bg;
92
+ } else {
93
+ char = ' ';
94
+ cellFg = fg;
95
+ cellBg = bg;
96
+ }
97
+
98
+ // Some terminals may not render block chars at full width — guard
99
+ const charWidth = getDisplayWidth(char);
100
+ if (charWidth <= 0) {
101
+ line[termCol] = createStyledCell(' ', { fg: cellFg, bg: cellBg });
102
+ } else {
103
+ line[termCol] = createStyledCell(char, { fg: cellFg, bg: cellBg });
104
+ }
105
+ }
106
+
107
+ // Fill trailing with bg up to end of QR block
108
+ for (let col = leftPad + cols; col < leftPad + cols + 1 && col < width; col++) {
109
+ line[col] = createStyledCell(' ', { fg, bg });
110
+ }
111
+
112
+ lines.push(line);
113
+ }
114
+
115
+ return lines;
116
+ }
117
+
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Pure formatting, label, and color helpers for renderSettingsModal.
3
+ * Extracted from settings-modal.ts to keep the renderer under the 800-line
4
+ * architecture cap. No layout logic lives here.
5
+ */
6
+
7
+ import type { SettingEntry, McpEntry, SubscriptionEntry } from '../input/settings-modal.ts';
8
+ import { SETTINGS_CATEGORIES } from '../input/settings-modal.ts';
9
+
10
+ export function formatValue(entry: SettingEntry): string {
11
+ const val = entry.currentValue;
12
+ if (val === null || val === undefined) return '(unset)';
13
+ if (typeof val === 'boolean') return val ? 'true' : 'false';
14
+ if (typeof val === 'string' && val === '') return '(empty)';
15
+ return String(val);
16
+ }
17
+
18
+ export function valueColor(entry: SettingEntry): string {
19
+ if (!entry.isDefault) return '#00ffcc'; // cyan-green = modified
20
+ return '244'; // dim = default
21
+ }
22
+
23
+ export function flagStateColor(state: string, killed: boolean): string {
24
+ if (killed) return '#ef4444'; // red
25
+ if (state === 'enabled') return '#00ffcc'; // cyan-green
26
+ return '244'; // dim
27
+ }
28
+
29
+ export function mcpTrustColor(mode: McpEntry['trustMode']): string {
30
+ switch (mode) {
31
+ case 'allow-all':
32
+ return '#ef4444';
33
+ case 'ask-on-risk':
34
+ return '#eab308';
35
+ case 'constrained':
36
+ return '#00ffcc';
37
+ case 'blocked':
38
+ return '244';
39
+ default:
40
+ return '244';
41
+ }
42
+ }
43
+
44
+ export function subscriptionStateColor(state: SubscriptionEntry['state']): string {
45
+ switch (state) {
46
+ case 'active':
47
+ return '#00ffcc';
48
+ case 'pending':
49
+ return '#eab308';
50
+ case 'available':
51
+ return '#38bdf8';
52
+ default:
53
+ return '244';
54
+ }
55
+ }
56
+
57
+ export function inferSubscriptionRouteReason(entry: SubscriptionEntry): string | undefined {
58
+ if (entry.routeReason?.trim()) return entry.routeReason;
59
+ if (entry.state === 'active' && entry.oauthConfigured) {
60
+ return 'ambient key override enabled for this provider.';
61
+ }
62
+ if (entry.state === 'pending' && entry.oauthConfigured) {
63
+ return 'oauth configuration present; ambient key override will apply after activation.';
64
+ }
65
+ return undefined;
66
+ }
67
+
68
+ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], string> = {
69
+ display: 'Display',
70
+ ui: 'UI',
71
+ provider: 'Provider',
72
+ subscriptions: 'Subscriptions',
73
+ behavior: 'Behavior',
74
+ storage: 'Storage',
75
+ permissions: 'Permissions',
76
+ mcp: 'MCP',
77
+ sandbox: 'Sandbox',
78
+ danger: 'Danger',
79
+ tools: 'Tools',
80
+ flags: 'Flags',
81
+ };
82
+
83
+ export const SETTING_LABELS: Partial<Record<string, string>> = {
84
+ 'ui.systemMessages': 'System Message Target',
85
+ 'ui.operationalMessages': 'Operational Message Target',
86
+ 'ui.wrfcMessages': 'WRFC Message Target',
87
+ 'ui.voiceEnabled': 'Voice Surface',
88
+ 'behavior.autoCompactThreshold': 'Auto-Compact %',
89
+ 'behavior.staleContextWarnings': 'Context Warnings',
90
+ 'behavior.returnContextMode': 'Return Context',
91
+ 'behavior.guidanceMode': 'Guidance Mode',
92
+ 'storage.secretPolicy': 'Secret Policy',
93
+ 'sandbox.vmBackend': 'Sandbox Backend',
94
+ 'sandbox.qemuBinary': 'QEMU Binary',
95
+ 'sandbox.qemuImagePath': 'QEMU Image',
96
+ 'sandbox.qemuExecWrapper': 'QEMU Wrapper',
97
+ 'tools.llmProvider': 'Tool LLM Provider',
98
+ 'tools.llmModel': 'Tool LLM Model',
99
+ 'tools.autoHeal': 'Auto-Heal',
100
+ 'tools.defaultTokenBudget': 'Default Token Budget',
101
+ 'tools.hooksFile': 'Hooks File',
102
+ 'helper.enabled': 'Helper Enabled',
103
+ 'helper.globalProvider': 'Helper Provider',
104
+ 'helper.globalModel': 'Helper Model',
105
+ };
106
+
107
+ export function getSettingLabel(entry: SettingEntry): string {
108
+ return SETTING_LABELS[entry.setting.key] ?? entry.setting.key.replace(/^[^.]+\./, '');
109
+ }
110
+
111
+ export function describeUiRouting(value: string): string {
112
+ switch (value) {
113
+ case 'panel':
114
+ return 'render in panels only';
115
+ case 'conversation':
116
+ return 'render inline in conversation';
117
+ case 'both':
118
+ return 'render in both conversation and panels';
119
+ default:
120
+ return value;
121
+ }
122
+ }
@@ -17,116 +17,17 @@ import { SETTINGS_CATEGORIES } from '../input/settings-modal.ts';
17
17
  import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
18
18
  import { getOverlaySurfaceMetrics, getStableOverlayContentRows } from './overlay-viewport.ts';
19
19
  import { getVisibleWindow } from './surface-layout.ts';
20
-
21
- // ---------------------------------------------------------------------------
22
- // Helpers
23
- // ---------------------------------------------------------------------------
24
-
25
- function formatValue(entry: SettingEntry): string {
26
- const val = entry.currentValue;
27
- if (val === null || val === undefined) return '(unset)';
28
- if (typeof val === 'boolean') return val ? 'true' : 'false';
29
- if (typeof val === 'string' && val === '') return '(empty)';
30
- return String(val);
31
- }
32
-
33
- function valueColor(entry: SettingEntry): string {
34
- if (!entry.isDefault) return '#00ffcc'; // cyan-green = modified
35
- return '244'; // dim = default
36
- }
37
-
38
- function flagStateColor(state: string, killed: boolean): string {
39
- if (killed) return '#ef4444'; // red
40
- if (state === 'enabled') return '#00ffcc'; // cyan-green
41
- return '244'; // dim
42
- }
43
-
44
- function mcpTrustColor(mode: McpEntry['trustMode']): string {
45
- switch (mode) {
46
- case 'allow-all':
47
- return '#ef4444';
48
- case 'ask-on-risk':
49
- return '#eab308';
50
- case 'constrained':
51
- return '#00ffcc';
52
- case 'blocked':
53
- return '244';
54
- default:
55
- return '244';
56
- }
57
- }
58
-
59
- function subscriptionStateColor(state: SubscriptionEntry['state']): string {
60
- switch (state) {
61
- case 'active':
62
- return '#00ffcc';
63
- case 'pending':
64
- return '#eab308';
65
- case 'available':
66
- return '#38bdf8';
67
- default:
68
- return '244';
69
- }
70
- }
71
-
72
- function inferSubscriptionRouteReason(entry: SubscriptionEntry): string | undefined {
73
- if (entry.routeReason?.trim()) return entry.routeReason;
74
- if (entry.state === 'active' && entry.oauthConfigured) {
75
- return 'ambient key override enabled for this provider.';
76
- }
77
- if (entry.state === 'pending' && entry.oauthConfigured) {
78
- return 'oauth configuration present; ambient key override will apply after activation.';
79
- }
80
- return undefined;
81
- }
82
-
83
- const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], string> = {
84
- display: 'Display',
85
- ui: 'UI',
86
- provider: 'Provider',
87
- subscriptions: 'Subscriptions',
88
- behavior: 'Behavior',
89
- storage: 'Storage',
90
- permissions: 'Permissions',
91
- mcp: 'MCP',
92
- sandbox: 'Sandbox',
93
- danger: 'Danger',
94
- tools: 'Tools',
95
- flags: 'Flags',
96
- };
97
-
98
- const SETTING_LABELS: Partial<Record<string, string>> = {
99
- 'ui.systemMessages': 'System Message Target',
100
- 'ui.operationalMessages': 'Operational Message Target',
101
- 'ui.wrfcMessages': 'WRFC Message Target',
102
- 'ui.voiceEnabled': 'Voice Surface',
103
- 'behavior.autoCompactThreshold': 'Auto-Compact %',
104
- 'behavior.staleContextWarnings': 'Context Warnings',
105
- 'behavior.returnContextMode': 'Return Context',
106
- 'behavior.guidanceMode': 'Guidance Mode',
107
- 'storage.secretPolicy': 'Secret Policy',
108
- 'sandbox.vmBackend': 'Sandbox Backend',
109
- 'sandbox.qemuBinary': 'QEMU Binary',
110
- 'sandbox.qemuImagePath': 'QEMU Image',
111
- 'sandbox.qemuExecWrapper': 'QEMU Wrapper',
112
- };
113
-
114
- function getSettingLabel(entry: SettingEntry): string {
115
- return SETTING_LABELS[entry.setting.key] ?? entry.setting.key.replace(/^[^.]+\./, '');
116
- }
117
-
118
- function describeUiRouting(value: string): string {
119
- switch (value) {
120
- case 'panel':
121
- return 'render in panels only';
122
- case 'conversation':
123
- return 'render inline in conversation';
124
- case 'both':
125
- return 'render in both conversation and panels';
126
- default:
127
- return value;
128
- }
129
- }
20
+ import {
21
+ formatValue,
22
+ valueColor,
23
+ flagStateColor,
24
+ mcpTrustColor,
25
+ subscriptionStateColor,
26
+ inferSubscriptionRouteReason,
27
+ CATEGORY_LABELS,
28
+ getSettingLabel,
29
+ describeUiRouting,
30
+ } from './settings-modal-helpers.ts';
130
31
 
131
32
  // ---------------------------------------------------------------------------
132
33
  // Renderer
@@ -161,6 +62,7 @@ export function renderSettingsModal(
161
62
  const isSubscriptionsTab = SETTINGS_CATEGORIES[modal.categoryIndex] === 'subscriptions';
162
63
  const isFlagsTab = SETTINGS_CATEGORIES[modal.categoryIndex] === 'flags';
163
64
  const isUiTab = SETTINGS_CATEGORIES[modal.categoryIndex] === 'ui';
65
+ const isToolsTab = SETTINGS_CATEGORIES[modal.categoryIndex] === 'tools';
164
66
  let persistentHelpers: import('./modal-factory.ts').ModalHelperRow[] | undefined;
165
67
  sections.push({
166
68
  type: 'text',
@@ -174,7 +76,9 @@ export function renderSettingsModal(
174
76
  ? 'Control shell presentation, including where operational and WRFC updates render across conversation and panels.'
175
77
  : isFlagsTab
176
78
  ? 'Feature flags control staged or experimental behavior. Some changes may require restart.'
177
- : 'Browse and adjust operator-facing runtime settings by category.',
79
+ : isToolsTab
80
+ ? 'Configure tool LLM routing and helper model. Provider and model fields are optional — empty means use the active provider.'
81
+ : 'Browse and adjust operator-facing runtime settings by category.',
178
82
  style: { fg: '246', dim: true },
179
83
  });
180
84
 
@@ -529,6 +433,138 @@ export function renderSettingsModal(
529
433
  );
530
434
  }
531
435
 
436
+ // ── Tools tab ─────────────────────────────────────────────────
437
+ if (isToolsTab) {
438
+ const toolsItems = modal.currentItems; // includes helper.* entries routed into tools group
439
+
440
+ if (toolsItems.length === 0) {
441
+ sections.push({
442
+ type: 'text',
443
+ content: '(no tool or helper settings available)',
444
+ style: { fg: '240', dim: true },
445
+ });
446
+ } else {
447
+ const labelW = Math.floor(contentW * 0.38);
448
+ const valW = Math.floor(contentW * 0.30);
449
+ const srcW = Math.max(0, contentW - labelW - valW - 4);
450
+
451
+ sections.push({
452
+ type: 'text',
453
+ content: `${fitDisplay('Setting', labelW)} ${fitDisplay('Value', valW)} Source`,
454
+ style: { fg: '240', dim: true },
455
+ });
456
+ sections.push({ type: 'separator' });
457
+
458
+ const window = getVisibleWindow(toolsItems.length, modal.selectedIndex, maxVisibleRows);
459
+ const visibleItems = toolsItems.slice(window.start, window.end);
460
+
461
+ // Render each entry as an individual text row so section headers can interleave.
462
+ // Section headers are emitted when the key prefix changes (tools.* vs helper.*).
463
+ let lastGroupPrefix = '';
464
+ for (let i = 0; i < visibleItems.length; i++) {
465
+ const entry = visibleItems[i]!;
466
+ const isSelected = window.start + i === modal.selectedIndex;
467
+ const isEditing = isSelected && modal.editingMode;
468
+ const prefix = entry.setting.key.split('.')[0]!;
469
+
470
+ // Emit a section header when the group prefix changes
471
+ if (prefix !== lastGroupPrefix) {
472
+ lastGroupPrefix = prefix;
473
+ const sectionLabel = prefix === 'helper' ? '── Helper Model ──' : '── Tool LLM ──';
474
+ sections.push({
475
+ type: 'text',
476
+ content: fitDisplay(sectionLabel, contentW),
477
+ style: { fg: '243', dim: true },
478
+ });
479
+ }
480
+
481
+ const label = getSettingLabel(entry);
482
+ const labelStr = fitDisplay(label, labelW);
483
+
484
+ let valueStr: string;
485
+ if (entry.setting.type === 'boolean') {
486
+ const boolVal = Boolean(entry.currentValue);
487
+ valueStr = isEditing ? `${modal.editBuffer}\u2588` : (boolVal ? '[on]' : '[off]');
488
+ } else if (isEditing) {
489
+ valueStr = `${modal.editBuffer}\u2588`;
490
+ } else {
491
+ valueStr = formatValue(entry);
492
+ }
493
+
494
+ const valStr = fitDisplay(valueStr, valW);
495
+ const sourceText = entry.effectiveSource ?? 'default';
496
+ const srcStr = fitDisplay(sourceText, srcW);
497
+ const rowLabel = `${labelStr} ${valStr} ${srcStr}`;
498
+
499
+ // Render as a single-item list so the ModalFactory applies selection highlight
500
+ sections.push({
501
+ type: 'list',
502
+ items: [{
503
+ label: rowLabel,
504
+ selected: isSelected,
505
+ style: isSelected ? undefined : { fg: valueColor(entry) },
506
+ }],
507
+ });
508
+ }
509
+
510
+ if (toolsItems.length > maxVisibleRows) {
511
+ sections.push({
512
+ type: 'text',
513
+ content: `[${window.start + 1}-${window.end} of ${toolsItems.length}]`,
514
+ style: { fg: '244', dim: true },
515
+ });
516
+ }
517
+
518
+ // Description of selected entry
519
+ const selected = modal.getSelected();
520
+ if (selected) {
521
+ sections.push({ type: 'separator' });
522
+ sections.push({
523
+ type: 'text',
524
+ content: truncateDisplay(selected.setting.description, contentW),
525
+ style: { fg: '246', dim: true },
526
+ });
527
+ if (selected.setting.type === 'boolean') {
528
+ sections.push({
529
+ type: 'text',
530
+ content: truncateDisplay(`Currently ${Boolean(selected.currentValue) ? 'enabled' : 'disabled'}. Press Enter or Space to toggle.`, contentW),
531
+ style: { fg: '#38bdf8', dim: true },
532
+ });
533
+ } else {
534
+ const emptyNote = selected.currentValue === '' || selected.currentValue === null || selected.currentValue === undefined
535
+ ? ' (empty = use active provider default)'
536
+ : '';
537
+ sections.push({
538
+ type: 'text',
539
+ content: truncateDisplay(`Current: ${formatValue(selected)}${emptyNote}`, contentW),
540
+ style: { fg: '#38bdf8', dim: true },
541
+ });
542
+ }
543
+ }
544
+ }
545
+
546
+ const hints = modal.editingMode
547
+ ? ['[Enter] Confirm', '[Esc] Cancel']
548
+ : ['[Tab] Category', '[\u2191\u2193] Navigate', '[Enter] Toggle / Edit', '[Esc] Close'];
549
+
550
+ return ModalFactory.createModal(
551
+ {
552
+ title: 'Settings',
553
+ width: boxW,
554
+ margin: boxMargin,
555
+ targetContentRows,
556
+ tabs: SETTINGS_CATEGORIES.map((category, index) => ({
557
+ label: CATEGORY_LABELS[category],
558
+ active: index === modal.categoryIndex,
559
+ })),
560
+ sections,
561
+ hints,
562
+ helpers: persistentHelpers,
563
+ },
564
+ width,
565
+ );
566
+ }
567
+
532
568
  // ── Settings list ────────────────────────────────────────────
533
569
  const items = modal.currentItems;
534
570
 
@@ -82,7 +82,7 @@ export type CreateBootstrapCommandContextOptions = {
82
82
  providerApi?: ProviderApi;
83
83
  subscriptionManager?: import('@pellux/goodvibes-sdk/platform/config/subscriptions').SubscriptionManager;
84
84
  secretsManager?: import('../config/secrets.ts').SecretsManager;
85
- serviceRegistry?: import('../config/service-registry.ts').ServiceRegistry;
85
+ serviceRegistry?: import('@pellux/goodvibes-sdk/platform/config/service-registry').ServiceRegistry;
86
86
  localUserAuthManager?: import('@pellux/goodvibes-sdk/platform/security/user-auth').UserAuthManager;
87
87
  tokenAuditor?: import('@pellux/goodvibes-sdk/platform/security/token-audit').ApiTokenAuditor;
88
88
  replayEngine?: import('@pellux/goodvibes-sdk/platform/core/deterministic-replay').DeterministicReplayEngine;
@@ -92,7 +92,7 @@ export interface BootstrapCommandSectionOptions {
92
92
  readonly benchmarkStore?: import('@pellux/goodvibes-sdk/platform/providers/model-benchmarks').BenchmarkStore;
93
93
  readonly subscriptionManager?: import('@pellux/goodvibes-sdk/platform/config/subscriptions').SubscriptionManager;
94
94
  readonly secretsManager?: import('../config/secrets.ts').SecretsManager;
95
- readonly serviceRegistry?: import('../config/service-registry.ts').ServiceRegistry;
95
+ readonly serviceRegistry?: import('@pellux/goodvibes-sdk/platform/config/service-registry').ServiceRegistry;
96
96
  readonly localUserAuthManager?: import('@pellux/goodvibes-sdk/platform/security/user-auth').UserAuthManager;
97
97
  readonly tokenAuditor?: import('@pellux/goodvibes-sdk/platform/security/token-audit').ApiTokenAuditor;
98
98
  readonly replayEngine?: import('@pellux/goodvibes-sdk/platform/core/deterministic-replay').DeterministicReplayEngine;
@@ -182,25 +182,41 @@ export function createBootstrapCommandActions(
182
182
  clearScreen: () => unwiredShellAction('clearScreen'),
183
183
  activatePlan,
184
184
  requestPermission: (request) => requestPermission(request),
185
- completeModelSelection: ({ model, effort, contextCap }) => {
185
+ completeModelSelection: ({ model, effort, contextCap, target }) => {
186
186
  if (!model) return;
187
187
  const def = model;
188
188
  const key = def.registryKey ?? `${def.provider}:${def.id}`;
189
+ const resolvedTarget = target ?? 'main';
189
190
  try {
190
- if (contextCap != null && contextCap > 0) {
191
- providerRegistry.setModelContextCap(key, contextCap);
191
+ if (resolvedTarget === 'helper') {
192
+ // Write to helper config keys and enable the helper
193
+ configManager.set('helper.globalProvider', def.provider);
194
+ configManager.set('helper.globalModel', key);
195
+ configManager.set('helper.enabled', true);
196
+ conversation.log(`Helper model set to: ${def.displayName} (${def.provider})`, { fg: '135' });
197
+ } else if (resolvedTarget === 'tool') {
198
+ // Write to tool LLM config keys and enable the tool LLM
199
+ configManager.set('tools.llmProvider', def.provider);
200
+ configManager.set('tools.llmModel', key);
201
+ configManager.setDynamic('tools.llmEnabled' as never, true);
202
+ conversation.log(`Tool LLM set to: ${def.displayName} (${def.provider})`, { fg: '135' });
203
+ } else {
204
+ // Default: main provider/model
205
+ if (contextCap != null && contextCap > 0) {
206
+ providerRegistry.setModelContextCap(key, contextCap);
207
+ }
208
+ providerRegistry.setCurrentModel(key);
209
+ runtime.model = key;
210
+ runtime.provider = def.provider;
211
+ runtime.reasoningEffort = effort as 'instant' | 'low' | 'medium' | 'high';
212
+ configManager.set('provider.model', key);
213
+ configManager.set('provider.provider', def.provider);
214
+ configManager.set('provider.reasoningEffort', effort as 'instant' | 'low' | 'medium' | 'high');
215
+ const ctxNote = contextCap != null && contextCap > 0
216
+ ? `, context cap: ${contextCap.toLocaleString()}`
217
+ : '';
218
+ conversation.log(`Switched to model: ${def.displayName} (${def.provider}), effort: ${effort}${ctxNote}`, { fg: '135' });
192
219
  }
193
- providerRegistry.setCurrentModel(key);
194
- runtime.model = key;
195
- runtime.provider = def.provider;
196
- runtime.reasoningEffort = effort as 'instant' | 'low' | 'medium' | 'high';
197
- configManager.set('provider.model', key);
198
- configManager.set('provider.provider', def.provider);
199
- configManager.set('provider.reasoningEffort', effort as 'instant' | 'low' | 'medium' | 'high');
200
- const ctxNote = contextCap != null && contextCap > 0
201
- ? `, context cap: ${contextCap.toLocaleString()}`
202
- : '';
203
- conversation.log(`Switched to model: ${def.displayName} (${def.provider}), effort: ${effort}${ctxNote}`, { fg: '135' });
204
220
  } catch (e) {
205
221
  conversation.log(`Error switching model: ${summarizeError(e)}`, { fg: '#ef4444' });
206
222
  }
@@ -226,8 +226,30 @@ export async function initializeBootstrapCore(
226
226
  });
227
227
 
228
228
  const renderRequestRef = { value: (): void => {} };
229
+ // R1: Coalescing render scheduler — collapses N same-microtask requestRender() calls into 1.
230
+ // Also enforces a 16ms minimum interval to cap at ~60fps during streaming.
231
+ let renderScheduled = false;
232
+ let lastRenderTime = 0;
233
+ const RENDER_INTERVAL_MS = 16;
229
234
  const requestRender = (): void => {
230
- renderRequestRef.value();
235
+ if (renderScheduled) return;
236
+ renderScheduled = true;
237
+ setImmediate(() => {
238
+ renderScheduled = false;
239
+ const now = Date.now();
240
+ const elapsed = now - lastRenderTime;
241
+ if (elapsed < RENDER_INTERVAL_MS) {
242
+ // Too soon — debounce to the tail of the current 16ms window
243
+ const delay = RENDER_INTERVAL_MS - elapsed;
244
+ setTimeout(() => {
245
+ lastRenderTime = Date.now();
246
+ renderRequestRef.value();
247
+ }, delay);
248
+ } else {
249
+ lastRenderTime = now;
250
+ renderRequestRef.value();
251
+ }
252
+ });
231
253
  };
232
254
  const permissionPromptRef = {
233
255
  requestPermission: (async () => ({ approved: false, remember: false })) as PermissionRequestHandler,
@@ -35,6 +35,7 @@ import {
35
35
  import { startBackgroundProviderRegistration } from '@pellux/goodvibes-sdk/platform/runtime/bootstrap-background';
36
36
  import { restoreSavedModel } from '@pellux/goodvibes-sdk/platform/runtime/bootstrap-helpers';
37
37
  import { startExternalServices, type ExternalServicesHandle } from '@pellux/goodvibes-sdk/platform/runtime/bootstrap-services';
38
+ import { getOrCreateCompanionToken } from '@pellux/goodvibes-sdk/platform/pairing/companion-token';
38
39
  import type { UiRuntimeServices } from './ui-services.ts';
39
40
  import { createDeferredStartupCoordinator } from '@pellux/goodvibes-sdk/platform/runtime/deferred-startup';
40
41
  import { initializeBootstrapCore } from './bootstrap-core.ts';
@@ -320,11 +321,15 @@ export async function bootstrapRuntime(
320
321
  deferredStartup.schedule({
321
322
  label: 'external-services',
322
323
  run: async () => {
324
+ // Register the persistent companion-pairing token as the daemon's shared
325
+ // bearer, so tokens scanned from the /qrcode panel's QR actually
326
+ // authenticate against the embedded daemon this surface starts.
327
+ const companionTokenRecord = getOrCreateCompanionToken('tui');
323
328
  externalServicesPromise = startExternalServices(
324
329
  configManager,
325
330
  runtimeBus,
326
331
  hookDispatcher,
327
- {},
332
+ { sharedDaemonToken: companionTokenRecord.token },
328
333
  services,
329
334
  );
330
335
  externalServices = await externalServicesPromise;
@@ -3,13 +3,13 @@
3
3
  *
4
4
  * Import from this module to access the individual diagnostic panel providers.
5
5
  */
6
- export { ToolCallsPanel } from './tool-calls.ts';
7
- export { AgentsPanel } from './agents.ts';
8
- export { TasksPanel } from './tasks.ts';
9
- export { EventsPanel } from './events.ts';
6
+ export { ToolCallsPanel } from '@pellux/goodvibes-sdk/platform/runtime/diagnostics/panels/tool-calls';
7
+ export { AgentsPanel } from '@pellux/goodvibes-sdk/platform/runtime/diagnostics/panels/agents';
8
+ export { TasksPanel } from '@pellux/goodvibes-sdk/platform/runtime/diagnostics/panels/tasks';
9
+ export { EventsPanel } from '@pellux/goodvibes-sdk/platform/runtime/diagnostics/panels/events';
10
10
  export { StateInspectorPanel } from '@pellux/goodvibes-sdk/platform/runtime/diagnostics/panels/state-inspector';
11
11
  export type { InspectableDomain } from '@pellux/goodvibes-sdk/platform/runtime/diagnostics/panels/state-inspector';
12
- export { HealthPanel } from './health.ts';
12
+ export { HealthPanel } from '@pellux/goodvibes-sdk/platform/runtime/diagnostics/panels/health';
13
13
  export { DivergencePanel } from '@pellux/goodvibes-sdk/platform/runtime/diagnostics/panels/divergence';
14
14
  export { ReplayPanel } from '@pellux/goodvibes-sdk/platform/runtime/diagnostics/panels/replay';
15
15
  export { PolicyPanel } from './policy.ts';