@pellux/goodvibes-tui 0.18.23 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +7 -3
  5. package/src/core/conversation-rendering.ts +8 -6
  6. package/src/core/orchestrator.ts +1 -1
  7. package/src/input/commands/diff-runtime.ts +6 -5
  8. package/src/input/commands/guidance-runtime.ts +1 -1
  9. package/src/input/commands/health-runtime.ts +2 -2
  10. package/src/input/commands/local-setup-review.ts +1 -1
  11. package/src/input/commands/session-content.ts +1 -1
  12. package/src/input/commands/shell-core.ts +3 -2
  13. package/src/input/commands/skills-runtime.ts +2 -2
  14. package/src/input/commands/subscription-runtime.ts +4 -4
  15. package/src/input/handler.ts +8 -10
  16. package/src/input/panel-integration-actions.ts +2 -1
  17. package/src/input/settings-modal-types.ts +60 -0
  18. package/src/input/settings-modal.ts +83 -65
  19. package/src/panels/agent-inspector-panel.ts +10 -9
  20. package/src/panels/agent-logs-panel.ts +26 -6
  21. package/src/panels/approval-panel.ts +1 -0
  22. package/src/panels/automation-control-panel.ts +1 -0
  23. package/src/panels/base-panel.ts +108 -3
  24. package/src/panels/communication-panel.ts +1 -0
  25. package/src/panels/context-visualizer-panel.ts +2 -0
  26. package/src/panels/control-plane-panel.ts +1 -0
  27. package/src/panels/diff-panel.ts +2 -0
  28. package/src/panels/file-explorer-panel.ts +51 -31
  29. package/src/panels/file-preview-panel.ts +57 -35
  30. package/src/panels/git-panel.ts +12 -13
  31. package/src/panels/hooks-panel.ts +3 -1
  32. package/src/panels/incident-review-panel.ts +4 -2
  33. package/src/panels/knowledge-panel.ts +75 -107
  34. package/src/panels/local-auth-panel.ts +1 -0
  35. package/src/panels/marketplace-panel.ts +51 -69
  36. package/src/panels/mcp-panel.ts +3 -1
  37. package/src/panels/memory-panel.ts +90 -158
  38. package/src/panels/ops-control-panel.ts +1 -0
  39. package/src/panels/orchestration-panel.ts +70 -51
  40. package/src/panels/panel-list-panel.ts +5 -4
  41. package/src/panels/panel-manager.ts +3 -0
  42. package/src/panels/plan-dashboard-panel.ts +2 -0
  43. package/src/panels/plugins-panel.ts +1 -0
  44. package/src/panels/polish.ts +51 -2
  45. package/src/panels/provider-accounts-panel.ts +1 -0
  46. package/src/panels/provider-health-panel.ts +6 -8
  47. package/src/panels/routes-panel.ts +3 -1
  48. package/src/panels/schedule-panel.ts +7 -6
  49. package/src/panels/scrollable-list-panel.ts +19 -2
  50. package/src/panels/security-panel.ts +17 -15
  51. package/src/panels/services-panel.ts +6 -4
  52. package/src/panels/session-browser-panel.ts +19 -18
  53. package/src/panels/settings-sync-panel.ts +3 -1
  54. package/src/panels/skills-panel.ts +114 -230
  55. package/src/panels/subscription-panel.ts +1 -0
  56. package/src/panels/system-messages-panel.ts +147 -141
  57. package/src/panels/tasks-panel.ts +1 -0
  58. package/src/panels/token-budget-panel.ts +2 -0
  59. package/src/panels/watchers-panel.ts +1 -0
  60. package/src/panels/worktree-panel.ts +1 -0
  61. package/src/panels/wrfc-panel.ts +2 -0
  62. package/src/renderer/agent-detail-modal.ts +2 -2
  63. package/src/renderer/ansi-sanitize.ts +76 -0
  64. package/src/renderer/buffer.ts +12 -1
  65. package/src/renderer/help-overlay.ts +14 -3
  66. package/src/renderer/settings-modal-helpers.ts +27 -0
  67. package/src/renderer/settings-modal.ts +18 -1
  68. package/src/renderer/status-glyphs.ts +21 -0
  69. package/src/renderer/status-token.ts +4 -8
  70. package/src/renderer/tool-call.ts +4 -3
  71. package/src/runtime/bootstrap-core.ts +1 -1
  72. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  73. package/src/runtime/bootstrap.ts +7 -8
  74. package/src/runtime/diagnostics/panels/policy.ts +2 -1
  75. package/src/shell/ui-openers.ts +1 -1
  76. package/src/version.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,40 @@ All notable changes to GoodVibes TUI.
