@pellux/goodvibes-tui 0.18.13 → 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 (65) hide show
  1. package/CHANGELOG.md +122 -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/builtin/session.ts +66 -0
  22. package/src/panels/builtin/shared.ts +1 -1
  23. package/src/panels/provider-account-snapshot.ts +1 -1
  24. package/src/panels/provider-accounts-panel.ts +23 -27
  25. package/src/panels/qr-panel.ts +182 -0
  26. package/src/panels/scrollable-list-panel.ts +407 -0
  27. package/src/panels/services-panel.ts +1 -1
  28. package/src/panels/subscription-panel.ts +1 -1
  29. package/src/panels/worktree-panel.ts +20 -19
  30. package/src/renderer/qr-renderer.ts +117 -0
  31. package/src/renderer/settings-modal-helpers.ts +122 -0
  32. package/src/renderer/settings-modal.ts +147 -111
  33. package/src/runtime/bootstrap-command-context.ts +1 -1
  34. package/src/runtime/bootstrap-command-parts.ts +31 -15
  35. package/src/runtime/bootstrap.ts +6 -1
  36. package/src/runtime/diagnostics/panels/index.ts +5 -5
  37. package/src/runtime/services.ts +1 -1
  38. package/src/runtime/store/domains/domain-read-matrix.ts +0 -2
  39. package/src/runtime/ui-events.ts +1 -46
  40. package/src/runtime/ui-read-model-helpers.ts +1 -32
  41. package/src/runtime/ui-read-models-observability-maintenance.ts +1 -81
  42. package/src/runtime/ui-read-models-observability-options.ts +1 -5
  43. package/src/runtime/ui-read-models-observability-remote.ts +1 -73
  44. package/src/runtime/ui-read-models-observability-security.ts +1 -172
  45. package/src/runtime/ui-read-models-observability-system.ts +1 -217
  46. package/src/runtime/ui-read-models-observability.ts +1 -59
  47. package/src/runtime/ui-service-queries.ts +1 -114
  48. package/src/version.ts +1 -1
  49. package/src/config/service-registry.ts +0 -1
  50. package/src/config/subscription-providers.ts +0 -1
  51. package/src/runtime/diagnostics/actions.ts +0 -776
  52. package/src/runtime/diagnostics/index.ts +0 -99
  53. package/src/runtime/diagnostics/panels/agents.ts +0 -252
  54. package/src/runtime/diagnostics/panels/events.ts +0 -188
  55. package/src/runtime/diagnostics/panels/health.ts +0 -242
  56. package/src/runtime/diagnostics/panels/tasks.ts +0 -251
  57. package/src/runtime/diagnostics/panels/tool-calls.ts +0 -267
  58. package/src/runtime/diagnostics/provider.ts +0 -262
  59. package/src/runtime/store/domains/conversation.ts +0 -1
  60. package/src/runtime/store/domains/permissions.ts +0 -1
  61. package/src/runtime/store/helpers/reducers/conversation.ts +0 -1
  62. package/src/runtime/store/helpers/reducers/lifecycle.ts +0 -1
  63. package/src/runtime/store/helpers/reducers/shared.ts +0 -60
  64. package/src/runtime/store/helpers/reducers/sync.ts +0 -555
  65. package/src/runtime/store/helpers/reducers.ts +0 -30
