@pellux/goodvibes-agent 0.1.8 → 0.1.10

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 (39) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -1
  3. package/docs/getting-started.md +1 -1
  4. package/docs/release-and-publishing.md +2 -2
  5. package/package.json +4 -1
  6. package/src/cli/agent-knowledge-command.ts +43 -21
  7. package/src/cli/help.ts +17 -4
  8. package/src/cli/management-commands.ts +3 -3
  9. package/src/cli/management.ts +7 -1
  10. package/src/cli/parser.ts +3 -0
  11. package/src/cli/service-posture.ts +6 -6
  12. package/src/cli/status.ts +9 -9
  13. package/src/cli/surface-command.ts +3 -3
  14. package/src/cli/types.ts +2 -0
  15. package/src/input/commands/experience-runtime.ts +1 -1
  16. package/src/input/commands/hooks-runtime.ts +30 -2
  17. package/src/input/handler.ts +1 -0
  18. package/src/input/onboarding/onboarding-runtime-status.ts +8 -8
  19. package/src/input/onboarding/onboarding-wizard-apply.ts +13 -53
  20. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +4 -4
  21. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +1 -1
  22. package/src/input/onboarding/onboarding-wizard-constants.ts +7 -7
  23. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +4 -4
  24. package/src/input/onboarding/onboarding-wizard-steps.ts +13 -13
  25. package/src/input/settings-modal-agent-policy.ts +18 -0
  26. package/src/input/settings-modal-types.ts +17 -0
  27. package/src/input/settings-modal.ts +30 -29
  28. package/src/main.ts +3 -26
  29. package/src/renderer/process-indicator.ts +7 -6
  30. package/src/renderer/process-modal.ts +17 -8
  31. package/src/renderer/settings-modal.ts +12 -8
  32. package/src/renderer/ui-factory.ts +4 -32
  33. package/src/runtime/bootstrap-shell.ts +0 -13
  34. package/src/runtime/bootstrap.ts +0 -10
  35. package/src/runtime/onboarding/derivation.ts +6 -6
  36. package/src/verification/live-verifier.ts +148 -13
  37. package/src/version.ts +10 -3
  38. package/src/input/commands/quit-shared.ts +0 -162
  39. package/src/renderer/git-status.ts +0 -89
@@ -26,7 +26,7 @@ export function buildCloudflareStep(controller: OnboardingWizardController): Onb
26
26
  const bind = controller.runtimeSnapshot?.bindSettings.controlPlane;
27
27
  const defaultDaemonBaseUrl = normalizeText(config?.daemonBaseUrl)
28
28
  || `http://${bind?.host && bind.host !== '0.0.0.0' && bind.host !== '::' ? bind.host : '127.0.0.1'}:${bind?.port ?? 3421}`;
29
- const resultMessage = controller.textState.get('cloudflare.action-status') ?? 'No Cloudflare daemon action has run in this wizard session.';
29
+ const resultMessage = controller.textState.get('cloudflare.action-status') ?? 'No Cloudflare action has run in this wizard session.';
30
30
  const fields: OnboardingWizardFieldDefinition[] = [
31
31
  {
32
32
  kind: 'checklist',
@@ -96,7 +96,7 @@ export function buildCloudflareStep(controller: OnboardingWizardController): Onb
96
96
  kind: 'text',
97
97
  id: 'cloudflare.bootstrap-env-name',
98
98
  label: 'Bootstrap token environment variable',
99
- hint: 'The TUI reads this environment variable once and passes the value to the SDK token-create route. It is not persisted.',
99
+ hint: 'Agent reads this environment variable once and passes the value to the SDK token-create route. It is not persisted.',
100
100
  placeholder: 'GOODVIBES_CLOUDFLARE_BOOTSTRAP_TOKEN',
101
101
  defaultValue: 'GOODVIBES_CLOUDFLARE_BOOTSTRAP_TOKEN',
102
102
  });
@@ -227,8 +227,8 @@ export function buildCloudflareStep(controller: OnboardingWizardController): Onb
227
227
  id: 'cloudflare.tunnel-name',
228
228
  label: 'Tunnel name',
229
229
  hint: 'Cloudflare Tunnel name to create or reuse.',
230
- placeholder: 'goodvibes-daemon',
231
- defaultValue: config?.tunnelName || 'goodvibes-daemon',
230
+ placeholder: 'goodvibes-agent-daemon',
231
+ defaultValue: config?.tunnelName || 'goodvibes-agent-daemon',
232
232
  },