4
4
 
5
5
  ---
6
6
 
7
+ ## [0.19.0] — 2026-04-18
8
+
9
+ ### Changed
10
+ - Upgraded `@pellux/goodvibes-sdk` from 0.19.6 to 0.21.1 (soak-period release).
11
+ TUI adaptation required: regenerated `docs/foundation-artifacts/operator-contract.json`
12
+ to match the updated `buildOperatorContract()` output in the new SDK version.
13
+ `peer-contract.json`, `knowledge-graphql.graphql`, and `knowledge-store.sql` were
14
+ unchanged by this SDK bump.
15
+
16
+ ### Added
17
+ - Wave B panel migration: migrated 5 panels (knowledge, marketplace, memory,
18
+ system-messages, orchestration) to `ScrollableListPanel<T>`/`SearchableListPanel<T>`
19
+ generics; added `docs/panel-authoring.md` as the canonical panel authoring guide.
20
+ - Wave C-α reliability pass: F-perf-01 (trackedRender on 5 hot panels),
21
+ F-perf-02 (async panel fs + skills-panel de-blocking), F-perf-03 (timer registry
22
+ + 5-panel zombie-timer leak prevention), F-errors-02 (observable async failures —
23
+ no silent `.catch(() => {})`), F-sec-02 (ANSI escape sanitization at
24
+ tool-call untrusted-content entry points).
25
+ - `src/input/settings-modal-types.ts`: extracted `SettingsCategory`,
26
+ `SETTINGS_CATEGORIES`, `SettingEntry`, `FlagEntry`, `McpEntry`,
27
+ `SubscriptionEntry` type definitions out of the settings-modal module.
28
+
29
+ ### Security
30
+ - Postinstall patcher from `@pellux/goodvibes-sdk@0.21.1` mitigates three
31
+ minimatch ReDoS advisories in the consumer install tree.
32
+ - Added `overrides: { minimatch: ^10.2.5 }` to TUI's own `package.json`
33
+ so `npm audit` reports clean for the TUI install tree independently.
34
+
35
+ ### Fixed
36
+ - Foundation artifacts gate now passes: `operator-contract.json` updated to
37
+ match SDK 0.21.1 `buildOperatorContract()` canonical output.
38
+
39
+ ---
40
+
7
41
  ## [0.18.23] — 2026-04-16
8
42
 
9
43
  ### Wave 4α+β performance bundle + α review follow-ups + regression fixes
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.23-blue.svg)](https://github.com/mgd34msu/goodvibes-tui)
5
+ [![Version](https://img.shields.io/badge/version-0.19.0-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.37"
6
+ "version": "0.21.1"
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.23",
3
+ "version": "0.19.0",
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",
@@ -89,7 +89,7 @@
89
89
  "@anthropic-ai/vertex-sdk": "^0.16.0",
90
90
  "@ast-grep/napi": "^0.42.0",
91
91
  "@aws/bedrock-token-generator": "^1.1.0",
92
- "@pellux/goodvibes-sdk": "0.18.37",
92
+ "@pellux/goodvibes-sdk": "0.21.1",
93
93
  "bash-language-server": "^5.6.0",
94
94
  "fuse.js": "^7.1.0",
95
95
  "graphql": "^16.13.2",
@@ -115,8 +115,12 @@
115
115
  "typescript": "^5.9.3"
116
116
  },
117
117
  "trustedDependencies": [
118
+ "@pellux/goodvibes-sdk",
118
119
  "tree-sitter-css",
119
120
  "tree-sitter-javascript",
120
121
  "tree-sitter-python"
121
- ]
122
+ ],
123
+ "overrides": {
124
+ "minimatch": "^10.2.5"
125
+ }
122
126
  }
