@pellux/goodvibes-tui 0.18.12 → 0.18.17

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 (196) hide show
  1. package/CHANGELOG.md +172 -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/config/index.ts +1 -138
  6. package/src/core/conversation-rendering.ts +3 -3
  7. package/src/core/conversation.ts +176 -423
  8. package/src/core/history.ts +45 -0
  9. package/src/core/orchestrator.ts +3 -735
  10. package/src/core/system-message-router.ts +19 -58
  11. package/src/daemon/cli.ts +82 -6
  12. package/src/input/command-registry.ts +2 -0
  13. package/src/input/commands/control-room-runtime.ts +1 -1
  14. package/src/input/commands/health-runtime.ts +1 -1
  15. package/src/input/commands/local-setup-review.ts +1 -1
  16. package/src/input/commands/platform-access-runtime.ts +1 -1
  17. package/src/input/commands/qrcode-runtime.ts +20 -0
  18. package/src/input/commands/subscription-runtime.ts +1 -1
  19. package/src/input/commands.ts +2 -0
  20. package/src/input/handler-content-actions.ts +2 -2
  21. package/src/input/handler-feed.ts +7 -1
  22. package/src/input/handler-modal-routes.ts +19 -2
  23. package/src/input/handler-modal-token-routes.ts +4 -1
  24. package/src/input/handler-picker-routes.ts +4 -2
  25. package/src/input/handler-ui-state.ts +1 -1
  26. package/src/input/handler.ts +1 -1
  27. package/src/input/model-picker.ts +11 -0
  28. package/src/input/search.ts +1 -1
  29. package/src/input/selection.ts +2 -2
  30. package/src/input/settings-modal.ts +31 -3
  31. package/src/main.ts +1 -1
  32. package/src/panels/agent-inspector-panel.ts +3 -3
  33. package/src/panels/agent-logs-panel.ts +26 -27
  34. package/src/panels/approval-panel.ts +2 -2
  35. package/src/panels/automation-control-panel.ts +3 -3
  36. package/src/panels/base-panel.ts +14 -14
  37. package/src/panels/builtin/operations.ts +1 -1
  38. package/src/panels/builtin/session.ts +67 -1
  39. package/src/panels/builtin/shared.ts +4 -4
  40. package/src/panels/cockpit-panel.ts +2 -2
  41. package/src/panels/communication-panel.ts +3 -3
  42. package/src/panels/context-visualizer-panel.ts +2 -2
  43. package/src/panels/control-plane-panel.ts +3 -3
  44. package/src/panels/cost-tracker-panel.ts +3 -3
  45. package/src/panels/debug-panel.ts +2 -2
  46. package/src/panels/diff-panel.ts +2 -2
  47. package/src/panels/docs-panel.ts +1 -1
  48. package/src/panels/eval-panel.ts +2 -2
  49. package/src/panels/file-explorer-panel.ts +3 -3
  50. package/src/panels/file-preview-panel.ts +3 -3
  51. package/src/panels/forensics-panel.ts +2 -2
  52. package/src/panels/git-panel.ts +1 -1
  53. package/src/panels/hooks-panel.ts +3 -3
  54. package/src/panels/incident-review-panel.ts +1 -1
  55. package/src/panels/intelligence-panel.ts +2 -2
  56. package/src/panels/knowledge-panel.ts +1 -1
  57. package/src/panels/local-auth-panel.ts +2 -2
  58. package/src/panels/marketplace-panel.ts +1 -1
  59. package/src/panels/mcp-panel.ts +3 -3
  60. package/src/panels/memory-panel.ts +1 -1
  61. package/src/panels/ops-control-panel.ts +3 -3
  62. package/src/panels/ops-strategy-panel.ts +2 -2
  63. package/src/panels/orchestration-panel.ts +2 -2
  64. package/src/panels/panel-list-panel.ts +6 -6
  65. package/src/panels/plan-dashboard-panel.ts +1 -1
  66. package/src/panels/plugins-panel.ts +2 -2
  67. package/src/panels/policy-panel.ts +2 -2
  68. package/src/panels/polish.ts +3 -3
  69. package/src/panels/provider-account-snapshot.ts +1 -1
  70. package/src/panels/provider-accounts-panel.ts +25 -29
  71. package/src/panels/provider-health-panel.ts +2 -2
  72. package/src/panels/provider-stats-panel.ts +3 -3
  73. package/src/panels/qr-panel.ts +182 -0
  74. package/src/panels/remote-panel.ts +3 -3
  75. package/src/panels/routes-panel.ts +3 -3
  76. package/src/panels/sandbox-panel.ts +2 -2
  77. package/src/panels/schedule-panel.ts +1 -1
  78. package/src/panels/scrollable-list-panel.ts +407 -0
  79. package/src/panels/security-panel.ts +2 -2
  80. package/src/panels/services-panel.ts +3 -3
  81. package/src/panels/session-browser-panel.ts +2 -2
  82. package/src/panels/settings-sync-panel.ts +2 -2
  83. package/src/panels/skills-panel.ts +6 -6
  84. package/src/panels/subscription-panel.ts +3 -3
  85. package/src/panels/symbol-outline-panel.ts +3 -3
  86. package/src/panels/system-messages-panel.ts +4 -4
  87. package/src/panels/tasks-panel.ts +2 -2
  88. package/src/panels/thinking-panel.ts +3 -3
  89. package/src/panels/token-budget-panel.ts +1 -1
  90. package/src/panels/tool-inspector-panel.ts +3 -3
  91. package/src/panels/types.ts +5 -5
  92. package/src/panels/watchers-panel.ts +3 -3
  93. package/src/panels/welcome-panel.ts +1 -1
  94. package/src/panels/worktree-panel.ts +22 -21
  95. package/src/panels/wrfc-panel.ts +3 -3
  96. package/src/permissions/prompt.ts +3 -22
  97. package/src/plugins/loader.ts +15 -304
  98. package/src/renderer/agent-detail-modal.ts +1 -1
  99. package/src/renderer/autocomplete-overlay.ts +2 -2
  100. package/src/renderer/bookmark-modal.ts +1 -1
  101. package/src/renderer/bottom-bar.ts +2 -2
  102. package/src/renderer/buffer.ts +1 -1
  103. package/src/renderer/code-block.ts +2 -2
  104. package/src/renderer/compositor.ts +2 -2
  105. package/src/renderer/context-inspector.ts +1 -1
  106. package/src/renderer/conversation-layout.ts +2 -2
  107. package/src/renderer/conversation-overlays.ts +1 -1
  108. package/src/renderer/conversation-surface.ts +2 -2
  109. package/src/renderer/diff-view.ts +2 -2
  110. package/src/renderer/diff.ts +1 -1
  111. package/src/renderer/file-picker-overlay.ts +2 -2
  112. package/src/renderer/file-tree.ts +2 -2
  113. package/src/renderer/help-overlay.ts +1 -1
  114. package/src/renderer/history-search-overlay.ts +2 -2
  115. package/src/renderer/live-tail-modal.ts +1 -1
  116. package/src/renderer/markdown.ts +2 -2
  117. package/src/renderer/modal-factory.ts +3 -3
  118. package/src/renderer/model-picker-overlay.ts +2 -2
  119. package/src/renderer/overlay-box.ts +2 -2
  120. package/src/renderer/panel-composite.ts +1 -1
  121. package/src/renderer/panel-picker-overlay.ts +2 -2
  122. package/src/renderer/panel-tab-bar.ts +1 -1
  123. package/src/renderer/panel-workspace-bar.ts +1 -1
  124. package/src/renderer/process-indicator.ts +2 -2
  125. package/src/renderer/process-modal.ts +1 -1
  126. package/src/renderer/profile-picker-modal.ts +2 -2
  127. package/src/renderer/progress.ts +2 -2
  128. package/src/renderer/qr-renderer.ts +117 -0
  129. package/src/renderer/search-overlay.ts +2 -2
  130. package/src/renderer/selection-modal-overlay.ts +2 -2
  131. package/src/renderer/session-picker-modal.ts +2 -2
  132. package/src/renderer/settings-modal-helpers.ts +122 -0
  133. package/src/renderer/settings-modal.ts +149 -113
  134. package/src/renderer/shell-surface.ts +1 -1
  135. package/src/renderer/system-message.ts +1 -1
  136. package/src/renderer/tab-strip.ts +2 -2
  137. package/src/renderer/text-layout.ts +1 -1
  138. package/src/renderer/thinking.ts +1 -1
  139. package/src/renderer/tool-call.ts +2 -2
  140. package/src/renderer/ui-factory.ts +2 -2
  141. package/src/runtime/bootstrap-command-context.ts +5 -6
  142. package/src/runtime/bootstrap-command-parts.ts +32 -18
  143. package/src/runtime/bootstrap-core.ts +3 -2
  144. package/src/runtime/bootstrap-hook-bridge.ts +15 -174
  145. package/src/runtime/bootstrap-shell.ts +4 -4
  146. package/src/runtime/bootstrap.ts +7 -2
  147. package/src/runtime/context.ts +4 -20
  148. package/src/runtime/diagnostics/panels/index.ts +6 -6
  149. package/src/runtime/diagnostics/panels/ops.ts +1 -1
  150. package/src/runtime/diagnostics/panels/panel-resources.ts +118 -0
  151. package/src/runtime/perf/panel-contracts.ts +32 -0
  152. package/src/runtime/perf/panel-health-monitor.ts +18 -0
  153. package/src/runtime/services.ts +5 -5
  154. package/src/runtime/store/domains/domain-read-matrix.ts +0 -2
  155. package/src/runtime/store/selectors/index.ts +11 -6
  156. package/src/runtime/store/state.ts +12 -4
  157. package/src/runtime/ui-events.ts +1 -0
  158. package/src/runtime/ui-read-model-helpers.ts +1 -32
  159. package/src/runtime/ui-read-models-observability-maintenance.ts +1 -81
  160. package/src/runtime/ui-read-models-observability-options.ts +1 -5
  161. package/src/runtime/ui-read-models-observability-remote.ts +1 -73
  162. package/src/runtime/ui-read-models-observability-security.ts +1 -172
  163. package/src/runtime/ui-read-models-observability-system.ts +1 -217
  164. package/src/runtime/ui-read-models-observability.ts +1 -59
  165. package/src/runtime/ui-service-queries.ts +1 -114
  166. package/src/runtime/ui-services.ts +1 -1
  167. package/src/shell/ui-openers.ts +1 -1
  168. package/src/tools/index.ts +1 -186
  169. package/src/types/grid.ts +48 -0
  170. package/src/utils/clipboard.ts +21 -0
  171. package/src/utils/splash-lines.ts +1 -1
  172. package/src/utils/terminal-width.ts +185 -0
  173. package/src/version.ts +1 -1
  174. package/src/config/service-registry.ts +0 -1
  175. package/src/config/subscription-providers.ts +0 -127
  176. package/src/daemon/facade-composition.ts +0 -398
  177. package/src/daemon/facade.ts +0 -638
  178. package/src/daemon/surface-policy.ts +0 -60
  179. package/src/daemon/types.ts +0 -191
  180. package/src/runtime/diagnostics/actions.ts +0 -776
  181. package/src/runtime/diagnostics/index.ts +0 -99
  182. package/src/runtime/diagnostics/panels/agents.ts +0 -252
  183. package/src/runtime/diagnostics/panels/events.ts +0 -188
  184. package/src/runtime/diagnostics/panels/health.ts +0 -242
  185. package/src/runtime/diagnostics/panels/tasks.ts +0 -251
  186. package/src/runtime/diagnostics/panels/tool-calls.ts +0 -267
  187. package/src/runtime/diagnostics/provider.ts +0 -262
  188. package/src/runtime/store/domains/conversation.ts +0 -181
  189. package/src/runtime/store/domains/permissions.ts +0 -143
  190. package/src/runtime/store/helpers/reducers/conversation.ts +0 -228
  191. package/src/runtime/store/helpers/reducers/lifecycle.ts +0 -440
  192. package/src/runtime/store/helpers/reducers/shared.ts +0 -60
  193. package/src/runtime/store/helpers/reducers/sync.ts +0 -555
  194. package/src/runtime/store/helpers/reducers.ts +0 -30
  195. package/src/runtime/ui-read-models-core.ts +0 -95
  196. package/src/runtime/ui-read-models-operations.ts +0 -203