233
233
  {
234
234
  kind: 'text',
@@ -176,7 +176,7 @@ export function buildCloudflareProvisionRequest(controller: OnboardingWizardCont
176
176
  daemonHostname: controller.getStringFieldValue('cloudflare.daemon-hostname', controller.runtimeSnapshot?.config.cloudflare.daemonHostname ?? ''),
177
177
  queueName: controller.getStringFieldValue('cloudflare.queue-name', controller.runtimeSnapshot?.config.cloudflare.queueName ?? 'goodvibes-batch'),
178
178
  deadLetterQueueName: controller.getStringFieldValue('cloudflare.dead-letter-queue-name', controller.runtimeSnapshot?.config.cloudflare.deadLetterQueueName ?? 'goodvibes-batch-dlq'),
179
- tunnelName: controller.getStringFieldValue('cloudflare.tunnel-name', controller.runtimeSnapshot?.config.cloudflare.tunnelName ?? 'goodvibes-daemon'),
179
+ tunnelName: controller.getStringFieldValue('cloudflare.tunnel-name', controller.runtimeSnapshot?.config.cloudflare.tunnelName ?? 'goodvibes-agent-daemon'),
180
180
  tunnelId: controller.getStringFieldValue('cloudflare.tunnel-id', controller.runtimeSnapshot?.config.cloudflare.tunnelId ?? ''),
181
181
  tunnelServiceUrl: controller.getStringFieldValue('cloudflare.tunnel-service-url', ''),
182
182
  tunnelTokenRef: controller.getStringFieldValue('cloudflare.tunnel-token-ref', controller.runtimeSnapshot?.config.cloudflare.tunnelTokenRef ?? ''),
@@ -30,13 +30,13 @@ export const DEFAULT_CAPABILITIES: readonly OnboardingStep1CapabilityItem[] = [
30
30
  id: 'network-access',
31
31
  label: 'Let other devices use GoodVibes',
32
32
  selected: false,
33
- detail: 'Make enabled GoodVibes services reachable from other devices on your LAN. Local authentication is required.',
33
+ detail: 'Review external daemon surfaces that are reachable from other devices on your LAN. Local authentication is required.',
34
34
  },
35
35
  {
36
36
  id: 'webhook-events',
37
37
  label: 'Receive webhooks or events from other tools',
38
38
  selected: false,
39
- detail: 'Turn on the HTTP listener for incoming webhooks, callbacks, and automation events.',
39
+ detail: 'Review the external HTTP listener required for incoming webhooks, callbacks, and automation events.',
40
40
  },
41
41
  {
42
42
  id: 'external-integrations',
@@ -48,7 +48,7 @@ export const DEFAULT_CAPABILITIES: readonly OnboardingStep1CapabilityItem[] = [
48
48
  id: 'cloudflare-batch',
49
49
  label: 'Use Cloudflare for batch or remote daemon work',
50
50
  selected: false,
51
- detail: 'Optionally configure Cloudflare Workers and Queues for explicit or eligible background batch jobs. Immediate local daemon behavior stays the default unless enabled.',
51
+ detail: 'Optionally configure Cloudflare Workers and Queues for explicit or eligible background batch jobs. The external daemon still owns execution.',
52
52
  },
53
53
  ];
54
54
 
@@ -60,13 +60,13 @@ export const REASONING_OPTIONS: readonly OnboardingWizardRadioOption[] = [
60
60
  ];
61
61
 
62
62
  export const NETWORK_MODE_OPTIONS: readonly OnboardingWizardRadioOption[] = [
63
- { id: 'local-network-default', label: 'Local Network (Default)', hint: 'Use the default LAN-facing setup for enabled browser, service, and event features.' },
64
- { id: 'custom', label: 'Custom', hint: 'Choose IP addresses and ports for each enabled service.' },
63
+ { id: 'local-network-default', label: 'Local Network (Default)', hint: 'Review the default LAN-facing external daemon setup for browser, control-plane, and event features.' },
64
+ { id: 'custom', label: 'Custom', hint: 'Review IP addresses and ports for each external daemon surface.' },
65
65
  ];
66
66
 
67
67
  export const HITL_MODE_OPTIONS: readonly OnboardingWizardRadioOption[] = [
68
68
  { id: 'quiet', label: 'Quiet', hint: 'Only interrupt for important attention requests.' },
69
- { id: 'balanced', label: 'Balanced', hint: 'Show important activity without turning the TUI into a log stream.' },
69
+ { id: 'balanced', label: 'Balanced', hint: 'Show important activity without turning Agent into a log stream.' },
70
70
  { id: 'operator', label: 'Operator', hint: 'Keep operational activity visible for hands-on supervision.' },
71
71
  ];
72
72
 
@@ -90,7 +90,7 @@ export const SECRET_POLICY_OPTIONS: readonly OnboardingWizardRadioOption[] = [
90
90
 
91
91
  export const TELEGRAM_MODE_OPTIONS: readonly OnboardingWizardRadioOption[] = [
92
92
  { id: 'webhook', label: 'Webhook', hint: 'Receive Telegram updates through a webhook.' },
93
- { id: 'polling', label: 'Polling', hint: 'Poll Telegram for updates from the background service.' },
93
+ { id: 'polling', label: 'Polling', hint: 'The external daemon polls Telegram for updates.' },
94
94
  ];
95
95
 
96
96
  export const WHATSAPP_PROVIDER_OPTIONS: readonly OnboardingWizardRadioOption[] = [
@@ -105,8 +105,8 @@ export const HOME_ASSISTANT_SURFACE_SPEC: ExternalSurfaceSpec = {
105
105
  kind: 'text',
106
106
  label: 'Home Assistant device ID',
107
107
  hint: 'Stable device identifier exposed by the GoodVibes daemon.',
108
- placeholder: 'goodvibes-daemon',
109
- defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.deviceId ?? 'goodvibes-daemon',
108
+ placeholder: 'goodvibes-agent',
109
+ defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.deviceId ?? 'goodvibes-agent',
110
110
  },
111
111
  {
112
112
  id: 'external-services.homeassistant.device-name',
@@ -114,8 +114,8 @@ export const HOME_ASSISTANT_SURFACE_SPEC: ExternalSurfaceSpec = {
114
114
  kind: 'text',
115
115
  label: 'Home Assistant device name',
116
116
  hint: 'Display name for the GoodVibes daemon device in Home Assistant.',
117
- placeholder: 'GoodVibes Daemon',
118
- defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.deviceName ?? 'GoodVibes Daemon',
117
+ placeholder: 'GoodVibes Agent',
118
+ defaultValue: (snapshot) => snapshot?.config.surfaces.homeassistant.deviceName ?? 'GoodVibes Agent',
119
119
  },
120
120
  {
121
121
  id: 'external-services.homeassistant.event-type',
@@ -436,7 +436,7 @@ function buildExternalSurfaceStep(
436
436
  id: `external-surface:${surface.id}` as OnboardingWizardExternalSurfaceStepId,
437
437
  title,
438
438
  shortLabel: surface.label.replace(/ surface$/i, ''),
439
- description: `Configure ${surface.label}. Settings are saved either way; Agent does not start or own the background service.`,
439
+ description: `Configure ${surface.label}. Settings are saved either way; Agent does not start or own the external daemon service.`,
440
440
  summaryTitle: `${surface.label} setup`,
441
441
  summaryLines: [
442
442
  `External activation requested: ${autoStartValue === 'yes' ? 'yes' : 'no'}`,
@@ -540,8 +540,8 @@ export function buildNetworkStep(controller: OnboardingWizardController): Onboar
540
540
  const sharedIpField: OnboardingWizardChecklistFieldDefinition = {
541
541
  kind: 'checklist',
542
542
  id: 'network.shared-ip',
543
- label: 'Use the same IP address for all services',
544
- hint: 'When included, browser, GoodVibes service, and webhook listener network bindings share one IP address.',
543
+ label: 'Use the same IP address for all external daemon surfaces',
544
+ hint: 'When included, browser, external daemon control plane, and webhook listener network bindings share one IP address in the daemon host configuration.',
545
545
  defaultValue: controller.getSharedIpDefault(networkEnabled),
546
546
  };
547
547
  const sharedIp = controller.getBooleanFieldValue(sharedIpField.id, sharedIpField.defaultValue);
@@ -561,8 +561,8 @@ export function buildNetworkStep(controller: OnboardingWizardController): Onboar
561
561
  fields.push({
562
562
  kind: 'text',
563
563
  id: 'network.service-port',
564
- label: 'GoodVibes service port',
565
- hint: 'Port for the background service and control plane.',
564
+ label: 'External daemon control-plane port',
565
+ hint: 'Port exposed by the external daemon control plane.',
566
566
  placeholder: '3421',
567
567
  defaultValue: String(bindSettings?.controlPlane.port ?? 3421),
568
568
  });
@@ -570,8 +570,8 @@ export function buildNetworkStep(controller: OnboardingWizardController): Onboar
570
570
  fields.push({
571
571
  kind: 'text',
572
572
  id: 'network.service-ip',
573
- label: 'GoodVibes service IP address',
574
- hint: 'IP address for the background service and control plane.',
573
+ label: 'External daemon control-plane IP address',
574
+ hint: 'IP address exposed by the external daemon control plane.',
575
575
  placeholder: '0.0.0.0',
576
576
  defaultValue: normalizeText(bindSettings?.controlPlane.host) || '0.0.0.0',
577
577
  });
@@ -625,7 +625,7 @@ export function buildNetworkStep(controller: OnboardingWizardController): Onboar
625
625
  id: 'network',
626
626
  title: 'Network setup',
627
627
  shortLabel: 'Network',
628
- description: 'Choose the LAN default or customize IP addresses and ports for the enabled browser, service, and listener surfaces.',
628
+ description: 'Review LAN defaults or IP addresses and ports for the external daemon browser, control-plane, and listener surfaces. Agent does not apply daemon bind changes.',
629
629
  summaryTitle: 'Bind posture',
630
630
  summaryLines: [
631
631
  `Mode: ${custom ? 'custom' : 'local network default'}`,
@@ -653,7 +653,7 @@ export function buildAccountsStep(controller: OnboardingWizardController): Onboa
653
653
  id: 'accounts.admin-username',
654
654
  label: 'Local auth admin username',
655
655
  hint: needsAuthBootstrap
656
- ? 'Required before any background service, browser surface, or listener is exposed.'
656
+ ? 'Required before the external daemon exposes browser, control-plane, or listener surfaces.'
657
657
  : 'Optional. Enter an existing admin username to rotate its password, or a new username to create another admin.',
658
658
  placeholder: defaultAdminUsername,
659
659
  defaultValue: defaultAdminUsername,
@@ -666,7 +666,7 @@ export function buildAccountsStep(controller: OnboardingWizardController): Onboa
666
666
  hint: needsAuthBootstrap
667
667
  ? controller.hasBootstrapCredentialPresent()
668
668
  ? 'Creates or updates the named local admin, removes the bootstrap credential file, and retires the bootstrap admin when it is a different user.'
669
- : 'Creates the first local admin user and an initial session before LAN/server settings are applied.'
669
+ : 'Creates the first local admin user and an initial session before external daemon network settings are used by the daemon owner.'
670
670
  : 'Optional. Leave blank to keep existing local auth unchanged; enter a password to create or rotate the named admin user.',
671
671
  placeholder: needsAuthBootstrap ? 'password required' : 'leave blank to keep unchanged',
672
672
  defaultValue: '',
@@ -724,7 +724,7 @@ export function buildAccountsStep(controller: OnboardingWizardController): Onboa
724
724
  title: 'Subscriptions and auth review',
725
725
  shortLabel: 'Accounts',
726
726
  description: needsAuthBootstrap
727
- ? 'Create wizard-owned local auth before any LAN, browser, service, or listener settings are applied.'
727
+ ? 'Create local auth state before external daemon LAN, browser, service, or listener settings are applied by the daemon owner.'
728
728
  : 'Review existing subscription and local auth state. Existing local auth is kept unless you change it elsewhere.',
729
729
  summaryTitle: 'Stored account state',
730
730
  summaryLines: [
@@ -732,8 +732,8 @@ export function buildAccountsStep(controller: OnboardingWizardController): Onboa
732
732
  `Auth: ${auth?.userCount ?? 0} users / ${auth?.sessionCount ?? 0} sessions`,
733
733
  needsAuthBootstrap
734
734
  ? controller.hasBootstrapCredentialPresent()
735
- ? 'Bootstrap credentials will be replaced before network settings are applied'
736
- : 'Local admin will be created before network settings are applied'
735
+ ? 'Bootstrap credentials will be replaced before external network settings are used'
736
+ : 'Local admin will be created before external network settings are used'
737
737
  : controller.hasLocalAuthUser() ? 'Existing local auth will be kept' : 'Local auth is not required for this setup',
738
738
  ],
739
739
  fields,
@@ -0,0 +1,18 @@
1
+ export const AGENT_EXTERNAL_DAEMON_SETTING_LOCK_REASON = 'GoodVibes Agent connects to an external daemon. Change this from GoodVibes TUI or the daemon host; Agent settings are read-only for daemon lifecycle and bind posture.';
2
+
3
+ const EXTERNAL_DAEMON_SETTING_PREFIXES = [
4
+ 'service.',
5
+ 'controlPlane.',
6
+ 'httpListener.',
7
+ 'web.',
8
+ ] as const;
9
+
10
+ const EXTERNAL_DAEMON_SETTING_KEYS = new Set<string>([
11
+ 'danger.daemon',
12
+ 'danger.httpListener',
13
+ ]);
14
+
15
+ export function isExternalDaemonOwnedSettingKey(key: string): boolean {
16
+ return EXTERNAL_DAEMON_SETTING_KEYS.has(key)
17
+ || EXTERNAL_DAEMON_SETTING_PREFIXES.some((prefix) => key.startsWith(prefix));
18
+ }
@@ -1,7 +1,24 @@
1
1
  import type { ConfigSetting } from '@pellux/goodvibes-sdk/platform/config';
2
+ import type { ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
2
3
  import type { ProviderAuthFreshness, ProviderAuthRoute } from '@/runtime/index.ts';
3
4
  import type { FeatureFlag, FlagState } from '@/runtime/index.ts';
4
5
 
6
+ export interface SettingsModalChange {
7
+ readonly key: ConfigKey;
8
+ readonly previousValue: unknown;
9
+ readonly value: unknown;
10
+ }
11
+
12
+ export interface SettingsModalChangeResult {
13
+ readonly message?: string;
14
+ }
15
+
16
+ export type SettingsModalChangeHandler = (change: SettingsModalChange) => SettingsModalChangeResult | void;
17
+
18
+ export interface SettingsModalOpenOptions {
19
+ readonly onSettingApplied?: SettingsModalChangeHandler;
20
+ }
21
+
5
22
  export type SettingsCategory =
6
23
  | 'display'
7
24
  | 'ui'
@@ -1,14 +1,4 @@
1
- /**
2
- * SettingsModal — state management for the /settings and /config fullscreen workspace.
3
- *
4
- * Loads CONFIG_SCHEMA, groups settings by category, and tracks UI state:
5
- * - Active category (Tab to cycle)
6
- * - Selected setting index within category (↑↓)
7
- * - Editing mode for inline string/number input
8
- * - Feature flags tab with runtime toggle support
9
- *
10
- * Saves changes via configManager.set(key, value) or featureFlagManager methods.
11
- */
1
+ /** SettingsModal state for the /settings and /config fullscreen workspace. */
12
2
 
13
3
  import { CONFIG_SCHEMA, type ConfigKey, type PersistedFlagState } from '@pellux/goodvibes-sdk/platform/config';
14
4
  import type { ModelPickerTarget } from './model-picker.ts';
@@ -40,24 +30,11 @@ import {
40
30
  type SettingEntry,
41
31
  type SettingsCategory,
42
32
  type SettingsFocusPane,
33
+ type SettingsModalChangeHandler,
34
+ type SettingsModalOpenOptions,
43
35
  type SubscriptionEntry,
44
36
  } from './settings-modal-types.ts';
45
-
46
- export interface SettingsModalChange {
47
- readonly key: ConfigKey;
48
- readonly previousValue: unknown;
49
- readonly value: unknown;
50
- }
51
-
52
- export interface SettingsModalChangeResult {
53
- readonly message?: string;
54
- }
55
-
56
- export type SettingsModalChangeHandler = (change: SettingsModalChange) => SettingsModalChangeResult | void;
57
-
58
- export interface SettingsModalOpenOptions {
59
- readonly onSettingApplied?: SettingsModalChangeHandler;
60
- }
37
+ import { AGENT_EXTERNAL_DAEMON_SETTING_LOCK_REASON, isExternalDaemonOwnedSettingKey } from './settings-modal-agent-policy.ts';
61
38
 
62
39
  export {
63
40
  SETTINGS_CATEGORIES,
@@ -67,8 +44,13 @@ export {
67
44
  type SettingEntry,
68
45
  type SettingsCategory,
69
46
  type SettingsFocusPane,
47
+ type SettingsModalChange,
48
+ type SettingsModalChangeHandler,
49
+ type SettingsModalChangeResult,
50
+ type SettingsModalOpenOptions,
70
51
  type SubscriptionEntry,
71
52
  } from './settings-modal-types.ts';
53
+ export { AGENT_EXTERNAL_DAEMON_SETTING_LOCK_REASON, isExternalDaemonOwnedSettingKey } from './settings-modal-agent-policy.ts';
72
54
 
73
55
  // ---------------------------------------------------------------------------
74
56
  // SettingsModal
@@ -369,6 +351,7 @@ export class SettingsModal {
369
351
 
370
352
  const entry = this.getSelected();
371
353
  if (!entry || !this.configManager) return;
354
+ if (this._blockExternalDaemonOwnedSetting(entry)) return;
372
355
 
373
356
  const { setting } = entry;
374
357
 
@@ -433,6 +416,7 @@ export class SettingsModal {
433
416
 
434
417
  const entry = this.getSelected();
435
418
  if (!entry || !this.configManager) return;
419
+ if (this._blockExternalDaemonOwnedSetting(entry)) return;
436
420
  const { setting } = entry;
437
421
 
438
422
  if (setting.type === 'boolean') {
@@ -554,6 +538,11 @@ export class SettingsModal {
554
538
 
555
539
  const entry = this.getSelected();
556
540
  if (!entry || !this.configManager) return false;
541
+ if (this._blockExternalDaemonOwnedSetting(entry)) {
542
+ this.editingMode = false;
543
+ this.editBuffer = '';
544
+ return false;
545
+ }
557
546
 
558
547
  const { setting } = entry;
559
548
  let parsed: unknown = this.editBuffer;
@@ -600,6 +589,7 @@ export class SettingsModal {
600
589
  if (this.editingMode || !this.configManager) return null;
601
590
  const entry = this.getSelected();
602
591
  if (!entry) return null;
592
+ if (this._blockExternalDaemonOwnedSetting(entry)) return null;
603
593
  const key = entry.setting.key as ConfigKey;
604
594
  this._setValue(key, entry.setting.default);
605
595
  if (isSecretConfigKey(key) && this.secretsManager) {
@@ -635,15 +625,16 @@ export class SettingsModal {
635
625
  const cat = rawCat as SettingsCategory;
636
626
  const currentValue = configManager.get(setting.key as ConfigKey);
637
627
  const resolved = getResolvedSettingLookup(configManager, setting.key as ConfigKey)?.entry;
628
+ const daemonOwned = isExternalDaemonOwnedSettingKey(setting.key);
638
629
  const entry: SettingEntry = {
639
630
  setting,
640
631
  currentValue,
641
632
  isDefault: currentValue === setting.default,
642
633
  effectiveSource: resolved?.effectiveSource,
643
- locked: resolved?.locked,
634
+ locked: daemonOwned || resolved?.locked,
644
635
  conflict: resolved?.conflict,
645
636
  sourceLabel: resolved?.sourceLabel,
646
- lockReason: resolved?.lockReason,
637
+ lockReason: daemonOwned ? AGENT_EXTERNAL_DAEMON_SETTING_LOCK_REASON : resolved?.lockReason,
647
638
  };
648
639
  if (this.groups.has(cat)) this.groups.get(cat)!.push(entry);
649
640
  if ((rawCat === 'controlPlane' || rawCat === 'httpListener' || rawCat === 'web') && this.groups.has('network')) {
@@ -752,6 +743,10 @@ export class SettingsModal {
752
743
 
753
744
  private _setValue(key: ConfigKey, value: unknown): void {
754
745
  if (!this.configManager) return;
746
+ if (isExternalDaemonOwnedSettingKey(key)) {
747
+ this.lastSettingEffectMessage = AGENT_EXTERNAL_DAEMON_SETTING_LOCK_REASON;
748
+ return;
749
+ }
755
750
  // Diff previous value before writing — avoids false restart notices on no-op saves
756
751
  const previousValue = this.configManager.get(key);
757
752
  const isRestartKey = ['host', 'port', 'hostMode', 'enabled'].includes(key.split('.')[1] ?? '');
@@ -790,4 +785,10 @@ export class SettingsModal {
790
785
  }
791
786
  }
792
787
 
788
+ private _blockExternalDaemonOwnedSetting(entry: SettingEntry): boolean {
789
+ if (!isExternalDaemonOwnedSettingKey(entry.setting.key)) return false;
790
+ this.lastSettingEffectMessage = entry.lockReason ?? AGENT_EXTERNAL_DAEMON_SETTING_LOCK_REASON;
791
+ return true;
792
+ }
793
+
793
794
  }
package/src/main.ts CHANGED
@@ -20,8 +20,6 @@ import { registerBuiltinCommands } from './input/commands.ts';
20
20
  import { ScheduleManager } from '@pellux/goodvibes-sdk/platform/tools';
21
21
  import { InputHistory } from './input/input-history.ts';
22
22
  import { getTierPromptSupplement, getTierForContextWindow } from '@pellux/goodvibes-sdk/platform/providers';
23
- import { GitStatusProvider } from './renderer/git-status.ts';
24
- import type { GitHeaderInfo } from './renderer/git-status.ts';
25
23
  import { createShellLayout } from './renderer/layout-engine.ts';
26
24
  import { buildShellFooter, estimateShellFooterHeight } from './renderer/shell-surface.ts';
27
25
  import { buildConversationViewport } from './renderer/conversation-layout.ts';
@@ -53,7 +51,6 @@ import { attachSpokenTurnModelRouting, createSpokenTurnInputOptions } from './au
53
51
  import { allowTerminalWrite, installTuiTerminalOutputGuard } from './runtime/terminal-output-guard.ts';
54
52
  import { ProjectPlanningCoordinator } from './planning/project-planning-coordinator.ts';
55
53
  import { buildCommandArgsHint } from './input/command-args-hint.ts';
56
- import { summarizeRunningAgents } from './renderer/process-summary.ts';
57
54
  import { GOODVIBES_AGENT_PAIRING_SURFACE } from './config/surface.ts';
58
55
 
59
56
  const ALT_SCREEN_ENTER = '\x1b[?1049h';
@@ -93,8 +90,6 @@ async function main() {
93
90
  commandRegistry,
94
91
  inputHistory,
95
92
  hookDispatcher,
96
- gitStatusProvider,
97
- lastGitInfoRef,
98
93
  bootstrapUnsubs,
99
94
  agentStatusIntervalRef,
100
95
  orchestratorRefs,
@@ -133,15 +128,12 @@ async function main() {
133
128
  const sessionSnapshot = uiServices.readModels.session.getSnapshot();
134
129
  const tasksSnapshot = uiServices.readModels.tasks.getSnapshot();
135
130
  const remoteSnapshot = uiServices.readModels.remote.getSnapshot();
136
- const worktreeSnapshot = uiServices.readModels.worktrees.getSnapshot();
137
131
  return {
138
132
  pendingApprovals: sessionSnapshot.pendingApproval ? 1 : 0,
139
133
  activeTasks: tasksSnapshot.tasks.filter((task) => task.status === 'running' || task.status === 'queued').length,
140
134
  blockedTasks: tasksSnapshot.tasks.filter((task) => task.status === 'blocked').length,
141
135
  remoteContracts: remoteSnapshot.contracts.length,
142
136
  remoteRunners: remoteSnapshot.contracts.slice(0, 4).map((contract) => contract.runnerId),
143
- worktreeCount: worktreeSnapshot.records.length,
144
- worktreePaths: worktreeSnapshot.records.slice(0, 3).map((record) => record.path),
145
137
  openPanels: panelManager.getAllOpen().map((panel) => panel.id),
146
138
  };
147
139
  };
@@ -478,15 +470,9 @@ async function main() {
478
470
  // Cache the current model for consistent values across the entire render frame
479
471
  const currentModel = providerRegistry.getCurrentModel();
480
472
  const sessionSnapshot = uiServices.readModels.session.getSnapshot();
481
- const agentSnapshot = uiServices.readModels.agents.getSnapshot();
482
473
 
483
- const headerLines = UIFactory.createHeader(width, currentModel.id, currentModel.provider, conversation.title || undefined, lastGitInfoRef.value);
484
- const managerAgents = agentManager.list().filter(
485
- (a) => a.status === 'running' || a.status === 'pending',
486
- );
487
- const runtimeAgents = agentSnapshot.active;
488
- const runningAgentSummary = summarizeRunningAgents(managerAgents, runtimeAgents, ctx.services.wrfcController.listChains());
489
- const runningAgentCount = runningAgentSummary.count;
474
+ const headerLines = UIFactory.createHeader(width, currentModel.id, currentModel.provider, conversation.title || undefined);
475
+ const runningAgentCount = 0;
490
476
  const runningProcessCount = processManager.list().filter((p) => !p.status.startsWith('done')).length;
491
477
  const cw = getPromptContentWidth();
492
478
  const promptInfo = input.getWrappedPromptInfo(cw);
@@ -533,7 +519,7 @@ async function main() {
533
519
  runningAgentCount,
534
520
  runningProcessCount,
535
521
  indicatorFocused: input.indicatorFocused,
536
- runningAgentProgress: runningAgentSummary.progress,
522
+ runningAgentProgress: undefined,
537
523
  composerMode: composerState.modeLabel,
538
524
  composerStatus: composerState.statusLabel,
539
525
  composerFlags: composerState.flags,
@@ -684,8 +670,6 @@ async function main() {
684
670
  });
685
671
 
686
672
  // --- Streaming speed + tool preview wiring ---
687
- const refreshGit = () => gitStatusProvider.refresh().then((info) => { lastGitInfoRef.value = info; render(); }).catch(() => { /* non-fatal */ });
688
- // Refresh git status after each turn completes or after tool results arrive
689
673
  unsubs.push(uiServices.events.turns.on('TURN_COMPLETED', () => {
690
674
  // Auto-save after every LLM turn so kills don't lose the session
691
675
  try {
@@ -701,13 +685,6 @@ async function main() {
701
685
  );
702
686
  hookDispatcher.fire({ path: 'Lifecycle:session:save' as HookEventPath, phase: 'Lifecycle' as HookPhase, category: 'session' as HookCategory, specific: 'save', sessionId: runtime.sessionId, timestamp: Date.now(), payload: { sessionId: runtime.sessionId } }).catch((err: unknown) => logger.debug('hook fire error', { error: summarizeError(err) }));
703
687
  } catch (e) { logger.debug('auto-save on turn:complete failed', { error: summarizeError(e) }); }
704
- refreshGit();
705
- }));
706
- unsubs.push(uiServices.events.tools.on('TOOL_SUCCEEDED', () => {
707
- refreshGit();
708
- }));
709
- unsubs.push(uiServices.events.tools.on('TOOL_FAILED', () => {
710
- refreshGit();
711
688
  }));
712
689
 
713
690
  unsubs.push(uiServices.events.turns.on('STREAM_START', () => {
@@ -20,7 +20,7 @@ function truncateToWidth(text: string, maxWidth: number): string {
20
20
  * renderProcessIndicator — shows a one-line summary of active runtime
21
21
  * activity below the input area.
22
22
  *
23
- * Dimmed when no entries are active, highlighted (cyan) when delegated agent
23
+ * Dimmed when no entries are active, highlighted (cyan) when delegated work
24
24
  * records or shell exec processes are running. Includes an `Enter to view`
25
25
  * hint when active.
26
26
  */
@@ -32,6 +32,7 @@ export function renderProcessIndicator(
32
32
  agentProgress?: string,
33
33
  ): Line[] {
34
34
  const total = agentCount + toolCount;
35
+ const delegationLabel = (count: number): string => `${count} delegation${count !== 1 ? 's' : ''}`;
35
36
  const renderPlainStatus = (text: string, style: { fg: string; bold?: boolean; dim?: boolean }): Line[] => (
36
37
  [UIFactory.stringToLine(` ${text}`, width, style)]
37
38
  );
@@ -59,7 +60,7 @@ export function renderProcessIndicator(
59
60
  // --- Focused state: always render before idle/active branches ---
60
61
  if (focused) {
61
62
  const parts: string[] = [];
62
- if (agentCount > 0) parts.push(`${agentCount} agent${agentCount !== 1 ? 's' : ''}`);
63
+ if (agentCount > 0) parts.push(delegationLabel(agentCount));
63
64
  if (toolCount > 0) parts.push(`${toolCount} tool${toolCount !== 1 ? 's' : ''} running`);
64
65
  const label = total === 0
65
66
  ? `No runtime activity ${GLYPHS.status.pending} back to input`
@@ -71,18 +72,18 @@ export function renderProcessIndicator(
71
72
  return renderPlainStatus('No runtime activity', { fg: '238', dim: true });
72
73
  }
73
74
 
74
- // Build the label: "2 agents | Turn 3 | write - src/foo.ts"
75
+ // Build the label: "2 delegations | Turn 3 | write - src/foo.ts"
75
76
  const parts: string[] = [];
76
77
  if (agentCount > 0) {
77
- parts.push(`${agentCount} agent${agentCount !== 1 ? 's' : ''}`);
78
+ parts.push(delegationLabel(agentCount));
78
79
  }
79
80
  if (toolCount > 0) {
80
81
  parts.push(`${toolCount} tool${toolCount !== 1 ? 's' : ''} running`);
81
82
  }
82
83
  // Append the first running agent's progress (truncated to fit)
83
84
  /**
84
- * Number of columns reserved for the agent count label and hint text.
85
- * Breakdown: "bg: N agents" prefix (~15 chars) + " | " separator (~3)
85
+ * Number of columns reserved for the delegation count label and hint text.
86
+ * Breakdown: "N delegations" prefix (~15 chars) + " | " separator (~3)
86
87
  * + " Enter to view " hint (~17) + padding (~8) ≈ 43 chars.
87
88
  */
88
89
  const PROGRESS_RESERVED_CHARS = 43;
@@ -45,6 +45,12 @@ export interface ProcessModalDeps {
45
45
  readonly agentManager: Pick<AgentManager, 'list' | 'getStatus'>;
46
46
  readonly processManager: Pick<ProcessManager, 'list' | 'getStatus' | 'stop'>;
47
47
  readonly wrfcController: Pick<WrfcController, 'getChain'> & Partial<Pick<WrfcController, 'listChains'>>;
48
+ /**
49
+ * GoodVibes Agent must not present local AgentManager records as an owned
50
+ * execution lane. Tests for copied TUI primitives can opt into read-only
51
+ * display, but product runtime should keep this hidden.
52
+ */
53
+ readonly agentEntries: 'hidden' | 'read-only';
48
54
  }
49
55
 
50
56
  type WrfcChainLike = {
@@ -504,14 +510,17 @@ export class ProcessModal {
504
510
  const now = Date.now();
505
511
  const result: ProcessEntry[] = [];
506
512
 
507
- // Agents only show active (pending/running), grouped by stable parent/child hierarchy.
508
- result.push(...buildAgentEntries(
509
- manager.list(),
510
- this.deps,
511
- now,
512
- (key) => this.groupOrder.get(key),
513
- (key) => this.ensureGroupOrder(key),
514
- ));
513
+ // Local AgentManager activity is hidden in the Agent product. Build/fix/review
514
+ // execution belongs to explicit GoodVibes TUI delegation, not a local lane.
515
+ if (this.deps.agentEntries === 'read-only') {
516
+ result.push(...buildAgentEntries(
517
+ manager.list(),
518
+ this.deps,
519
+ now,
520
+ (key) => this.groupOrder.get(key),
521
+ (key) => this.ensureGroupOrder(key),
522
+ ));
523
+ }
515
524
 
516
525
  // Background exec processes — only show running
517
526
  const pm = this.deps.processManager;
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { Line } from '../types/grid.ts';
9
9
  import type { SettingsModal, SettingEntry, FlagEntry, McpEntry, SubscriptionEntry, SettingsCategory } from '../input/settings-modal.ts';
10
- import { SETTINGS_CATEGORIES, SETTINGS_CATEGORY_GROUPS } from '../input/settings-modal.ts';
10
+ import { isExternalDaemonOwnedSettingKey, SETTINGS_CATEGORIES, SETTINGS_CATEGORY_GROUPS } from '../input/settings-modal.ts';
11
11
  import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
12
12
  import { CATEGORY_LABELS, describeUiRouting, formatValue, getSettingLabel, inferSubscriptionRouteReason, valueColor } from './settings-modal-helpers.ts';
13
13
  import { isSecretConfigKey } from '../config/secret-config.ts';
@@ -34,10 +34,10 @@ const CATEGORY_INFO: Record<SettingsCategory, string> = {
34
34
  wrfc: 'WRFC is external to normal Agent operation. Review these copied compatibility values only for explicit GoodVibes TUI build delegation.',
35
35
  helper: 'Helper model defaults used by helper subsystems when they do not use the main chat route.',
36
36
  tts: 'Text-to-speech provider, voice, and optional spoken-turn LLM overrides.',
37
- service: 'Background service posture: enabled state, autostart, restart behavior, service name, platform, and logs.',
38
- controlPlane: 'Daemon control-plane settings for local admin/API access.',
39
- httpListener: 'HTTP listener settings for webhook and integration ingress.',
40
- web: 'Browser surface settings for the local or network web UI.',
37
+ service: 'External daemon service posture. Agent shows these copied compatibility keys for inspection only and does not install, start, stop, restart, or autostart services.',
38
+ controlPlane: 'External daemon control-plane settings for local admin/API access. Agent connects to this daemon and does not mutate its bind posture.',
39
+ httpListener: 'External HTTP listener settings for webhook and integration ingress. Agent does not start or expose the listener.',
40
+ web: 'External browser surface settings. Agent does not own the web listener or network bind lifecycle.',
41
41
  batch: 'Batch execution settings, including local vs Cloudflare queue behavior.',
42
42
  automation: 'Scheduled and automated run settings, concurrency, timeout, catch-up, cooldown, and retention behavior.',
43
43
  watchers: 'File/process watcher heartbeat, polling, and recovery-window behavior.',
@@ -49,10 +49,10 @@ const CATEGORY_INFO: Record<SettingsCategory, string> = {
49
49
  surfaces: 'External app surfaces such as Slack, Discord, ntfy, Home Assistant, Telegram, webhooks, chat bridges, and messaging providers.',
50
50
  cloudflare: 'Optional Cloudflare control plane, batch queue, Worker, Tunnel, Access, DNS, KV, Durable Objects, Secrets Store, and R2 settings.',
51
51
  release: 'Release-channel preference.',
52
- danger: 'High-impact switches for daemon and HTTP listener behavior. These are operational overrides, not normal preferences.',
52
+ danger: 'High-impact daemon and listener switches. Agent renders daemon-owned switches read-only; use GoodVibes TUI or the daemon host to change them.',
53
53
  tools: 'Tool LLM and helper model routing. Empty provider/model values inherit the active chat route unless a specific helper/tool route is set.',
54
54
  flags: 'Feature flags are SDK runtime gates. They are separate from normal config keys because they enable or disable staged runtime behavior.',
55
- network: 'Combined network view for daemon control-plane, HTTP listener, browser web surface, and general outbound network settings.',
55
+ network: 'Read-only view of external daemon control-plane, HTTP listener, and browser web bind posture plus editable non-daemon network settings.',
56
56
  };
57
57
 
58
58
  const ENUM_VALUE_DESCRIPTIONS: Record<string, Record<string, string>> = {
@@ -479,6 +479,10 @@ function footerText(modal: SettingsModal): string {
479
479
  if (modal.currentCategory === 'subscriptions') return 'Focus settings · Up/Down provider · Left categories · Tab pane · Enter review/sign out · Esc close';
480
480
  if (modal.currentCategory === 'mcp') return 'Focus settings · Up/Down server · Left categories · Tab pane · Enter edit trust · Esc close';
481
481
  if (modal.currentCategory === 'flags') return 'Focus feature flags · Up/Down flag · Left categories · Tab pane · Enter/Space toggle · Esc close';
482
+ const selected = modal.getSelected();
483
+ if (selected && isExternalDaemonOwnedSettingKey(selected.setting.key)) {
484
+ return 'Read-only external daemon setting · Change from GoodVibes TUI or daemon host · Esc close';
485
+ }
482
486
  return 'Focus settings · Up/Down setting · Left categories · Tab pane · Enter/Space edit/toggle · R reset · Esc close';
483
487
  }
484
488
 
@@ -488,7 +492,7 @@ export function renderSettingsModal(
488
492
  viewportHeight = 24,
489
493
  ): Line[] {
490
494
  const notices = [
491
- ...(modal.lastSaveTriggeredRestart ? [`Restarting ${modal.lastSaveTriggeredRestart}`] : []),
495
+ ...(modal.lastSaveTriggeredRestart ? [`External daemon owner must restart ${modal.lastSaveTriggeredRestart}`] : []),
492
496
  ...(modal.lastSettingEffectMessage ? [modal.lastSettingEffectMessage] : []),
493
497
  ];
494
498
  const metrics = getFullscreenWorkspaceMetrics({ width, height: viewportHeight });