package/CHANGELOG.md CHANGED
@@ -4,6 +4,128 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.18.17] — 2026-04-16
8
+
9
+ ### Bug Fixes
10
+
11
+ - **Companion pairing token registered with embedded daemon**: `src/runtime/bootstrap.ts` now loads the persistent companion-pairing token via `getOrCreateCompanionToken('tui')` and passes it as `sharedDaemonToken` to `startExternalServices`. The TUI's QR panel advertises this token as the bearer for phone pairing; before this fix, the embedded daemon was started with no shared token and rejected every scanned token with `authenticated: false, authMode: "invalid"`
12
+ - **QR code visual alignment**: `src/renderer/qr-renderer.ts` now uses `leftPad = 1` (down from 2) and prepends a single top quiet-band row. The QR's finder patterns now register symmetrically on both axes; previous rendering was mis-aligned by one cell horizontally and had no top quiet band
13
+
14
+ ### Dependencies
15
+
16
+ - Bumped `@pellux/goodvibes-sdk` 0.18.36 → 0.18.37, picking up: `sharedDaemonToken`/`sharedHttpListenerToken` factory options on `startHostServices`, bootstrap credential drift detection that warns when `auth-bootstrap.txt` falls out of sync with `auth-users.json`
17
+ - Regenerated `docs/foundation-artifacts/*` against SDK 0.18.37
18
+
19
+ ### Tests
20
+
21
+ - Updated `src/test/runtime/bootstrap-services.test.ts` `daemonEnable`/`listenerEnable` expectations to include the new second argument (`undefined` when no shared token is supplied)
22
+ - Test suite: 437/437 passing, typecheck clean, architecture check green
23
+
24
+ ---
25
+
26
+ ## [0.18.16] — 2026-04-16
27
+
28
+ ### Bug Fixes
29
+
30
+ - **resolveToolLLM tests**: enabled `tools.llmEnabled` by default in `createTestManagers()` (`src/test/helpers/test-managers.ts`) so tool LLM resolution tests exercise the resolution logic directly; previously every test hit the gate and resolved to `null`
31
+ - **Domain boundary contract test (GC-ARCH-001)**: removed `'conversation'` and `'permissions'` from the `DOMAINS` array in `src/runtime/store/domains/domain-read-matrix.ts` — the files they referenced were deleted in 0.18.15 and the filesystem↔array consistency check was failing
32
+
33
+ ### Architecture
34
+
35
+ - **settings-modal decomposition**: extracted 10 pure helpers (`formatValue`, `valueColor`, `flagStateColor`, `mcpTrustColor`, `subscriptionStateColor`, `inferSubscriptionRouteReason`, `CATEGORY_LABELS`, `SETTING_LABELS`, `getSettingLabel`, `describeUiRouting`) into `src/renderer/settings-modal-helpers.ts`. `settings-modal.ts` drops from 844 → 737 lines, back under the 800-line architecture cap
36
+
37
+ ### Dependencies
38
+
39
+ - Bumped `@pellux/goodvibes-sdk` 0.18.33 → 0.18.36, picking up: daemon shutdown symmetry, event bus iteration fix, atomic session writes, rate limiter TTL + LRU + sweep, `fetchWithTimeout` helper, restored port honoring in `resolveHostBinding` for `local`/`network` hostModes, and restored constructor-injected port/host in `resolveDaemonFacadeRuntime`
40
+ - Regenerated `docs/foundation-artifacts/*` against the new SDK
41
+
42
+ ### Tests & Checks
43
+
44
+ - Test suite: 437/437 passing (was 431/437 after 0.18.15)
45
+ - Architecture check: passing (was failing with `settings-modal.ts` 844 > 800-line cap)
46
+ - Typecheck: clean
47
+
48
+ ---
49
+
50
+ ## [0.18.15] — 2026-04-16
51
+
52
+ ### Correctness Fix
53
+
54
+ - **daemon SIGINT/SIGTERM drain**: `src/daemon/cli.ts` — added AbortController signaling for in-flight requests, 15-second `Promise.race` shutdown deadline, hard `process.exit(1)` if deadline exceeded, debounced double-signal guard (`shutdownInFlight` flag)
55
+
56
+ ### Dead Code Removal (Tier 3 items 15-17)
57
+
58
+ - Deleted 15 TUI mirror files with zero external importers:
59
+ - `src/runtime/diagnostics/index.ts`, `provider.ts`, `actions.ts`
60
+ - `src/runtime/diagnostics/panels/agents.ts`, `events.ts`, `health.ts`, `tasks.ts`, `tool-calls.ts`
61
+ - `src/runtime/store/helpers/reducers.ts` (barrel) and 4 sub-reducers
62
+ - `src/runtime/store/domains/permissions.ts`, `conversation.ts`
63
+ - Updated `src/runtime/diagnostics/panels/index.ts` to re-export deleted panels from SDK
64
+
65
+ ### Config Re-export Shim Inlining (Tier 3 item 18)
66
+
67
+ - Deleted `src/config/service-registry.ts` and `src/config/subscription-providers.ts` (1-line SDK re-exports)
68
+ - Updated 29 call sites to import directly from `@pellux/goodvibes-sdk/platform/config/*`
69
+
70
+ ### SDK Consolidation — UI Read Models (Tier 3 items 19-20)
71
+
72
+ - Converted 9 TUI mirror files to 1-line SDK re-exports (preserving all call-site import paths):
73
+ - `ui-events.ts`, `ui-service-queries.ts`, `ui-read-model-helpers.ts`
74
+ - `ui-read-models-observability.ts` and 4 observability sub-files (maintenance, options, remote, security, system)
75
+ - Skipped (TUI-specific divergence): `ui-services.ts` (uses TUI `SecretsManager` subclass), `ui-read-models.ts` (depends on TUI `RuntimeServices`)
76
+ - panel-resources drift: TUI version (119 lines) uses `panel-health-monitor.ts`; SDK version (152 lines) uses `component-health-monitor.js` — different monitor interfaces, TUI-specific binding kept
77
+
78
+ ---
79
+
80
+ ## [0.18.14] — 2026-04-16
81
+
82
+ ### Panel Navigation Overhaul
83
+
84
+ - Created `ScrollableListPanel<T>` and `SearchableListPanel<T>` base classes in `src/panels/scrollable-list-panel.ts`
85
+ - Migrated 30 panels from hand-coded scroll/cursor management to the shared base classes
86
+ - All list panels now have consistent navigation: up/down/j/k, pageup/pagedown, home/end/g/G, enter to select
87
+ - Selection is always visible within the viewport — guaranteed by `getVisibleWindow()` from `surface-layout.ts`
88
+ - Removed ~150 lines of duplicated scroll boilerplate across panels
89
+
90
+ ### Modal Viewport Fixes
91
+
92
+ - Fixed modal sizing: height is exactly 45% of viewport (both min and max — all modals same size), width is 50% with 25% minimum
93
+ - Fixed modal scroll/selection: 6 modal/overlay files updated to use shared `getVisibleWindow()` instead of inline scroll math
94
+ - Autocomplete overlay, file picker, bookmark modal, session picker, profile picker, and live tail modal all use the same viewport function
95
+
96
+ ### Settings Modal: Tools Tab
97
+
98
+ - Added proper tools tab UI with "Tool LLM" and "Helper Model" section headers
99
+ - Helper config keys (`helper.enabled`, `helper.globalProvider`, `helper.globalModel`) now routed into the tools tab
100
+ - Boolean settings display as [on]/[off] toggles
101
+ - Selecting a provider/model setting opens the full model picker instead of a text field
102
+ - Model picker now supports 3 target modes: main, helper, and tool
103
+ - Selecting a helper/tool model auto-enables the feature (`helper.enabled: true` / `tools.llmEnabled: true`)
104
+
105
+ ### QR Code Pairing for Companion Apps
106
+
107
+ - Added `/qrcode` command (aliases `/qr`, `/pair`) that opens a QR code panel
108
+ - QR panel displays connection info (daemon URL, token, username) + scannable QR code
109
+ - QR rendered using Unicode half-block characters (▀/▄/█) for compact terminal display
110
+ - Supports `r` to regenerate token (invalidates old one) and `c` to copy token to clipboard
111
+ - Daemon standalone mode (`goodvibes-daemon`) now prints QR + connection info to stdout on startup
112
+ - Companion tokens persist to `.goodvibes/tui/companion-token.json` with `gv_` prefix
113
+ - Built on SDK 0.18.30 pairing module
114
+
115
+ ### Health Monitoring Rename
116
+
117
+ - Renamed `panelHealthMonitor` to `componentHealthMonitor` across 20 files to align with SDK 0.18.29's generic naming
118
+ - Deprecated `Panel*` type aliases preserved for backward compatibility
119
+
120
+ ### SDK 0.18.30 Update
121
+
122
+ - Updated to `@pellux/goodvibes-sdk@0.18.30`
123
+ - Consumes new pairing module, `tools.llmEnabled` config, and all 0.18.29 boundary cleanup
124
+
125
+ ### Verification
126
+
127
+ - Full typecheck passes: `bun x tsc --noEmit` — 0 errors
128
+
7
129
  ## [0.18.13] — 2026-04-16