@@ -32,43 +32,19 @@ import { getConfigSnapshot } from '../config/index.ts';
32
32
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config/manager';
33
33
  import type { ConversationManager } from './conversation';
34
34
  import type { SystemMessagesPanel, SystemMessagePriority } from '../panels/system-messages-panel.ts';
35
-
36
- // ---------------------------------------------------------------------------
37
- // Priority classification patterns
38
- // ---------------------------------------------------------------------------
39
-
40
- /**
41
- * Patterns that identify HIGH-priority messages:
42
- * fatal errors, model/provider switches, session save/load, compaction.
43
- */
44
- const HIGH_PRIORITY_RE =
45
- /\bfatal\b|\bcrash\w*|\bunhandled exception\b|\[Model\]|\[Provider\].*switch|\[Session\].*(?:saved|loaded|restored)|\[Compaction\]|\[Recovery\].*Failed/i;
46
-
47
- /**
48
- * Classify a message as high or low priority based on content.
49
- * Used by routeAuto() when the caller doesn't specify priority.
50
- *
51
- * @internal
52
- */
53
- function classifyPriority(message: string): SystemMessagePriority {
54
- return HIGH_PRIORITY_RE.test(message) ? 'high' : 'low';
55
- }
56
-
57
- export type SystemMessageKind = 'system' | 'operational' | 'wrfc';
58
- export type SystemMessageTarget = 'conversation' | 'panel' | 'both';
59
-
60
- function defaultTargetForKind(kind: SystemMessageKind): SystemMessageTarget {
61
- if (kind === 'wrfc') return 'both';
62
- return 'panel';
63
- }
64
-
65
- function classifyKind(message: string): SystemMessageKind {
66
- if (/^\[WRFC\]/i.test(message)) return 'wrfc';
67
- if (/^\[(Scan|Local|Agents|MCP|Plugin|Hook|Tool|Exec|Remote|Bridge|Approval)\]/i.test(message)) {
68
- return 'operational';
69
- }
70
- return 'system';
71
- }
35
+ import {
36
+ classifySystemMessageKind,
37
+ classifySystemMessagePriority,
38
+ defaultSystemMessageTarget,
39
+ resolveSystemMessageDelivery,
40
+ type SystemMessageKind,
41
+ type SystemMessageTarget,
42
+ } from '@pellux/goodvibes-sdk/platform/runtime/system-message-policy';
43
+
44
+ export type {
45
+ SystemMessageKind,
46
+ SystemMessageTarget,
47
+ } from '@pellux/goodvibes-sdk/platform/runtime/system-message-policy';
72
48
 