@@ -110,12 +110,14 @@ export function renderConversationAssistantMessage(
110
110
  // determines `numWidth` (digit count) and thus `gutterW` (gutter column width).
111
111
  // 2. Render pass: render at `width - gutterW` with the gutter factored in.
112
112
  //
113
- // Single-pass is not feasible here because `numWidth` depends on `totalLines`,
114
- // which is unknown before rendering. The 4α commit message claim that this
115
- // "eliminates double-parse when line numbers are enabled" was inaccurate:
116
- // eliminated the legacy `renderMarkdown()` duplicate that was used for the
117
- // code-block line-number mode ('code'). The 'all' mode double-call is unavoidable
118
- // by design and remains unchanged.
113
+ // Single-pass is not pursued here. It would require either a pessimistic
114
+ // `numWidth=6` (fits 999,999 lines, but wastes 3-4 gutter columns on typical
115
+ // messages) or rendering the numbered output into a scratch buffer and trimming.
116
+ // Neither is clearly better than the current two-pass measurement approach.
117
+ // The commit message claim that this "eliminates double-parse when line
118
+ // numbers are enabled" was inaccurate: 4α eliminated the legacy
119
+ // `renderMarkdown()` duplicate used for code-block line-number mode ('code').
120
+ // The 'all' mode double-call is a deliberate design choice and remains unchanged.
119
121
  const measureWidth = showAllLineNumbers ? width : 0;
120
122
  const totalLines = showAllLineNumbers
121
123
  ? renderMarkdownTracked(message.content, measureWidth, { codeBlockLineNumbers: false }).lines.length
@@ -1,3 +1,3 @@
1
1
  // Thin TUI wrapper — re-exports the SDK Orchestrator unchanged.
2
2
  // The SDK class already contains all behaviour including getSpinner().
3
- export { Orchestrator } from '@pellux/goodvibes-sdk/platform/core/orchestrator';
3
+ export { Orchestrator, type OrchestratorOptions } from '@pellux/goodvibes-sdk/platform/core/orchestrator';
@@ -1,4 +1,5 @@
1
1
  import { join } from 'path';
2
+ import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
2
3
  import type { CommandRegistry } from '../command-registry.ts';
3
4
  import { requirePanelManager, requireSessionChangeTracker, requireShellPaths } from './runtime-services.ts';
4
5
 
@@ -86,7 +87,7 @@ export function registerDiffRuntimeCommands(registry: CommandRegistry): void {
86
87
  return (await new Response(proc.stdout).text()).trim().split('\n').filter(Boolean);
87
88
  })();
88
89
  if (workingChangedFiles.length > 0) {
89
- enrichSemanticDiff(diffPanel, workingChangedFiles, 'HEAD', () => ctx.renderRequest(), workingDirectory).catch(() => {});
90
+ enrichSemanticDiff(diffPanel, workingChangedFiles, 'HEAD', () => ctx.renderRequest(), workingDirectory).catch((err) => { logger.debug('semantic diff enrichment failed', { err }); });
90
91
  }
91
92
  break;
92
93
  }
@@ -111,7 +112,7 @@ export function registerDiffRuntimeCommands(registry: CommandRegistry): void {
111
112
  return (await new Response(stagedProc.stdout).text()).trim().split('\n').filter(Boolean);
112
113
  })();
113
114
  if (stagedChangedFiles.length > 0) {
114
- enrichSemanticDiff(diffPanel, stagedChangedFiles, 'HEAD', () => ctx.renderRequest(), workingDirectory).catch(() => {});
115
+ enrichSemanticDiff(diffPanel, stagedChangedFiles, 'HEAD', () => ctx.renderRequest(), workingDirectory).catch((err) => { logger.debug('semantic diff enrichment failed', { err }); });
115
116
  }
116
117
  }
117
118
  break;
@@ -126,7 +127,7 @@ export function registerDiffRuntimeCommands(registry: CommandRegistry): void {
126
127
  return (await new Response(proc.stdout).text()).trim().split('\n').filter(Boolean);
127
128
  })();
128
129
  if (headChangedFiles.length > 0) {
129
- enrichSemanticDiff(diffPanel, headChangedFiles, 'HEAD', () => ctx.renderRequest(), workingDirectory).catch(() => {});
130
+ enrichSemanticDiff(diffPanel, headChangedFiles, 'HEAD', () => ctx.renderRequest(), workingDirectory).catch((err) => { logger.debug('semantic diff enrichment failed', { err }); });
130
131
  }
131
132
  break;
132
133
  }
@@ -137,7 +138,7 @@ export function registerDiffRuntimeCommands(registry: CommandRegistry): void {
137
138
  ctx.print(`Loading session diff (${sessionFiles.length} file${sessionFiles.length === 1 ? '' : 's'} changed this session)...`);
138
139
  await diffPanel.showFileDiffs(sessionFiles, 'HEAD');
139
140
  ctx.print(`Diff panel updated: ${sessionFiles.length} session file${sessionFiles.length === 1 ? '' : 's'}.`);
140
- enrichSemanticDiff(diffPanel, sessionFiles, 'HEAD', () => ctx.renderRequest(), workingDirectory).catch(() => {});
141
+ enrichSemanticDiff(diffPanel, sessionFiles, 'HEAD', () => ctx.renderRequest(), workingDirectory).catch((err) => { logger.debug('semantic diff enrichment failed', { err }); });
141
142
  } else {
142
143
  ctx.print('No session changes tracked yet. Showing diff vs HEAD...');
143
144
  await diffPanel.showGitDiff('HEAD');
@@ -148,7 +149,7 @@ export function registerDiffRuntimeCommands(registry: CommandRegistry): void {
148
149
  return (await new Response(proc.stdout).text()).trim().split('\n').filter(Boolean);
149
150
  })();