8
130
 
9
131
  ### SDK/TUI Boundary Separation
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml/badge.svg)](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Version](https://img.shields.io/badge/version-0.18.13-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.18.17-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
6
6
 
7
7
  A terminal-native AI coding, operations, automation, knowledge, and integration console with a typed runtime, omnichannel surfaces, structured memory/knowledge, and a raw ANSI renderer.
8
8
 
@@ -3,7 +3,7 @@
3
3
  "product": {
4
4
  "id": "goodvibes",
5
5
  "surface": "operator",
6
- "version": "0.18.29"
6
+ "version": "0.18.37"
7
7
  },
8
8
  "auth": {
9
9
  "modes": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pellux/goodvibes-tui",
3
- "version": "0.18.13",
3
+ "version": "0.18.17",
4
4
  "description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
5
5
  "type": "module",
6
6
  "main": "src/main.ts",
@@ -16,6 +16,7 @@
16
16
  "src",
17
17
  "!src/test",
18
18
  "!src/**/*.test.ts",
19
+ "!src/**/__tests__",
19
20
  "scripts/postinstall.js",
20
21
  "README.md",
21
22
  "CHANGELOG.md",
@@ -88,7 +89,7 @@
88
89
  "@anthropic-ai/vertex-sdk": "^0.16.0",
89
90
  "@ast-grep/napi": "^0.42.0",
90
91
  "@aws/bedrock-token-generator": "^1.1.0",
91
- "@pellux/goodvibes-sdk": "0.18.29",
92
+ "@pellux/goodvibes-sdk": "0.18.37",
92
93
  "bash-language-server": "^5.6.0",
93
94
  "fuse.js": "^7.1.0",
94
95
  "graphql": "^16.13.2",
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);
@@ -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
  }
@@ -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();
@@ -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;
@@ -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);