73
49
  function targetForKind(
74
50
  configManager: Pick<ConfigManager, 'getRaw'>,
@@ -80,21 +56,6 @@ function targetForKind(
80
56
  return ui.systemMessages;
81
57
  }
82
58
 
83
- function resolveDelivery(
84
- target: SystemMessageTarget,
85
- hasPanel: boolean,
86
- ): { readonly toPanel: boolean; readonly toConversation: boolean } {
87
- if (target === 'both') {
88
- return { toPanel: hasPanel, toConversation: true };
89
- }
90
- if (target === 'conversation') {
91
- return { toPanel: false, toConversation: true };
92
- }
93
- return hasPanel
94
- ? { toPanel: true, toConversation: false }
95
- : { toPanel: false, toConversation: true };
96
- }
97
-
98
59
  // ---------------------------------------------------------------------------
99
60
  // SystemMessageRouter
100
61
  // ---------------------------------------------------------------------------
@@ -107,7 +68,7 @@ export class SystemMessageRouter {
107
68
  constructor(
108
69
  private readonly conversation: ConversationManager,
109
70
  private panel: SystemMessagesPanel | null,
110
- private readonly getTargetForKind: (kind: SystemMessageKind) => SystemMessageTarget = defaultTargetForKind,
71
+ private readonly getTargetForKind: (kind: SystemMessageKind) => SystemMessageTarget = defaultSystemMessageTarget,
111
72
  ) {}
112
73
 
113
74
  // ── Public API ────────────────────────────────────────────────────────────
@@ -127,7 +88,7 @@ export class SystemMessageRouter {
127
88
  kind: SystemMessageKind,
128
89
  ): void {
129
90
  const target = this.getTargetForKind(kind);
130
- const delivery = resolveDelivery(target, this.panel !== null);
91
+ const delivery = resolveSystemMessageDelivery(target, this.panel !== null);
131
92
  if (delivery.toPanel) {
132
93
  this.panel?.push(message, priority);
133
94
  }
@@ -137,7 +98,7 @@ export class SystemMessageRouter {
137
98
  }
138
99
 
139
100
  routeSystemMessage(message: string, priority: SystemMessagePriority): void {
140
- this.routeTypedSystemMessage(message, priority, classifyKind(message));
101
+ this.routeTypedSystemMessage(message, priority, classifySystemMessageKind(message));
141
102
  }
142
103
 
143
104
  /**
@@ -149,8 +110,8 @@ export class SystemMessageRouter {
149
110
  * @param message - Message text.
150
111
  */
151
112
  routeAuto(message: string): void {
152
- const priority = classifyPriority(message);
153
- this.routeTypedSystemMessage(message, priority, classifyKind(message));
113
+ const priority: SystemMessagePriority = classifySystemMessagePriority(message);
114
+ this.routeTypedSystemMessage(message, priority, classifySystemMessageKind(message));
154
115
  }
155
116
 
156
117
  /**
@@ -204,7 +165,7 @@ export class SystemMessageRouter {
204
165
  export function createSystemMessageRouter(
205
166
  conversation: ConversationManager,
206
167
  panel: SystemMessagesPanel | null = null,
207
- getTargetForKind: (kind: SystemMessageKind) => SystemMessageTarget = defaultTargetForKind,
168
+ getTargetForKind: (kind: SystemMessageKind) => SystemMessageTarget = defaultSystemMessageTarget,
208
169
  ): SystemMessageRouter {
209
170
  return new SystemMessageRouter(conversation, panel, getTargetForKind);
210
171
  }
package/src/daemon/cli.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { homedir } from 'node:os';
1
+ import { homedir, networkInterfaces } from 'node:os';
2
+ import { readFileSync } from 'node:fs';
2
3
  import { ConfigManager } from '@pellux/goodvibes-sdk/platform/config/manager';
3
4
  import { RuntimeEventBus } from '@pellux/goodvibes-sdk/platform/runtime/events/index';
4
5
  import { createRuntimeStore } from '../runtime/store/index.ts';
@@ -8,6 +9,13 @@ import { HttpListener } from '@pellux/goodvibes-sdk/platform/daemon/http-listene
8
9
  import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
9
10
  import { GlobalNetworkTransportInstaller } from '@pellux/goodvibes-sdk/platform/runtime/network/index';
10
11
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
12
+ import {
13
+ getOrCreateCompanionToken,
14
+ buildCompanionConnectionInfo,
15
+ encodeConnectionPayload,
16
+ formatConnectionBlock,
17
+ } from '@pellux/goodvibes-sdk/platform/pairing/index';
18
+ import { generateQrMatrix, renderQrToString } from '@pellux/goodvibes-sdk/platform/pairing/qr-generator';
11
19
 
12
20
  type DaemonCliOwnership = {
13
21
  readonly workingDirectory: string;
@@ -19,6 +27,32 @@ type DaemonCliTokens = {
19
27
  readonly httpToken: string | undefined;
20
28
  };
21
29
 
30
+ function getLocalNetworkIp(): string {
31
+ const nets = networkInterfaces();
32
+ for (const name of Object.keys(nets)) {
33
+ for (const net of nets[name] ?? []) {
34
+ if (net.family === 'IPv4' && !net.internal) {
35
+ return net.address;
36
+ }
37
+ }
38
+ }
39
+ return 'localhost';
40
+ }
41
+
42
+ function readBootstrapPassword(credentialPath: string): string | undefined {
43
+ try {
44
+ const content = readFileSync(credentialPath, 'utf-8');
45
+ for (const line of content.split('\n')) {
46
+ if (line.startsWith('password=')) {
47
+ return line.slice('password='.length).trim();
48
+ }
49
+ }
50
+ } catch {
51
+ // credential file may not exist yet
52
+ }
53
+ return undefined;
54
+ }
55
+
22
56
  function resolveDaemonCliOwnership(): DaemonCliOwnership {
23
57
  return {
24
58
  workingDirectory: process.cwd(),
@@ -58,26 +92,68 @@ async function main(): Promise<void> {
58
92
  });
59
93
  const { daemonToken, httpToken } = readDaemonCliTokens(process.env);
60
94
 
61
- daemon.enable({ daemon: true }, daemonToken);
62
- listener.enable({ httpListener: true }, httpToken);
95
+ // If no explicit daemon token is set, use the companion token so mobile apps can connect.
96
+ const companionTokenRecord = getOrCreateCompanionToken('tui');
97
+ const effectiveDaemonToken = daemonToken ?? companionTokenRecord.token;
98
+ const effectiveHttpToken = httpToken ?? effectiveDaemonToken;
99
+
100
+ daemon.enable({ daemon: true }, effectiveDaemonToken);
101
+ listener.enable({ httpListener: true }, effectiveHttpToken);
63
102
 
64
103
  await Promise.all([
65
104
  daemon.start(),
66
105
  config.get('danger.httpListener') ? listener.start() : Promise.resolve(),
67
106
  ]);
68
107
 
108
+ const abortController = new AbortController();
109
+
69
110
  const shutdown = async (): Promise<void> => {
70
- await Promise.allSettled([listener.stop(), daemon.stop()]);
111
+ abortController.abort();
112
+ const SHUTDOWN_DEADLINE_MS = 15_000;
113
+ const timeout = new Promise<'timeout'>((resolve) =>
114
+ setTimeout(() => resolve('timeout'), SHUTDOWN_DEADLINE_MS)
115
+ );
116
+ const stop = Promise.allSettled([listener.stop(), daemon.stop()]).then(() => 'done' as const);
117
+ const result = await Promise.race([stop, timeout]);
118
+ if (result === 'timeout') {
119
+ logger.warn('shutdown deadline exceeded — forcing exit');
120
+ process.exit(1);
121
+ }
71
122
  process.exit(0);
72
123
  };
73
124
 
74
- process.on('SIGINT', () => void shutdown());
75
- process.on('SIGTERM', () => void shutdown());
125
+ let shutdownInFlight = false;
126
+ const handleSignal = (): void => {
127
+ if (shutdownInFlight) return;
128
+ shutdownInFlight = true;
129
+ void shutdown();
130
+ };
131
+
132
+ process.on('SIGINT', handleSignal);
133
+ process.on('SIGTERM', handleSignal);
76
134
 
77
135
  logger.info('goodvibes daemon host started', {
78
136
  daemon: config.get('danger.daemon'),
79
137
  httpListener: config.get('danger.httpListener'),
80
138
  });
139
+
140
+ // Print companion connection info + QR code to stdout.
141
+ // Use the config-driven control plane port, not a hardcoded default.
142
+ const daemonPort = config.get('controlPlane.port');
143
+ const daemonHost = String(process.env.GOODVIBES_DAEMON_HOST ?? getLocalNetworkIp());
144
+ const daemonUrl = `http://${daemonHost}:${daemonPort}`;
145
+ const bootstrapPassword = readBootstrapPassword(userAuth.getBootstrapCredentialPath());
146
+ const connectionInfo = buildCompanionConnectionInfo({
147
+ daemonUrl,
148
+ token: companionTokenRecord.token,
149
+ password: bootstrapPassword,
150
+ surface: 'tui',
151
+ });
152
+ const payload = encodeConnectionPayload(connectionInfo);
153
+ const qrMatrix = generateQrMatrix(payload);
154
+ const qrString = renderQrToString(qrMatrix);
155
+ // eslint-disable-next-line no-console
156
+ console.log(formatConnectionBlock(connectionInfo, qrString));
81
157
  }
82
158
 
83
159
  void main().catch(async (error) => {
@@ -62,6 +62,8 @@ export interface CommandUiActions {
62
62
  model: { id: string; provider: string; displayName: string; registryKey: string };
63
63
  effort: string;
64
64
  contextCap?: number | null;
65
+ /** Which config target to write the selected model to. Defaults to 'main'. */
66
+ target?: import('./model-picker.ts').ModelPickerTarget;
65
67
  }) => void;
66
68
  clearScreen?: () => void;
67
69
  activatePlan?: (planId: string, task: string) => void;
@@ -1,7 +1,7 @@
1
1
  import type { CommandRegistry } from '../command-registry.ts';
2
2
  import { buildMcpAttackPathReview } from '@pellux/goodvibes-sdk/platform/runtime/mcp/index';
3
3
  import { buildKnowledgeInjectionPrompt, selectKnowledgeForTask } from '@pellux/goodvibes-sdk/platform/state/knowledge-injection';
4
- import { listBuiltinSubscriptionProviders } from '../../config/subscription-providers.ts';
4
+ import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config/subscription-providers';
5
5
  import { requireReadModels, requireSubscriptionManager, requireTokenAuditor } from './runtime-services.ts';
6
6
  import { getMemoryApi } from './recall-query.ts';
7
7
 
@@ -1,4 +1,4 @@
1
- import { ServiceRegistry } from '../../config/service-registry.ts';
1
+ import { ServiceRegistry } from '@pellux/goodvibes-sdk/platform/config/service-registry';
2
2
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config/manager';
3
3
  import { evaluateSessionMaintenance, formatSessionMaintenanceLines } from '@pellux/goodvibes-sdk/platform/runtime/session-maintenance';
4
4
  import { estimateConversationTokens } from '@pellux/goodvibes-sdk/platform/core/context-compaction';
@@ -5,7 +5,7 @@ import { discoverSkills } from '../../panels/skills-panel.ts';
5
5
  import { buildSandboxReview, isRunningInWsl } from '@pellux/goodvibes-sdk/platform/runtime/sandbox/manager';
6
6
  import { renderQemuWrapperTemplate } from '@pellux/goodvibes-sdk/platform/runtime/sandbox/qemu-wrapper-template';
7
7
  import { getPluginDirectories } from '../../plugins/loader';
8
- import { listBuiltinSubscriptionProviders } from '../../config/subscription-providers.ts';
8
+ import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config/subscription-providers';
9
9
  import type { SetupReviewSnapshot } from './local-setup-transfer.ts';
10
10
  import { requireProviderApi, requireReadModels, requireServiceRegistry, requireShellPaths, requireSubscriptionManager } from './runtime-services.ts';
11
11
 
@@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, resolve } from 'node:path';
3
3
  import type { CommandRegistry } from '../command-registry.ts';