150
151
  if (fallbackFiles.length > 0) {
151
- enrichSemanticDiff(diffPanel, fallbackFiles, 'HEAD', () => ctx.renderRequest(), workingDirectory).catch(() => {});
152
+ enrichSemanticDiff(diffPanel, fallbackFiles, 'HEAD', () => ctx.renderRequest(), workingDirectory).catch((err) => { logger.debug('semantic diff enrichment failed', { err }); });
152
153
  }
153
154
  }
154
155
  break;
@@ -64,7 +64,7 @@ export function registerGuidanceRuntimeCommands(registry: CommandRegistry): void
64
64
  }
65
65
 
66
66
  const providerApi = requireProviderApi(ctx);
67
- const currentModel = await providerApi.getCurrentModel().catch(() => null);
67
+ const currentModel = await providerApi.getCurrentModel().catch(() => null); // best-effort: null handled as unknown context window
68
68
  const llmMessages = ctx.session.conversationManager.getMessagesForLLM();
69
69
  const readModels = requireReadModels(ctx);
70
70
  const session = readModels.session.getSnapshot();
@@ -240,7 +240,7 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
240
240
  if (sub === 'maintenance') {
241
241
  const session = readModels.session.getSnapshot();
242
242
  const providerApi = requireProviderApi(ctx);
243
- const currentModel = await providerApi.getCurrentModel().catch(() => null);
243
+ const currentModel = await providerApi.getCurrentModel().catch(() => null); // best-effort: null handled as unknown context window
244
244
  const llmMessages = typeof ctx.session.conversationManager.getMessagesForLLM === 'function'
245
245
  ? ctx.session.conversationManager.getMessagesForLLM()
246
246
  : [];
@@ -369,7 +369,7 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
369
369
 
370
370
  const session = readModels.session.getSnapshot();
371
371
  const providerApi = requireProviderApi(ctx);
372
- const currentModel = await providerApi.getCurrentModel().catch(() => null);
372
+ const currentModel = await providerApi.getCurrentModel().catch(() => null); // best-effort: null handled as unknown context window
373
373
  const llmMessages = typeof ctx.session.conversationManager.getMessagesForLLM === 'function'
374
374
  ? ctx.session.conversationManager.getMessagesForLLM()
375
375
  : [];
@@ -22,7 +22,7 @@ export async function buildSetupReviewSnapshot(ctx: CommandContext): Promise<Set
22
22
  }
23
23
  }
24
24
 
25
- const skills = discoverSkills(shellPaths);
25
+ const skills = await discoverSkills(shellPaths);
26
26
  const security = requireReadModels(ctx).security.getSnapshot();
27
27
  const plugins = security.plugins;
28
28
  const mcpServers = security.mcpServers;
@@ -161,7 +161,7 @@ export function registerSessionContentCommands(registry: CommandRegistry): void
161
161
 