4
4
  import { VERSION } from '../../version.ts';
5
- import { listBuiltinSubscriptionProviders } from '../../config/subscription-providers.ts';
5
+ import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config/subscription-providers';
6
6
  import { handleLocalAuthCommand } from './local-auth-runtime.ts';
7
7
  import { buildAuthInspectionSnapshot, inspectProviderAuth } from '@pellux/goodvibes-sdk/platform/runtime/auth/inspection';
8
8
  import { requireProfileManager, requireSecretsManager, requireServiceRegistry, requireShellPaths, requireSubscriptionManager } from './runtime-services.ts';
@@ -0,0 +1,20 @@
1
+ import type { CommandRegistry } from '../command-registry.ts';
2
+ import { openCommandPanel } from './runtime-services.ts';
3
+
4
+ /**
5
+ * Register the /qrcode command.
6
+ *
7
+ * Opens the QR Code panel which displays a scannable QR code for
8
+ * companion app pairing, along with connection URL, token, and username.
9
+ */
10
+ export function registerQrcodeRuntimeCommands(registry: CommandRegistry): void {
11
+ registry.register({
12
+ name: 'qrcode',
13
+ aliases: ['qr', 'pair'],
14
+ description: 'Open the QR code panel for companion app pairing',
15
+ usage: '',
16
+ handler(_args, ctx) {
17
+ openCommandPanel(ctx, 'qr-code');
18
+ },
19
+ });
20
+ }
@@ -4,7 +4,7 @@ import type { CommandContext, CommandRegistry } from '../command-registry.ts';
4
4
  import { createOAuthLocalListener } from '@pellux/goodvibes-sdk/platform/config/oauth-local-listener';
5
5
  import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibes-sdk/platform/config/openai-codex-auth';
6
6
  import type { OAuthProviderConfig, ProviderSubscription } from '@pellux/goodvibes-sdk/platform/config/subscriptions';
7
- import { getSubscriptionProviderConfig, listAvailableSubscriptionProviders } from '../../config/subscription-providers.ts';
7
+ import { getSubscriptionProviderConfig, listAvailableSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config/subscription-providers';
8
8
  import { inspectProviderAuth } from '@pellux/goodvibes-sdk/platform/runtime/auth/inspection';
9
9
  import { openExternalUrl } from '@pellux/goodvibes-sdk/platform/utils/open-external';
10
10
  import { requireSecretsManager, requireServiceRegistry, requireShellPaths, requireSubscriptionManager } from './runtime-services.ts';
@@ -52,6 +52,7 @@ import { registerProviderAccountsRuntimeCommands } from './commands/provider-acc
52
52
  import { registerLocalAuthRuntimeCommands } from './commands/local-auth-runtime.ts';
53
53
  import { registerIntelligenceRuntimeCommands } from './commands/intelligence-runtime.ts';
54
54
  import { registerConversationRuntimeCommands } from './commands/conversation-runtime.ts';
55
+ import { registerQrcodeRuntimeCommands } from './commands/qrcode-runtime.ts';
55
56
 
56
57
  /**
57
58
  * registerBuiltinCommands - Register all built-in slash commands into the registry.
@@ -98,6 +99,7 @@ export function registerBuiltinCommands(registry: CommandRegistry): void {
98
99
  registerLocalAuthRuntimeCommands(registry);
99
100
  registerIntelligenceRuntimeCommands(registry);
100
101
  registerConversationRuntimeCommands(registry);
102
+ registerQrcodeRuntimeCommands(registry);
101
103
  registerLocalRuntimeCommands(registry);
102
104
  registerSessionWorkflowCommands(registry);
103
105
  registerDiscoveryRuntimeCommands(registry);
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, writeFileSync, existsSync } from 'node:fs';
2
- import { copyToClipboard, pasteFromClipboard, pasteImageFromClipboard } from '@pellux/goodvibes-sdk/platform/utils/clipboard';
3
- import type { InfiniteBuffer } from '@pellux/goodvibes-sdk/platform/core/history';
2
+ import { copyToClipboard, pasteFromClipboard, pasteImageFromClipboard } from '../utils/clipboard.ts';
3
+ import type { InfiniteBuffer } from '../core/history.ts';
4
4
  import type { ConversationManager } from '../core/conversation';
5
5
  import type { PermissionCategory } from '@pellux/goodvibes-sdk/platform/permissions/manager';
6
6
  import type { ContentPart } from '@pellux/goodvibes-sdk/platform/providers/interface';
@@ -1,5 +1,5 @@
1
1
  import type { InputToken } from '@pellux/goodvibes-sdk/platform/core/tokenizer';
2
- import type { InfiniteBuffer } from '@pellux/goodvibes-sdk/platform/core/history';
2
+ import type { InfiniteBuffer } from '../core/history.ts';
3
3
  import type { CommandContext, CommandRegistry } from './command-registry.ts';
4
4
  import { AutocompleteEngine } from './autocomplete.ts';
5
5
  import { FilePickerModal } from './file-picker.ts';
@@ -166,6 +166,12 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
166
166
  searchManager: context.searchManager,
167
167
  scroll: context.scroll,
168
168
  getScrollTop: context.getScrollTop,
169
+ openModelPickerWithTarget: context.commandContext?.openModelPicker
170
+ ? (target: import('./model-picker.ts').ModelPickerTarget) => {
171
+ context.modelPicker.target = target;
172
+ context.commandContext!.openModelPicker!();
173
+ }
174
+ : undefined,
169
175
  }, token);
170
176
  context.selectionCallback = modalRoute.selectionCallback;
171
177
  context.helpOverlayActive = modalRoute.helpOverlayActive;
@@ -215,7 +215,10 @@ type SettingsRouteState = {
215
215
  nextCategory: () => void;
216
216
  editBackspace: () => void;
217
217
  editChar: (char: string) => void;
218
+ pendingModelPickerTarget: import('./model-picker.ts').ModelPickerTarget | null;
218
219
  };
220
+ /** Called when the settings modal requests the model picker for a non-main target. */
221
+ openModelPickerWithTarget?: (target: import('./model-picker.ts').ModelPickerTarget) => void;
219
222
  requestRender: () => void;
220
223
  handleEscape: () => void;
221
224
  };