162
162
  registry.register({
163
163
  name: 'undo',
164
- aliases: ['u'],
164
+ aliases: [],
165
165
  description: 'Undo last action. /undo file — revert last file write/edit. /undo — remove last conversation turn.',
166
166
  usage: '[file]',
167
167
  argsHint: '[file]',
@@ -5,6 +5,7 @@ import { REASONING_BUDGET_MAP } from '@pellux/goodvibes-sdk/platform/providers/i
5
5
  import { executeWriteQuit } from './quit-shared.ts';
6
6
  import { compactConversation, requireKeybindingsManager, requireProviderApi } from './runtime-services.ts';
7
7
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
8
+ import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
8
9
 
9
10
  export function registerShellCoreCommands(registry: CommandRegistry): void {
10
11
  registry.register({
@@ -34,7 +35,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
34
35
  ctx.platform.configManager.set('provider.model', selected.registryKey);
35
36
  ctx.platform.configManager.set('provider.provider', selected.providerId);
36
37
  ctx.print(`Switched to model: ${selected.displayName} (${selected.providerId})`);
37
- void providerApi.recordModelUsage(selected.registryKey).catch(() => undefined);
38
+ void providerApi.recordModelUsage(selected.registryKey).catch((err) => { logger.debug('model usage record failed', { err }); });
38
39
  } catch (e) {
39
40
  ctx.print(`Error: ${summarizeError(e)}`);
40
41
  }
@@ -202,7 +203,7 @@ export function registerShellCoreCommands(registry: CommandRegistry): void {
202
203
 
203
204
  registry.register({
204
205
  name: 'quit',
205
- aliases: ['q', ':q'],
206
+ aliases: [':q'],
206
207
  description: 'Exit the application',
207
208
  handler(_args, ctx) {
208
209
  ctx.exit();
@@ -19,7 +19,7 @@ export function registerSkillsRuntimeCommands(registry: CommandRegistry): void {
19
19
  aliases: ['skill'],
20
20
  description: 'Inspect installed skill packs',
21
21
  usage: '[open|list|show <name>|origins|browse [query]|installed|catalog-review <id>|publish-local <id> <path> <summary...>|unpublish <id>|install-hint <catalog-id>|install <id> [project|user]|update <id> [project|user]|uninstall <id> [project|user]]',
22
- handler(args, ctx) {
22
+ async handler(args, ctx) {
23
23
  const sub = args[0] ?? 'open';
24
24
  if (sub === 'open' || sub === 'panel') {
25
25
  if (ctx.showPanel) ctx.showPanel('skills');
@@ -31,7 +31,7 @@ export function registerSkillsRuntimeCommands(registry: CommandRegistry): void {
31
31
  }
32
32
  return;
33
33
  }
34
- const skills = discoverSkills(requireShellPaths(ctx));
34
+ const skills = await discoverSkills(requireShellPaths(ctx));
35
35
  const ecosystemPaths = requireEcosystemCatalogPaths(ctx);
36
36
  if (sub === 'list') {
37
37
  if (skills.length === 0) {
@@ -174,7 +174,7 @@ export function registerSubscriptionRuntimeCommands(registry: CommandRegistry):
174
174
  const openBrowser = !flags.has('--no-browser');
175
175
  const useManualMode = flags.has('--manual');
176
176
  if (provider === 'openai' && resolved.source === 'builtin') {
177
- const started = beginOpenAICodexLogin();
177
+ const started = await beginOpenAICodexLogin();
178
178
  manager.savePending({
179
179
  provider,
180
180
  state: started.state,
@@ -191,7 +191,7 @@ export function registerSubscriptionRuntimeCommands(registry: CommandRegistry):
191
191
  host: '127.0.0.1',
192
192
  port: 1455,
193
193
  path: '/auth/callback',
194
- }).catch(() => null);
194
+ }).catch(() => null); // best-effort: listener creation is optional; null triggers manual flow
195
195
  } catch {
196
196
  listener = null;
197
197
  }
@@ -272,14 +272,14 @@ export function registerSubscriptionRuntimeCommands(registry: CommandRegistry):
272
272
  host: resolved.oauth.localCallback.host,
273
273
  port: resolved.oauth.localCallback.port,
274
274
  path: resolved.oauth.localCallback.path,
275
- }).catch(() => null);
275
+ }).catch(() => null); // best-effort: local callback listener is optional; null falls back to manual redirect
276
276
  }
277
277
 
278
278
  if (listener) {
279
279
  activeConfig = { ...activeConfig, redirectUri: listener.redirectUri };
280
280
  }
281
281
 
282
- const started = manager.beginOAuthLogin(provider, activeConfig);
282
+ const started = await manager.beginOAuthLogin(provider, activeConfig);
283
283
  if (listener) {
284
284
  listener.setExpectedState(started.pending.state);
285
285
  }
@@ -81,7 +81,7 @@ import {
81
81
  } from './handler-picker-routes.ts';
82
82
  import { handleGlobalShortcutToken } from './handler-shortcuts.ts';
83
83
  import { feedInputTokens } from './handler-feed.ts';
84
- import { buildInitialFeedContext } from './feed-context-factory.ts';
84
+ import { buildInitialFeedContext, syncFeedContextMutableFields } from './feed-context-factory.ts';
85
85
  import { handlePanelIntegrationAction as runPanelIntegrationAction } from './panel-integration-actions.ts';
86
86
  import type { Panel } from '../panels/types.ts';
87
87
  import type { UiRuntimeServices } from '../runtime/ui-services.ts';
@@ -292,15 +292,13 @@ export class InputHandler {
292
292
 
293
293
  /** Sync mutable handler fields back into feedContext after in-feed mutations. */
294
294
  private syncFeedContextMutableFields(): void {
295
- const ctx = this.feedContext;
296
- ctx.prompt = this.prompt; ctx.cursorPos = this.cursorPos;
297
- ctx.commandMode = this.commandMode; ctx.panelFocused = this.panelFocused;
298
- ctx.indicatorFocused = this.indicatorFocused;
299
- ctx.helpOverlayActive = this.helpOverlayActive; ctx.helpScrollOffset = this.helpScrollOffset;
300
- ctx.shortcutsOverlayActive = this.shortcutsOverlayActive; ctx.shortcutsScrollOffset = this.shortcutsScrollOffset;
301
- ctx.selectionCallback = this.selectionCallback;
302
- ctx.nextPasteId = this.nextPasteId; ctx.nextImageId = this.nextImageId;
303
- ctx.mouseDownRow = this.mouseDownRow; ctx.mouseDownCol = this.mouseDownCol;
295
+ const h = this;
296
+ syncFeedContextMutableFields({ prompt: h.prompt, cursorPos: h.cursorPos, commandMode: h.commandMode,
297
+ panelFocused: h.panelFocused, indicatorFocused: h.indicatorFocused, helpOverlayActive: h.helpOverlayActive,
298
+ helpScrollOffset: h.helpScrollOffset, shortcutsOverlayActive: h.shortcutsOverlayActive,
299
+ shortcutsScrollOffset: h.shortcutsScrollOffset, selectionCallback: h.selectionCallback,
300
+ nextPasteId: h.nextPasteId, nextImageId: h.nextImageId, mouseDownRow: h.mouseDownRow,
301
+ mouseDownCol: h.mouseDownCol, contentWidth: h.contentWidth }, this.feedContext);
304
302
  }
305
303
 
306
304
  /** Wire in the InputHistory instance. Optional; disables history navigation if unset. */
@@ -1,4 +1,5 @@
1
1
  import type { CommandContext } from './command-registry.ts';
2
+ import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
2
3
  import type { Panel } from '../panels/types.ts';
3
4
  import type { PanelManager } from '../panels/panel-manager.ts';
4
5
  import { FileExplorerPanel } from '../panels/file-explorer-panel.ts';
@@ -69,7 +70,7 @@ export function handlePanelIntegrationAction(
69
70
  const parts = command.replace(/^\//, '').split(/\s+/).filter(Boolean);
70
71
  const [name, ...args] = parts;
71
72
  if (!name) return false;
72
- void commandContext.executeCommand(name, args).catch(() => {});
73
+ void commandContext.executeCommand(name, args).catch((err) => { logger.debug('approval panel command dispatch failed', { err }); });
73
74
  return true;
74
75
  }
75
76
 
@@ -0,0 +1,60 @@
1
+ import type { ConfigSetting } from '@pellux/goodvibes-sdk/platform/config/schema';
2
+ import type { ProviderAuthFreshness, ProviderAuthRoute } from '@pellux/goodvibes-sdk/platform/runtime/provider-accounts/registry';
3
+ import type { FeatureFlag, FlagState } from '@pellux/goodvibes-sdk/platform/runtime/feature-flags/types';
4
+
5
+ export type SettingsCategory = 'display' | 'ui' | 'provider' | 'subscriptions' | 'behavior' | 'storage' | 'permissions' | 'mcp' | 'sandbox' | 'danger' | 'tools' | 'flags' | 'network';
6
+
7
+ export const SETTINGS_CATEGORIES: SettingsCategory[] = [
8
+ 'display',
9
+ 'ui',
10
+ 'provider',
11
+ 'subscriptions',
12
+ 'behavior',
13
+ 'storage',
14
+ 'permissions',
15
+ 'mcp',
16
+ 'sandbox',
17
+ 'danger',
18
+ 'tools',
19
+ 'flags',
20
+ 'network',
21
+ ];
22
+
23
+ export interface SettingEntry {
24
+ setting: ConfigSetting;
25
+ currentValue: unknown;
26
+ isDefault: boolean;
27
+ effectiveSource?: 'default' | 'local' | 'synced' | 'managed';
28
+ locked?: boolean;
29
+ conflict?: boolean;
30
+ sourceLabel?: string;
31
+ lockReason?: string;
32
+ }
33
+
34
+ export interface FlagEntry {
35
+ flag: FeatureFlag;
36
+ state: FlagState;
37
+ }
38
+
39
+ export interface McpEntry {
40
+ name: string;
41
+ connected: boolean;
42
+ role: 'general' | 'docs' | 'filesystem' | 'git' | 'database' | 'browser' | 'automation' | 'ops' | 'remote';
43
+ trustMode: 'constrained' | 'ask-on-risk' | 'allow-all' | 'blocked';
44
+ allowedPaths: string[];
45
+ allowedHosts: string[];
46
+ }
47
+
48
+ export interface SubscriptionEntry {
49
+ provider: string;
50
+ state: 'active' | 'pending' | 'available';
51
+ tokenType?: string;
52
+ expiresAt?: number;
53
+ oauthConfigured: boolean;
54
+ activeRoute?: ProviderAuthRoute;
55
+ preferredRoute?: ProviderAuthRoute;
56
+ authFreshness?: ProviderAuthFreshness;
57
+ routeReason?: string;
58
+ issues?: string[];
59
+ nextActions?: string[];
60
+ }
@@ -23,67 +23,23 @@ import type { FeatureFlag, FlagState } from '@pellux/goodvibes-sdk/platform/runt
23
23
  import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp/registry';
24
24
  import { logger } from '@pellux/goodvibes-sdk/platform/utils/logger';
25
25
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils/error-display';
26
-
27
- // ---------------------------------------------------------------------------
28
- // Types
29
- // ---------------------------------------------------------------------------
30
-
31
- export type SettingsCategory = 'display' | 'ui' | 'provider' | 'subscriptions' | 'behavior' | 'storage' | 'permissions' | 'mcp' | 'sandbox' | 'danger' | 'tools' | 'flags';
32
-
33
- export const SETTINGS_CATEGORIES: SettingsCategory[] = [
34
- 'display',
35
- 'ui',
36
- 'provider',
37
- 'subscriptions',
38
- 'behavior',
39
- 'storage',
40
- 'permissions',
41
- 'mcp',
42
- 'sandbox',
43
- 'danger',
44
- 'tools',
45
- 'flags',
46
- ];
47
-
48
- export interface SettingEntry {
49
- setting: ConfigSetting;
50
- currentValue: unknown;
51
- isDefault: boolean;
52
- effectiveSource?: 'default' | 'local' | 'synced' | 'managed';
53
- locked?: boolean;
54
- conflict?: boolean;
55
- sourceLabel?: string;
56
- lockReason?: string;
57
- }
58
-
59
- /** A single feature flag entry for the flags tab. */
60
- export interface FlagEntry {
61
- flag: FeatureFlag;
62
- state: FlagState;
63
- }
64
-
65
- export interface McpEntry {
66
- name: string;
67
- connected: boolean;
68
- role: 'general' | 'docs' | 'filesystem' | 'git' | 'database' | 'browser' | 'automation' | 'ops' | 'remote';
69
- trustMode: 'constrained' | 'ask-on-risk' | 'allow-all' | 'blocked';
70
- allowedPaths: string[];
71
- allowedHosts: string[];
72
- }
73
-
74
- export interface SubscriptionEntry {
75
- provider: string;
76
- state: 'active' | 'pending' | 'available';
77
- tokenType?: string;
78
- expiresAt?: number;
79
- oauthConfigured: boolean;
80
- activeRoute?: ProviderAuthRoute;
81
- preferredRoute?: ProviderAuthRoute;
82
- authFreshness?: ProviderAuthFreshness;
83
- routeReason?: string;
84
- issues?: string[];
85
- nextActions?: string[];
86
- }
26
+ import {
27
+ SETTINGS_CATEGORIES,
28
+ type FlagEntry,
29
+ type McpEntry,
30
+ type SettingEntry,
31
+ type SettingsCategory,
32
+ type SubscriptionEntry,
33
+ } from './settings-modal-types.ts';
34
+
35
+ export {
36
+ SETTINGS_CATEGORIES,
37
+ type FlagEntry,
38
+ type McpEntry,
39
+ type SettingEntry,
40
+ type SettingsCategory,
41
+ type SubscriptionEntry,
42
+ } from './settings-modal-types.ts';
87
43
 
88
44
  /**
89
45
  * Map a config key to the model picker target it should open, or null if the
@@ -151,6 +107,13 @@ export class SettingsModal {
151
107
  /** Provider subscription entries (populated when subscriptions tab is active). */
152
108
  public subscriptionEntries: SubscriptionEntry[] = [];
153
109
 
110
+ /**
111
+ * Set after a network-category save that touches controlPlane or httpListener
112
+ * config keys. Renderer reads this to display a transient restart notice.
113
+ * Cleared on next open() or close().
114
+ */
115
+ public lastSaveTriggeredRestart: 'control-plane' | 'http-listener' | 'web' | null = null;
116
+
154
117
  private configManager: ConfigManager | null = null;
155
118
  private featureFlagManager: FeatureFlagManager | null = null;
156
119
  private mcpRegistry: McpRegistry | null = null;
@@ -185,6 +148,7 @@ export class SettingsModal {
185
148
  this.editBuffer = '';
186
149
  this.mcpAllowAllConfirmationTarget = null;
187
150
  this.subscriptionLogoutConfirmationTarget = null;
151
+ this.lastSaveTriggeredRestart = null;
188
152
  this.active = true;
189
153
  }
190
154
 
@@ -194,6 +158,7 @@ export class SettingsModal {
194
158
  this.editBuffer = '';
195
159
  this.mcpAllowAllConfirmationTarget = null;
196
160
  this.subscriptionLogoutConfirmationTarget = null;
161
+ this.lastSaveTriggeredRestart = null;
197
162
  this.serviceRegistry = null;
198
163
  }
199
164
 
@@ -548,7 +513,15 @@ export class SettingsModal {
548
513
  for (const setting of CONFIG_SCHEMA) {
549
514
  const rawCat = setting.key.split('.')[0] as string;
550
515
  // Route helper.* settings into the tools group for unified display
551
- const cat = (rawCat === 'helper' ? 'tools' : rawCat) as SettingsCategory;
516
+ // Route controlPlane.* and httpListener.* into the network group
517
+ let cat: SettingsCategory;
518
+ if (rawCat === 'helper') {
519
+ cat = 'tools';
520
+ } else if (rawCat === 'controlPlane' || rawCat === 'httpListener' || rawCat === 'web') {
521
+ cat = 'network';
522
+ } else {
523
+ cat = rawCat as SettingsCategory;
524
+ }
552
525
  if (!this.groups.has(cat)) continue;
553
526
  const currentValue = configManager.get(setting.key as ConfigKey);
554
527
  const resolved = getResolvedSettingLookup(configManager, setting.key as ConfigKey)?.entry;
@@ -720,17 +693,62 @@ export class SettingsModal {
720
693
  /** Returns [] for the flags category (flags use flagEntries instead). */
721
694
  private _currentItems(): SettingEntry[] {
722
695
  if (this.currentCategory === 'flags' || this.currentCategory === 'mcp' || this.currentCategory === 'subscriptions') return [];
723
- return this.groups.get(this.currentCategory) ?? [];
696
+ const items = this.groups.get(this.currentCategory) ?? [];
697
+ if (this.currentCategory === 'network') {
698
+ // Hide host fields when the corresponding hostMode is not 'custom'
699
+ return items.filter(entry => {
700
+ if (entry.setting.key === 'controlPlane.host') {
701
+ const hostMode = this.configManager?.get('controlPlane.hostMode');
702
+ return hostMode === 'custom';
703
+ }
704
+ if (entry.setting.key === 'httpListener.host') {
705
+ const hostMode = this.configManager?.get('httpListener.hostMode');
706
+ return hostMode === 'custom';
707
+ }
708
+ if (entry.setting.key === 'web.host') {
709
+ const hostMode = this.configManager?.get('web.hostMode');
710
+ return hostMode === 'custom';
711
+ }
712
+ return true;
713
+ });
714
+ }
715
+ return items;
724
716
  }
725
717
 
726
718
  private _setValue(key: ConfigKey, value: unknown): void {
727
719
  if (!this.configManager) return;
720
+ // Diff previous value before writing — avoids false restart notices on no-op saves
721
+ const previousValue = this.configManager.get(key);
722
+ const isRestartKey = ['host', 'port', 'hostMode', 'enabled'].includes(key.split('.')[1] ?? '');
728
723
  try {
729
724
  this.configManager.setDynamic(key, value);
730
725
  // Update the cached entry in-place — avoids full schema re-scan on each edit
731
726
  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;
727
+ // Resolve the display category from the key prefix
728
+ let cat: SettingsCategory;
729
+ if (rawCat === 'helper') {
730
+ cat = 'tools';
731
+ } else if (rawCat === 'controlPlane') {
732
+ cat = 'network';
733
+ // SDK auto-restarts the daemon server on controlPlane binding changes
734
+ if (isRestartKey && previousValue !== value) {
735
+ this.lastSaveTriggeredRestart = 'control-plane';
736
+ }
737
+ } else if (rawCat === 'httpListener') {
738
+ cat = 'network';
739
+ // SDK auto-restarts the HTTP listener on binding changes
740
+ if (isRestartKey && previousValue !== value) {
741
+ this.lastSaveTriggeredRestart = 'http-listener';
742
+ }
743
+ } else if (rawCat === 'web') {
744
+ cat = 'network';
745
+ // SDK auto-restarts the web server on binding changes
746
+ if (isRestartKey && previousValue !== value) {
747
+ this.lastSaveTriggeredRestart = 'web';
748
+ }
749
+ } else {
750
+ cat = rawCat as SettingsCategory;
751
+ }
734
752
  const entries = this.groups.get(cat);
735
753
  if (entries) {
736
754
  const entry = entries.find(e => e.setting.key === key);