@@ -231,7 +234,14 @@ export function handleSettingsModalToken(state: SettingsRouteState, token: Input
231
234
  if (token.logicalName === 'enter' || (token.logicalName === 'space' && !state.settingsModal.editingMode)) {
232
235
  if (state.settingsModal.editingMode) state.settingsModal.commitEdit();
233
236
  else if (state.settingsModal.currentCategory === 'flags') state.settingsModal.toggleSelectedFlag();
234
- else state.settingsModal.activateSelected();
237
+ else {
238
+ state.settingsModal.activateSelected();
239
+ const pickerTarget = state.settingsModal.pendingModelPickerTarget;
240
+ if (pickerTarget !== null) {
241
+ state.settingsModal.pendingModelPickerTarget = null;
242
+ state.openModelPickerWithTarget?.(pickerTarget);
243
+ }
244
+ }
235
245
  } else if ((token.logicalName === 'left' || token.logicalName === 'right') && !state.settingsModal.editingMode) {
236
246
  state.settingsModal.adjustSelected(token.logicalName, token.shift ? 10 : 1);
237
247
  } else if (token.logicalName === 'up') state.settingsModal.moveUp();
@@ -241,7 +251,14 @@ export function handleSettingsModalToken(state: SettingsRouteState, token: Input
241
251
  } else if (token.type === 'text') {
242
252
  if (token.value === ' ' && !state.settingsModal.editingMode) {
243
253
  if (state.settingsModal.currentCategory === 'flags') state.settingsModal.toggleSelectedFlag();
244
- else state.settingsModal.activateSelected();
254
+ else {
255
+ state.settingsModal.activateSelected();
256
+ const pickerTarget = state.settingsModal.pendingModelPickerTarget;
257
+ if (pickerTarget !== null) {
258
+ state.settingsModal.pendingModelPickerTarget = null;
259
+ state.openModelPickerWithTarget?.(pickerTarget);
260
+ }
261
+ }
245
262
  } else if (state.settingsModal.editingMode) {
246
263
  state.settingsModal.editChar(token.value);
247
264
  }
@@ -1,5 +1,5 @@
1
1
  import type { InputToken } from '@pellux/goodvibes-sdk/platform/core/tokenizer';
2
- import type { InfiniteBuffer } from '@pellux/goodvibes-sdk/platform/core/history';
2
+ import type { InfiniteBuffer } from '../core/history.ts';
3
3
  import type { SelectionResult, SelectionModal } from './selection-modal.ts';
4
4
  import type { BookmarkModal } from './bookmark-modal.ts';
5
5
  import type { SettingsModal } from './settings-modal.ts';
@@ -72,6 +72,8 @@ export type ModalTokenRouteState = {
72
72
  searchManager: SearchManager;
73
73
  scroll: (delta: number) => void;
74
74
  getScrollTop: () => number;
75
+ /** Callback to open the model picker with a specific target (helper or tool). Optional — only wired when available. */
76
+ openModelPickerWithTarget?: (target: import('./model-picker.ts').ModelPickerTarget) => void;
75
77
  };
76
78
 
77
79
  export function handleModalTokenRoutes(state: ModalTokenRouteState, token: InputToken): {
@@ -117,6 +119,7 @@ export function handleModalTokenRoutes(state: ModalTokenRouteState, token: Input
117
119
 
118
120
  if (handleSettingsModalToken({
119
121
  settingsModal: state.settingsModal,
122
+ openModelPickerWithTarget: state.openModelPickerWithTarget,
120
123
  requestRender: state.requestRender,
121
124
  handleEscape: state.handleEscape,
122
125
  }, token)) {
@@ -57,9 +57,11 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
57
57
  if (selected.reasoningEffort && selected.reasoningEffort.length > 0) {
58
58
  state.modelPicker.showEffortPicker(selected, currentEffort);
59
59
  } else {
60
+ const target = state.modelPicker.target;
60
61
  state.commandContext?.completeModelSelection?.({
61
62
  model: selected,
62
63
  effort: currentEffort,
64
+ target,
63
65
  });
64
66
  state.modelPicker.close();
65
67
  if (state.modalStack[state.modalStack.length - 1] === 'modelPicker') state.modalStack.pop();
@@ -76,7 +78,7 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
76
78
  } else if (mode === 'effort') {
77
79
  const model = state.modelPicker.pendingModel;
78
80
  const effort = state.modelPicker.effortLevels[idx];
79
- if (model && effort) state.commandContext?.completeModelSelection?.({ model, effort });
81
+ if (model && effort) state.commandContext?.completeModelSelection?.({ model, effort, target: state.modelPicker.target });
80
82
  state.modelPicker.close();
81
83
  if (state.modalStack[state.modalStack.length - 1] === 'modelPicker') state.modalStack.pop();
82
84
  } else if (mode === 'contextCap') {
@@ -86,7 +88,7 @@ export function handleModelPickerToken(state: ModelPickerRouteState, token: Inpu
86
88
  const parsedCap = rawInput.length > 0 ? parseInt(rawInput, 10) : null;
87
89
  const validCap = parsedCap !== null && parsedCap > 0 && parsedCap <= 10_000_000 ? parsedCap : null;
88
90
  const effort = state.commandContext?.session.runtime.reasoningEffort ?? 'medium';
89
- state.commandContext?.completeModelSelection?.({ model: capModel, effort, contextCap: validCap });
91
+ state.commandContext?.completeModelSelection?.({ model: capModel, effort, contextCap: validCap, target: state.modelPicker.target });
90
92
  }
91
93
  state.modelPicker.close();
92
94
  if (state.modalStack[state.modalStack.length - 1] === 'modelPicker') state.modalStack.pop();
@@ -1,5 +1,5 @@
1
1
  import type { InputToken } from '@pellux/goodvibes-sdk/platform/core/tokenizer';
2
- import type { InfiniteBuffer } from '@pellux/goodvibes-sdk/platform/core/history';
2
+ import type { InfiniteBuffer } from '../core/history.ts';
3
3
  import type { SearchManager } from './search.ts';
4
4
  import type { HistorySearch } from './input-history.ts';
5
5
 
@@ -1,6 +1,6 @@
1
1
  import { InputTokenizer } from '@pellux/goodvibes-sdk/platform/core/tokenizer';
2
2
  import { SelectionManager } from './selection.ts';
3
- import type { InfiniteBuffer } from '@pellux/goodvibes-sdk/platform/core/history';
3
+ import type { InfiniteBuffer } from '../core/history.ts';
4
4
  import type { CommandRegistry, CommandContext } from './command-registry.ts';
5
5
  import { AutocompleteEngine } from './autocomplete.ts';
6
6
  import { FilePickerModal } from './file-picker.ts';
@@ -7,6 +7,14 @@ import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers/
7
7
 
8
8
  export type PickerMode = 'model' | 'provider' | 'effort' | 'contextCap';
9
9
 
10
+ /**
11
+ * Which config keys the model picker writes to on commit.
12
+ * 'main' → provider.provider + provider.model (default)
13
+ * 'helper' → helper.globalProvider + helper.globalModel (+ helper.enabled: true)
14
+ * 'tool' → tools.llmProvider + tools.llmModel (+ tools.llmEnabled: true)
15
+ */
16
+ export type ModelPickerTarget = 'main' | 'helper' | 'tool';
17
+
10
18
  /**
11
19
  * Pricing tier filter.
12
20
  * 'paid' matches ModelDefinition tiers 'standard' and 'premium' for forward-compat
@@ -129,6 +137,8 @@ export class ModelPickerModal {
129
137
 
130
138
  public active = false;
131
139
  public mode: PickerMode = 'model';
140
+ /** Which config target this picker session will write to on commit. */
141
+ public target: ModelPickerTarget = 'main';
132
142
  public searchFocused = false;
133
143
  /** Tracks the mode we came from, for back-navigation. */
134
144
  public previousMode: PickerMode | null = null;
@@ -288,6 +298,7 @@ export class ModelPickerModal {
288
298
  close(): void {
289
299
  this.active = false;
290
300
  this.mode = 'model';
301
+ this.target = 'main';
291
302
  this.models = [];
292
303
  this.providers = [];
293
304
  this.pendingModel = null;
@@ -1,4 +1,4 @@
1
- import type { InfiniteBuffer } from '@pellux/goodvibes-sdk/platform/core/history';
1
+ import type { InfiniteBuffer } from '../core/history.ts';
2
2
 
3
3
  export interface SearchMatch {
4
4
  line: number;
@@ -1,5 +1,5 @@
1
- import type { InfiniteBuffer } from '@pellux/goodvibes-sdk/platform/core/history';
2
- import type { Cell } from '@pellux/goodvibes-sdk/platform/types/grid';
1
+ import type { InfiniteBuffer } from '../core/history.ts';
2
+ import type { Cell } from '../types/grid.ts';
3
3
 
4
4
  export interface SelectionPoint {
5
5
  col: number;
@@ -11,9 +11,10 @@
11
11
  */
12
12
 
13
13
  import { CONFIG_SCHEMA, type ConfigSetting, type ConfigKey, type PersistedFlagState } from '@pellux/goodvibes-sdk/platform/config/schema';
14
+ import type { ModelPickerTarget } from './model-picker.ts';
14
15
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config/manager';
15
16
  import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config/subscriptions';
16
- import { listBuiltinSubscriptionProviders } from '../config/subscription-providers.ts';
17
+ import { listBuiltinSubscriptionProviders } from '@pellux/goodvibes-sdk/platform/config/subscription-providers';
17
18
  import type { ProviderAuthFreshness, ProviderAuthRoute } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
18
19
  import { getResolvedSettingLookup } from '@pellux/goodvibes-sdk/platform/runtime/settings/control-plane';
19
20
  import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
@@ -84,6 +85,16 @@ export interface SubscriptionEntry {
84
85
  nextActions?: string[];
85
86
  }
86
87
 
88
+ /**
89
+ * Map a config key to the model picker target it should open, or null if the
90
+ * setting should use the normal inline text-edit flow.
91
+ */
92
+ function _modelPickerTargetForKey(key: string): ModelPickerTarget | null {
93
+ if (key === 'helper.globalProvider' || key === 'helper.globalModel') return 'helper';
94
+ if (key === 'tools.llmProvider' || key === 'tools.llmModel') return 'tool';
95
+ return null;
96
+ }
97
+
87
98
  function roundToPrecision(value: number, precision: number): number {
88
99
  const factor = 10 ** precision;
89
100
  return Math.round(value * factor) / factor;
@@ -121,6 +132,12 @@ export class SettingsModal {
121
132
  public editBuffer = '';
122
133
  /** Server awaiting explicit allow-all confirmation, if any. */
123
134
  public mcpAllowAllConfirmationTarget: string | null = null;
135
+ /**
136
+ * Set by activateSelected() when the highlighted setting should open the
137
+ * model picker rather than entering inline text edit mode.
138
+ * Consumed and cleared by the route handler after each Enter/Space action.
139
+ */
140
+ public pendingModelPickerTarget: ModelPickerTarget | null = null;
124
141
  /** Provider awaiting explicit logout confirmation, if any. */
125
142
  public subscriptionLogoutConfirmationTarget: string | null = null;
126
143
 
@@ -307,6 +324,13 @@ export class SettingsModal {
307
324
 
308
325
  const { setting } = entry;
309
326
 
327
+ // Delegate provider/model picker settings to the model picker UI
328
+ const pickerTarget = _modelPickerTargetForKey(setting.key);
329
+ if (pickerTarget !== null) {
330
+ this.pendingModelPickerTarget = pickerTarget;
331
+ return;
332
+ }
333
+
310
334
  if (setting.type === 'boolean') {
311
335
  const newVal = !entry.currentValue;
312
336
  this._setValue(setting.key, newVal);
@@ -522,7 +546,9 @@ export class SettingsModal {
522
546
  }
523
547
 
524
548
  for (const setting of CONFIG_SCHEMA) {
525
- const cat = setting.key.split('.')[0] as SettingsCategory;
549
+ const rawCat = setting.key.split('.')[0] as string;
550
+ // Route helper.* settings into the tools group for unified display
551
+ const cat = (rawCat === 'helper' ? 'tools' : rawCat) as SettingsCategory;
526
552
  if (!this.groups.has(cat)) continue;
527
553
  const currentValue = configManager.get(setting.key as ConfigKey);
528
554
  const resolved = getResolvedSettingLookup(configManager, setting.key as ConfigKey)?.entry;
@@ -702,7 +728,9 @@ export class SettingsModal {
702
728
  try {
703
729
  this.configManager.setDynamic(key, value);
704
730
  // Update the cached entry in-place — avoids full schema re-scan on each edit
705
- const cat = key.split('.')[0] as SettingsCategory;
731
+ const rawCat = key.split('.')[0] as string;
732
+ // helper.* entries are stored in the tools group
733
+ const cat = (rawCat === 'helper' ? 'tools' : rawCat) as SettingsCategory;
706
734
  const entries = this.groups.get(cat);
707
735
  if (entries) {
708
736
  const entry = entries.find(e => e.setting.key === key);
package/src/main.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  import { homedir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { Compositor } from './renderer/compositor.ts';
7
- import { type Line } from '@pellux/goodvibes-sdk/platform/types/grid';
7
+ import { type Line } from './types/grid.ts';
8
8
  import { UIFactory } from './renderer/ui-factory.ts';
9
9
  import { Orchestrator } from './core/orchestrator';
10
10
  import { InputHandler } from './input/handler.ts';