@pellux/goodvibes-agent 1.9.0 → 1.10.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.
- package/CHANGELOG.md +18 -0
- package/README.md +28 -1
- package/dist/package/main.js +36631 -28780
- package/dist/package/{web-tree-sitter-jbz042ba.wasm → web-tree-sitter-e011xaqr.wasm} +0 -0
- package/docs/README.md +1 -1
- package/docs/connected-host.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/release-and-publishing.md +5 -1
- package/docs/tools-and-commands.md +1 -0
- package/package.json +3 -3
- package/release/live-verification/live-verification.json +11 -11
- package/release/live-verification/live-verification.md +12 -12
- package/src/agent/email/email-service.ts +1 -1
- package/src/agent/email/imap-client.ts +4 -4
- package/src/agent/email/smtp-client.ts +5 -5
- package/src/cli/config-overrides.ts +29 -14
- package/src/cli/entrypoint.ts +12 -4
- package/src/cli/launch-auto-update.ts +218 -0
- package/src/cli/relay-command.ts +4 -4
- package/src/cli/service-posture.ts +2 -2
- package/src/cli/tui-startup.ts +31 -0
- package/src/cli/workspaces-command.ts +5 -1
- package/src/cli-flags.ts +1 -1
- package/src/config/index.ts +1 -1
- package/src/config/update-settings.ts +45 -0
- package/src/config/workspace-registration.ts +214 -15
- package/src/input/commands/runtime-services.ts +0 -5
- package/src/input/commands/update-runtime.ts +313 -0
- package/src/input/commands.ts +2 -0
- package/src/input/feed-context-factory.ts +2 -2
- package/src/input/handler-feed.ts +8 -8
- package/src/input/handler.ts +1 -1
- package/src/input/mcp-workspace.ts +5 -1
- package/src/input/panel-paste-flood-guard.ts +1 -1
- package/src/input/settings-modal-types.ts +11 -5
- package/src/input/settings-modal.ts +56 -61
- package/src/main.ts +19 -20
- package/src/renderer/activity-sidebar.ts +53 -4
- package/src/renderer/settings-modal-helpers.ts +2 -0
- package/src/renderer/settings-modal.ts +37 -25
- package/src/runtime/bootstrap-core.ts +16 -5
- package/src/runtime/bootstrap-external-services.ts +107 -11
- package/src/runtime/bootstrap-hook-bridge.ts +2 -2
- package/src/runtime/bootstrap-shell.ts +4 -4
- package/src/runtime/bootstrap.ts +34 -12
- package/src/runtime/connected-host-autostart.ts +269 -0
- package/src/runtime/daemon-receipts.ts +66 -0
- package/src/runtime/diagnostics/panels/index.ts +0 -2
- package/src/runtime/feature-enablement.ts +176 -0
- package/src/runtime/index.ts +12 -29
- package/src/runtime/memory-spine-adoption.ts +18 -0
- package/src/runtime/onboarding/apply.ts +50 -37
- package/src/runtime/onboarding/types.ts +2 -2
- package/src/runtime/onboarding/verify.ts +22 -4
- package/src/runtime/release-artifacts.ts +113 -0
- package/src/runtime/services.ts +228 -19
- package/src/runtime/session-spine-rest-transport.ts +84 -4
- package/src/runtime/ui-services.ts +1 -1
- package/src/runtime/update-check.ts +64 -0
- package/src/shell/ui-openers.ts +8 -0
- package/src/tools/agent-harness-metadata.ts +8 -0
- package/src/tools/agent-harness-setup-connected-host.ts +1 -1
- package/src/tools/agent-harness-setup-posture.ts +1 -1
- package/src/version.ts +1 -1
- package/src/runtime/diagnostics/panels/ops.ts +0 -156
- package/src/runtime/surface-feature-flags.ts +0 -100
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** SettingsModal state for the /settings and /config fullscreen workspace. */
|
|
2
2
|
|
|
3
|
-
import { CONFIG_SCHEMA, type ConfigKey
|
|
3
|
+
import { CONFIG_SCHEMA, type ConfigKey } from '@pellux/goodvibes-sdk/platform/config';
|
|
4
4
|
import type { ModelPickerTarget } from './model-picker.ts';
|
|
5
5
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
6
6
|
import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
@@ -24,7 +24,9 @@ import {
|
|
|
24
24
|
THEME_MODE_SYNTHETIC_SETTING,
|
|
25
25
|
} from '../renderer/theme-mode-config.ts';
|
|
26
26
|
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
27
|
-
import type {
|
|
27
|
+
import type { FlagState } from '@/runtime/index.ts';
|
|
28
|
+
import { FEATURE_SETTINGS } from '@/runtime/index.ts';
|
|
29
|
+
import { isFeatureEnabledInConfig, resolveFeatureEnablementWrite } from '../runtime/feature-enablement.ts';
|
|
28
30
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
29
31
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
30
32
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
@@ -98,7 +100,7 @@ export class SettingsModal {
|
|
|
98
100
|
/** Settings grouped by category. */
|
|
99
101
|
public groups: Map<SettingsCategory, SettingEntry[]> = new Map();
|
|
100
102
|
|
|
101
|
-
/** Feature
|
|
103
|
+
/** Feature entries grouped by settings domain (populated when the features tab is active). */
|
|
102
104
|
public flagEntries: FlagEntry[] = [];
|
|
103
105
|
/** MCP server trust entries (populated when mcp tab is active). */
|
|
104
106
|
public mcpEntries: McpEntry[] = [];
|
|
@@ -393,7 +395,7 @@ export class SettingsModal {
|
|
|
393
395
|
|
|
394
396
|
if (this.currentCategory === 'flags') {
|
|
395
397
|
const flagEntry = this.getSelectedFlag();
|
|
396
|
-
if (!flagEntry || flagEntry.state === 'killed' || !this.
|
|
398
|
+
if (!flagEntry || flagEntry.state === 'killed' || !this.configManager) return;
|
|
397
399
|
const targetState: FlagState = direction === 'right' ? 'enabled' : 'disabled';
|
|
398
400
|
if (flagEntry.state !== targetState) this._setSelectedFlagState(flagEntry, targetState);
|
|
399
401
|
return;
|
|
@@ -448,47 +450,41 @@ export class SettingsModal {
|
|
|
448
450
|
}
|
|
449
451
|
|
|
450
452
|
/**
|
|
451
|
-
* Toggle the currently selected feature
|
|
453
|
+
* Toggle the currently selected feature by writing its enablement domain
|
|
454
|
+
* settings key (there is no separate enablement namespace). The live
|
|
455
|
+
* settings bridge propagates the change into the gate manager; features
|
|
456
|
+
* that require a restart record an honest pending-restart state instead.
|
|
452
457
|
*
|
|
453
|
-
* Killed
|
|
454
|
-
*
|
|
455
|
-
* flags toggle immediately.
|
|
458
|
+
* Killed features cannot be toggled. Always-available features (constant
|
|
459
|
+
* enablement) refuse with guidance naming their real settings keys.
|
|
456
460
|
*/
|
|
457
461
|
toggleSelectedFlag(): void {
|
|
458
462
|
const flagEntry = this.getSelectedFlag();
|
|
459
|
-
if (!flagEntry || !this.
|
|
463
|
+
if (!flagEntry || !this.configManager) return;
|
|
460
464
|
|
|
461
|
-
|
|
465
|
+
// Killed features are blocked
|
|
466
|
+
if (flagEntry.state === 'killed') return;
|
|
462
467
|
|
|
463
|
-
|
|
464
|
-
if (state === 'killed') return;
|
|
465
|
-
|
|
466
|
-
const newState: FlagState = state === 'enabled' ? 'disabled' : 'enabled';
|
|
468
|
+
const newState: FlagState = flagEntry.state === 'enabled' ? 'disabled' : 'enabled';
|
|
467
469
|
|
|
468
470
|
this._setSelectedFlagState(flagEntry, newState);
|
|
469
471
|
}
|
|
470
472
|
|
|
471
473
|
private _setSelectedFlagState(flagEntry: FlagEntry, newState: FlagState): void {
|
|
472
|
-
if (!this.
|
|
473
|
-
|
|
474
|
+
if (!this.configManager) return;
|
|
475
|
+
if (newState === 'killed') return;
|
|
476
|
+
const feature = flagEntry.feature;
|
|
474
477
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
this.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
this.featureFlagManager.disable(flag.id);
|
|
486
|
-
}
|
|
487
|
-
this._persistFlagState(flag.id, newState, flag.defaultState as FlagState);
|
|
488
|
-
flagEntry.state = newState;
|
|
489
|
-
} catch (e) {
|
|
490
|
-
logger.error('SettingsModal: failed to toggle feature flag', { flag: flag.id, error: summarizeError(e) });
|
|
491
|
-
}
|
|
478
|
+
try {
|
|
479
|
+
const write = resolveFeatureEnablementWrite(feature.id, newState === 'enabled' ? 'enabled' : 'disabled');
|
|
480
|
+
this.configManager.setDynamic(write.key, write.value);
|
|
481
|
+
this._loadFlagEntries();
|
|
482
|
+
this.lastSettingEffectMessage = feature.restartRequired
|
|
483
|
+
? `${write.key} = ${String(write.value)} saved; takes effect on the next launch.`
|
|
484
|
+
: `${write.key} = ${String(write.value)}`;
|
|
485
|
+
} catch (e) {
|
|
486
|
+
logger.error('SettingsModal: failed to set feature enablement', { feature: feature.id, error: summarizeError(e) });
|
|
487
|
+
this.lastSettingEffectMessage = `Save failed: ${summarizeError(e)}`;
|
|
492
488
|
}
|
|
493
489
|
}
|
|
494
490
|
|
|
@@ -666,16 +662,37 @@ export class SettingsModal {
|
|
|
666
662
|
}
|
|
667
663
|
}
|
|
668
664
|
|
|
669
|
-
/**
|
|
665
|
+
/**
|
|
666
|
+
* Load or refresh the features tab from FEATURE_SETTINGS, grouped by
|
|
667
|
+
* settings domain. State prefers the live gate manager (kill-switch and
|
|
668
|
+
* pending-restart aware); without a manager it derives from the feature's
|
|
669
|
+
* bound domain settings key.
|
|
670
|
+
*/
|
|
670
671
|
private _loadFlagEntries(): void {
|
|
671
|
-
if (!this.
|
|
672
|
+
if (!this.configManager) {
|
|
672
673
|
this.flagEntries = [];
|
|
673
674
|
return;
|
|
674
675
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
676
|
+
const configManager = this.configManager;
|
|
677
|
+
const managerStates = this.featureFlagManager?.getAll() ?? null;
|
|
678
|
+
this.flagEntries = FEATURE_SETTINGS
|
|
679
|
+
.map((feature, declarationIndex) => {
|
|
680
|
+
const managed = managerStates?.get(feature.id);
|
|
681
|
+
const derivedState: FlagState = isFeatureEnabledInConfig(configManager, feature.id) ? 'enabled' : 'disabled';
|
|
682
|
+
return {
|
|
683
|
+
entry: {
|
|
684
|
+
feature,
|
|
685
|
+
state: managed?.state ?? derivedState,
|
|
686
|
+
enablementValue: String(configManager.get(feature.enablement.key)),
|
|
687
|
+
},
|
|
688
|
+
declarationIndex,
|
|
689
|
+
};
|
|
690
|
+
})
|
|
691
|
+
.sort((left, right) => (
|
|
692
|
+
left.entry.feature.domain.localeCompare(right.entry.feature.domain)
|
|
693
|
+
|| left.declarationIndex - right.declarationIndex
|
|
694
|
+
))
|
|
695
|
+
.map(({ entry }) => entry);
|
|
679
696
|
}
|
|
680
697
|
|
|
681
698
|
private _loadMcpEntries(): void {
|
|
@@ -697,28 +714,6 @@ export class SettingsModal {
|
|
|
697
714
|
this.subscriptionEntries = buildSubscriptionEntries(this.subscriptionManager, this.serviceRegistry);
|
|
698
715
|
}
|
|
699
716
|
|
|
700
|
-
/**
|
|
701
|
-
* Persist a flag state override to config.
|
|
702
|
-
* Deletes the entry when reverting to defaultState. Skips killed state.
|
|
703
|
-
*/
|
|
704
|
-
private _persistFlagState(flagId: string, newState: FlagState, defaultState: FlagState): void {
|
|
705
|
-
if (!this.configManager) return;
|
|
706
|
-
if (newState === 'killed') return; // never persist killed state
|
|
707
|
-
|
|
708
|
-
try {
|
|
709
|
-
const current = (this.configManager.getCategory('featureFlags') as Record<string, PersistedFlagState>) ?? {};
|
|
710
|
-
if (newState === defaultState) {
|
|
711
|
-
// Revert to default — remove override
|
|
712
|
-
delete current[flagId];
|
|
713
|
-
} else {
|
|
714
|
-
current[flagId] = newState;
|
|
715
|
-
}
|
|
716
|
-
this.configManager.mergeCategory('featureFlags', current);
|
|
717
|
-
} catch (e) {
|
|
718
|
-
logger.error('SettingsModal: failed to persist flag state', { flagId, error: summarizeError(e) });
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
717
|
/** Returns [] for the flags category (flags use flagEntries instead). */
|
|
723
718
|
private _currentItems(): SettingEntry[] {
|
|
724
719
|
if (this.currentCategory === 'flags' || this.currentCategory === 'mcp' || this.currentCategory === 'subscriptions') return [];
|
package/src/main.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { createShellLayout } from './renderer/layout-engine.ts';
|
|
|
28
28
|
import { buildShellFooter, estimateShellFooterHeight } from './renderer/shell-surface.ts';
|
|
29
29
|
import { buildConversationViewport } from './renderer/conversation-layout.ts';
|
|
30
30
|
import { applyConversationOverlays } from './renderer/conversation-overlays.ts';
|
|
31
|
-
import { buildActivitySidebarLines, resolveActivitySidebarWidth } from './renderer/activity-sidebar.ts';
|
|
31
|
+
import { buildActivitySidebarLines, buildSidebarAgentRows, resolveActivitySidebarWidth } from './renderer/activity-sidebar.ts';
|
|
32
32
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
33
33
|
import { bootstrapRuntime } from './runtime/bootstrap.ts';
|
|
34
34
|
import type { BootstrapContext } from './runtime/bootstrap.ts';
|
|
@@ -52,7 +52,8 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
52
52
|
import { installFocusModeExitGuard, markFocusModeEnabled, wrapRequestPermissionWithApprovalAlert } from './shell/terminal-focus-mode.ts';
|
|
53
53
|
import { CLEAR_VIEWPORT_HOME, buildEnterSequence, buildExitSequence } from './renderer/terminal-escapes.ts';
|
|
54
54
|
import { prepareShellCliRuntime } from './cli/entrypoint.ts';
|
|
55
|
-
import {
|
|
55
|
+
import { selfUpdateAtLaunch } from './cli/launch-auto-update.ts';
|
|
56
|
+
import { applyInitialTuiCliState, getInteractiveTerminalLaunchError, reportFatalStartupError } from './cli/tui-startup.ts';
|
|
56
57
|
import { wireSpokenTurnRuntime } from './audio/spoken-turn-wiring.ts';
|
|
57
58
|
import { createUnhandledRejectionHandler } from './runtime/unhandled-rejection-guard.ts';
|
|
58
59
|
import { attachSpokenTurnModelRouting, createSpokenTurnInputOptions } from './audio/spoken-turn-model-routing.ts';
|
|
@@ -84,6 +85,10 @@ async function main() {
|
|
|
84
85
|
process.exit(2);
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
// Launch-time self-update, before any bootstrap or terminal mode change; on
|
|
89
|
+
// an installed update this restarts onto the swapped binary and never returns.
|
|
90
|
+
const launchUpdateLines = await selfUpdateAtLaunch({ configManager, stdout });
|
|
91
|
+
|
|
87
92
|
const ctx: BootstrapContext = await bootstrapRuntime(stdout, {
|
|
88
93
|
configManager,
|
|
89
94
|
workingDir: bootstrapWorkingDir,
|
|
@@ -126,6 +131,10 @@ async function main() {
|
|
|
126
131
|
});
|
|
127
132
|
let activeConversationWidth = getTerminalSize(stdout).width;
|
|
128
133
|
conversation.setWidthProvider(() => activeConversationWidth);
|
|
134
|
+
|
|
135
|
+
// Re-surface pre-bootstrap launch-update lines in-session (the alternate
|
|
136
|
+
// screen wipes the stdout copies written before the renderer existed).
|
|
137
|
+
for (const line of launchUpdateLines) systemMessageRouter.high(`[Update] ${line}`);
|
|
129
138
|
{
|
|
130
139
|
const hitlMode = configManager.get('behavior.hitlMode') as HITLMode | undefined;
|
|
131
140
|
if (hitlMode && (hitlMode === 'quiet' || hitlMode === 'balanced' || hitlMode === 'operator')) {
|
|
@@ -622,17 +631,15 @@ async function main() {
|
|
|
622
631
|
contextWindow: currentModel.contextWindow,
|
|
623
632
|
});
|
|
624
633
|
|
|
625
|
-
// Activity sidebar (ambient status on wide terminals)
|
|
634
|
+
// Activity sidebar (ambient status on wide terminals). Agent rows carry
|
|
635
|
+
// the fleet read-model's headline and stall tell (see activity-sidebar.ts).
|
|
626
636
|
const sidebar = sidebarWidth > 0
|
|
627
637
|
? {
|
|
628
638
|
lines: buildActivitySidebarLines({
|
|
629
639
|
now: {
|
|
630
640
|
busy: orchestrator.isThinking,
|
|
631
641
|
label: sessionSnapshot.streamToolPreview?.trim() || undefined,
|
|
632
|
-
agents: activeAgents
|
|
633
|
-
label: agent.label,
|
|
634
|
-
progress: agent.latestProgress?.trim() || undefined,
|
|
635
|
-
})),
|
|
642
|
+
agents: buildSidebarAgentRows(activeAgents, ctx.services.processRegistry.query().nodes),
|
|
636
643
|
processes: runningProcessCount,
|
|
637
644
|
},
|
|
638
645
|
needsYou: pendingPermission
|
|
@@ -781,20 +788,12 @@ async function main() {
|
|
|
781
788
|
}));
|
|
782
789
|
}
|
|
783
790
|
main().catch((err: unknown) => {
|
|
784
|
-
|
|
785
|
-
try {
|
|
786
|
-
logger.error('Fatal error', { error: detail });
|
|
787
|
-
} catch {
|
|
788
|
-
// Startup diagnostics must never hide the original launch failure.
|
|
789
|
-
}
|
|
790
|
-
const userDetail = formatFatalStartupErrorForUser(err, {
|
|
791
|
+
reportFatalStartupError(err, {
|
|
791
792
|
binary: 'goodvibes-agent',
|
|
792
793
|
debug: process.env['GOODVIBES_AGENT_DEBUG'] === '1',
|
|
794
|
+
}, {
|
|
795
|
+
logError: (message, context) => logger.error(message, context),
|
|
796
|
+
writeStderr: (chunk) => process.stderr.write(chunk),
|
|
797
|
+
exit: (code) => process.exit(code),
|
|
793
798
|
});
|
|
794
|
-
try {
|
|
795
|
-
process.stderr.write(`goodvibes-agent failed to launch:\n${userDetail}\n`);
|
|
796
|
-
} catch {
|
|
797
|
-
// Ignore secondary stderr failures during process teardown.
|
|
798
|
-
}
|
|
799
|
-
process.exit(1);
|
|
800
799
|
});
|
|
@@ -28,12 +28,57 @@ export interface ActivitySidebarNow {
|
|
|
28
28
|
readonly busy: boolean;
|
|
29
29
|
/** Short human label for the current work, e.g. "Searching the web…". */
|
|
30
30
|
readonly label?: string;
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Background agents with their latest progress lines. `headline` is the
|
|
33
|
+
* fleet read-model's per-node headline (derived from task/phase identity
|
|
34
|
+
* only, replaced in place — never a feed) and wins over the raw progress
|
|
35
|
+
* line when present; `quietForMs` is the fleet stall tell (pure timestamp
|
|
36
|
+
* comparison), rendered as a quiet-duration marker.
|
|
37
|
+
*/
|
|
38
|
+
readonly agents: ReadonlyArray<{
|
|
39
|
+
readonly label: string;
|
|
40
|
+
readonly progress?: string;
|
|
41
|
+
readonly headline?: string;
|
|
42
|
+
readonly quietForMs?: number;
|
|
43
|
+
}>;
|
|
33
44
|
/** Count of running background processes. */
|
|
34
45
|
readonly processes: number;
|
|
35
46
|
}
|
|
36
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Join active agents with the fleet read-model's nodes (matched by id) into
|
|
50
|
+
* sidebar rows: the per-node headline (task/phase identity only — replaced
|
|
51
|
+
* in place, never a feed) wins over the raw progress line, and the stall
|
|
52
|
+
* tell renders as a quiet-duration marker.
|
|
53
|
+
*/
|
|
54
|
+
export function buildSidebarAgentRows(
|
|
55
|
+
activeAgents: ReadonlyArray<{ readonly id: string; readonly label: string; readonly latestProgress?: string | undefined }>,
|
|
56
|
+
fleetNodes: ReadonlyArray<{
|
|
57
|
+
readonly id: string;
|
|
58
|
+
readonly headline?: { readonly text: string } | undefined;
|
|
59
|
+
readonly stall?: { readonly quietForMs: number } | undefined;
|
|
60
|
+
}>,
|
|
61
|
+
): ActivitySidebarNow['agents'] {
|
|
62
|
+
const nodesById = new Map(fleetNodes.map((node) => [node.id, node]));
|
|
63
|
+
return activeAgents.slice(0, 3).map((agent) => {
|
|
64
|
+
const node = nodesById.get(agent.id);
|
|
65
|
+
return {
|
|
66
|
+
label: agent.label,
|
|
67
|
+
progress: agent.latestProgress?.trim() || undefined,
|
|
68
|
+
headline: node?.headline?.text,
|
|
69
|
+
quietForMs: node?.stall?.quietForMs,
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Compact quiet-duration text for the stall tell, e.g. "quiet 4m". */
|
|
75
|
+
function fmtQuietFor(quietForMs: number): string {
|
|
76
|
+
const minutes = Math.floor(quietForMs / 60_000);
|
|
77
|
+
if (minutes < 60) return `quiet ${Math.max(1, minutes)}m`;
|
|
78
|
+
const hours = Math.floor(minutes / 60);
|
|
79
|
+
return `quiet ${hours}h${minutes % 60 > 0 ? ` ${minutes % 60}m` : ''}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
37
82
|
export interface ActivitySidebarView {
|
|
38
83
|
readonly now: ActivitySidebarNow;
|
|
39
84
|
/** Plain-language items waiting on the user (approvals, prompts). */
|
|
@@ -124,10 +169,14 @@ export function buildActivitySidebarLines(
|
|
|
124
169
|
]));
|
|
125
170
|
}
|
|
126
171
|
for (const agent of view.now.agents.slice(0, 3)) {
|
|
127
|
-
const
|
|
172
|
+
const detail = agent.headline ?? agent.progress;
|
|
173
|
+
const text = detail ? `${agent.label} — ${detail}` : agent.label;
|
|
174
|
+
const quiet = agent.quietForMs !== undefined ? ` ${fmtQuietFor(agent.quietForMs)}` : '';
|
|
175
|
+
const room = Math.max(4, width - 4 - getDisplayWidth(quiet));
|
|
128
176
|
push(buildPanelLine(width, [
|
|
129
177
|
[' » ', C.info],
|
|
130
|
-
[truncateDisplay(text,
|
|
178
|
+
[truncateDisplay(text, room), C.dim],
|
|
179
|
+
...(quiet ? [[quiet, C.warn] as [string, string]] : []),
|
|
131
180
|
]));
|
|
132
181
|
}
|
|
133
182
|
if (view.now.processes > 0) {
|
|
@@ -117,6 +117,8 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
|
|
|
117
117
|
fetch: 'Fetch Safety',
|
|
118
118
|
security: 'Token Rotation',
|
|
119
119
|
integrations: 'Delivery Reliability',
|
|
120
|
+
update: 'Daemon Updates',
|
|
121
|
+
pricing: 'Model Pricing',
|
|
120
122
|
};
|
|
121
123
|
|
|
122
124
|
export const SETTING_LABELS: Partial<Record<string, string>> = {
|
|
@@ -42,7 +42,7 @@ const CATEGORY_INFO: Record<SettingsCategory, string> = {
|
|
|
42
42
|
web: 'Web companion surface settings including host, port, public URL, and static asset path.',
|
|
43
43
|
watchers: 'Polling watcher and heartbeat behavior for runtime recovery and periodic checks.',
|
|
44
44
|
network: 'Outbound TLS and remote fetch network policy.',
|
|
45
|
-
relay: 'Outbound zero-knowledge relay reachability for the connected GoodVibes daemon: an end-to-end encrypted tunnel (ECDH P-256 -> HKDF -> AES-256-GCM) that terminates INSIDE the daemon, so the relay operator only ever sees ciphertext plus connection metadata (who paired with whom, byte counts, timing) — never plaintext requests, responses, or the operator token.
|
|
45
|
+
relay: 'Outbound zero-knowledge relay reachability for the connected GoodVibes daemon: an end-to-end encrypted tunnel (ECDH P-256 -> HKDF -> AES-256-GCM) that terminates INSIDE the daemon, so the relay operator only ever sees ciphertext plus connection metadata (who paired with whom, byte counts, timing) — never plaintext requests, responses, or the operator token. relay.enabled is the relay-connect feature\'s switch. These are the connected daemon\'s own settings (imported here, not live-shared): changing them in Agent does not itself start or stop the daemon\'s relay registration.',
|
|
46
46
|
orchestration: 'Visible agent orchestration limits such as recursion and active-agent caps.',
|
|
47
47
|
planner: 'Planning-decomposition agent limits: decomposition strategy, max turns, token ceiling, and wall-clock timeout before falling back to the deterministic heuristic path.',
|
|
48
48
|
daemon: 'Whether the local session daemon runs, and whether it is embedded in this surface process instead of spawned as a detached background service.',
|
|
@@ -56,8 +56,10 @@ const CATEGORY_INFO: Record<SettingsCategory, string> = {
|
|
|
56
56
|
mcp: 'MCP server trust and scope review. Trust changes can expose local files, tools, databases, browsers, or remote automation depending on the server.',
|
|
57
57
|
surfaces: 'Messaging and notification channel accounts such as Slack, Discord, ntfy, Telegram, chat bridges, and delivery providers.',
|
|
58
58
|
release: 'Update-channel preference.',
|
|
59
|
+
update: "Connected-host self-update posture: whether the daemon checks for, verifies, and swaps in new releases on its own, how often it checks, and where releases are resolved from. The daemon applies these itself; the Agent only edits the shared keys.",
|
|
60
|
+
pricing: 'Manual model prices (USD per 1M tokens, keyed provider:model). A manual price outranks registration, provider-served, and catalog prices in the one pricing resolver; unknown models stay honestly unpriced.',
|
|
59
61
|
tools: 'Tool LLM and helper model routing. Empty provider/model values inherit the active chat route unless a specific helper/tool route is set.',
|
|
60
|
-
flags: '
|
|
62
|
+
flags: 'Every optional capability grouped by its settings domain: each feature is switched through a first-class domain settings key (shown per row), with its full description and related settings under the cursor.',
|
|
61
63
|
atRest: 'Data-at-rest protection: whether stored content is redacted, and retention limits by age and total size.',
|
|
62
64
|
learning: 'Idle-time memory consolidation: dedupe merges, confidence decay of never-referenced records, and review proposals. Off by default.',
|
|
63
65
|
agents: 'Agent runtime tuning: the context-window fraction that triggers sub-agent conversation compaction, and the token budget, relevance floor, and code-chunk limit for per-turn passive knowledge/code injection.',
|
|
@@ -197,22 +199,32 @@ function formatSubscriptionRoute(route: SubscriptionEntry['activeRoute'] | Subsc
|
|
|
197
199
|
return route ? formatProviderAuthRouteId(route) : 'n/a';
|
|
198
200
|
}
|
|
199
201
|
|
|
202
|
+
function describeFeatureEnablement(entry: FlagEntry): string {
|
|
203
|
+
const { key, kind, enabledValues } = entry.feature.enablement;
|
|
204
|
+
if (kind === 'boolean') return `Switch: ${key} (true/false).`;
|
|
205
|
+
if (kind === 'enum') return `Switch: ${key} — active while set to ${(enabledValues ?? []).join(' or ')}.`;
|
|
206
|
+
return `Always available; its settings (${entry.feature.settings.join(', ')}) govern runtime activation directly.`;
|
|
207
|
+
}
|
|
208
|
+
|
|
200
209
|
function buildFlagContext(entry: FlagEntry | null): string[] {
|
|
201
210
|
if (!entry) return ['Feature Controls', 'No feature control is selected.'];
|
|
202
211
|
return [
|
|
203
|
-
entry.
|
|
204
|
-
`ID: ${entry.
|
|
212
|
+
entry.feature.name,
|
|
213
|
+
`ID: ${entry.feature.id}`,
|
|
214
|
+
`Domain: ${entry.feature.domain}`,
|
|
205
215
|
`State: ${entry.state}`,
|
|
206
|
-
`Default: ${entry.
|
|
207
|
-
`
|
|
208
|
-
`Live toggleable: ${entry.
|
|
216
|
+
`Default: ${entry.feature.defaultEnabled ? 'enabled' : 'disabled'}`,
|
|
217
|
+
`Current value: ${entry.feature.enablement.key} = ${entry.enablementValue}`,
|
|
218
|
+
`Live toggleable: ${entry.feature.restartRequired ? 'no' : 'yes'}`,
|
|
219
|
+
'',
|
|
220
|
+
entry.feature.description,
|
|
209
221
|
'',
|
|
210
|
-
entry
|
|
211
|
-
|
|
222
|
+
describeFeatureEnablement(entry),
|
|
223
|
+
`Settings: ${entry.feature.settings.join(', ')}`,
|
|
212
224
|
'',
|
|
213
|
-
entry.
|
|
214
|
-
? 'Impact:
|
|
215
|
-
: 'Impact:
|
|
225
|
+
entry.feature.restartRequired
|
|
226
|
+
? 'Impact: the domain settings key is saved now and takes effect on the next Agent launch or owning-host reload.'
|
|
227
|
+
: 'Impact: changes to the domain settings key apply immediately through the live settings bridge.',
|
|
216
228
|
];
|
|
217
229
|
}
|
|
218
230
|
|
|
@@ -388,25 +400,25 @@ function renderSettingRows(modal: SettingsModal, width: number, height: number):
|
|
|
388
400
|
function renderFlagRows(modal: SettingsModal, width: number, height: number): string[] {
|
|
389
401
|
const rows: string[] = [];
|
|
390
402
|
const items = modal.flagEntries;
|
|
391
|
-
if (items.length === 0) return ['No
|
|
403
|
+
if (items.length === 0) return ['No features registered.'];
|
|
392
404
|
const selectedIndex = clamp(modal.selectedIndex, 0, items.length - 1);
|
|
393
|
-
const nameWidth = clamp(Math.floor(width * 0.
|
|
405
|
+
const nameWidth = clamp(Math.floor(width * 0.34), 22, 52);
|
|
394
406
|
const stateWidth = 10;
|
|
395
|
-
const
|
|
407
|
+
const domainWidth = 13;
|
|
396
408
|
const runtimeWidth = 10;
|
|
397
409
|
const defaultWidth = 9;
|
|
398
|
-
const
|
|
399
|
-
rows.push(` ${padDisplay('Feature
|
|
410
|
+
const settingWidth = Math.max(16, width - nameWidth - stateWidth - domainWidth - runtimeWidth - defaultWidth - 14);
|
|
411
|
+
rows.push(` ${padDisplay('Feature', nameWidth)} ${padDisplay('State', stateWidth)} ${padDisplay('Domain', domainWidth)} ${padDisplay('Applies', runtimeWidth)} ${padDisplay('Default', defaultWidth)} ${padDisplay('Setting', settingWidth)}`);
|
|
400
412
|
const visibleCount = Math.max(1, height - 2);
|
|
401
413
|
const window = stableWindow(items.length, selectedIndex, visibleCount);
|
|
402
|
-
if (window.start > 0) rows.push(`${GLYPHS.navigation.moreAbove} ${window.start} more
|
|
414
|
+
if (window.start > 0) rows.push(`${GLYPHS.navigation.moreAbove} ${window.start} more feature(s) above`);
|
|
403
415
|
for (let index = window.start; index < window.end; index += 1) {
|
|
404
416
|
const entry = items[index]!;
|
|
405
417
|
const selected = index === selectedIndex;
|
|
406
418
|
const marker = selected ? (modal.focusPane === 'settings' ? GLYPHS.navigation.selected : '•') : ' ';
|
|
407
|
-
rows.push(`${marker} ${padDisplay(entry.
|
|
419
|
+
rows.push(`${marker} ${padDisplay(entry.feature.name, nameWidth)} ${padDisplay(entry.state, stateWidth)} ${padDisplay(entry.feature.domain, domainWidth)} ${padDisplay(entry.feature.restartRequired ? 'next run' : 'now', runtimeWidth)} ${padDisplay(entry.feature.defaultEnabled ? 'enabled' : 'disabled', defaultWidth)} ${padDisplay(`${entry.feature.enablement.key}=${entry.enablementValue}`, settingWidth)}`);
|
|
408
420
|
}
|
|
409
|
-
if (window.end < items.length) rows.push(`${GLYPHS.navigation.moreBelow} ${items.length - window.end} more
|
|
421
|
+
if (window.end < items.length) rows.push(`${GLYPHS.navigation.moreBelow} ${items.length - window.end} more feature(s) below`);
|
|
410
422
|
return rows.slice(0, height);
|
|
411
423
|
}
|
|
412
424
|
|
|
@@ -478,7 +490,7 @@ function footerText(modal: SettingsModal): string {
|
|
|
478
490
|
if (modal.focusPane === 'categories') return 'Focus categories · Up/Down choose · Right/Enter settings · Tab pane · Esc close';
|
|
479
491
|
if (modal.currentCategory === 'subscriptions') return 'Focus settings · Up/Down provider · Left categories · Tab pane · Enter review/sign out · Esc close';
|
|
480
492
|
if (modal.currentCategory === 'mcp') return 'Focus settings · Up/Down server · Left categories · Tab pane · Enter edit trust · Esc close';
|
|
481
|
-
if (modal.currentCategory === 'flags') return 'Focus
|
|
493
|
+
if (modal.currentCategory === 'flags') return 'Focus features · Up/Down feature · Left categories · Tab pane · Enter/Space toggle · Esc close';
|
|
482
494
|
const selected = modal.getSelected();
|
|
483
495
|
if (selected && isExternalHostOwnedSettingKey(selected.setting.key)) {
|
|
484
496
|
return 'Read-only connected-host setting · Change from GoodVibes TUI or the owning host · Esc close';
|
|
@@ -495,9 +507,9 @@ export function renderSettingsModalPackageText(): string {
|
|
|
495
507
|
'Type',
|
|
496
508
|
'Source',
|
|
497
509
|
'Default',
|
|
498
|
-
'Feature
|
|
510
|
+
'Feature',
|
|
499
511
|
'State',
|
|
500
|
-
'
|
|
512
|
+
'Domain',
|
|
501
513
|
'Applies',
|
|
502
514
|
'Server',
|
|
503
515
|
'Trust',
|
|
@@ -509,7 +521,7 @@ export function renderSettingsModalPackageText(): string {
|
|
|
509
521
|
'OAuth',
|
|
510
522
|
'Note',
|
|
511
523
|
'No settings in this category.',
|
|
512
|
-
'No
|
|
524
|
+
'No features registered.',
|
|
513
525
|
'No MCP servers registered.',
|
|
514
526
|
'No provider subscriptions available or configured.',
|
|
515
527
|
'No setting is selected in this category.',
|
|
@@ -531,7 +543,7 @@ export function renderSettingsModalPackageText(): string {
|
|
|
531
543
|
'Focus categories · Up/Down choose · Right/Enter settings · Tab pane · Esc close',
|
|
532
544
|
'Focus settings · Up/Down provider · Left categories · Tab pane · Enter review/sign out · Esc close',
|
|
533
545
|
'Focus settings · Up/Down server · Left categories · Tab pane · Enter edit trust · Esc close',
|
|
534
|
-
'Focus
|
|
546
|
+
'Focus features · Up/Down feature · Left categories · Tab pane · Enter/Space toggle · Esc close',
|
|
535
547
|
'Read-only connected-host setting · Change from GoodVibes TUI or the owning host · Esc close',
|
|
536
548
|
'Focus settings · Up/Down setting · Left categories · Tab pane · Enter/Space edit/toggle · R reset · Esc close',
|
|
537
549
|
];
|
|
@@ -17,7 +17,7 @@ import type { OrchestratorUserInputOptions } from '../core/orchestrator.ts';
|
|
|
17
17
|
import type { ControlPlaneRecentEvent } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
18
18
|
import type { MutableRuntimeState } from '@/runtime/index.ts';
|
|
19
19
|
import type { BootstrapOptions } from './context.ts';
|
|
20
|
-
import { createFeatureFlagManager } from '@/runtime/index.ts';
|
|
20
|
+
import { createFeatureFlagManager, deriveFeatureStates, bindFeatureSettingsBridge } from '@/runtime/index.ts';
|
|
21
21
|
import { RuntimeEventBus } from '@/runtime/index.ts';
|
|
22
22
|
import type { SessionEvent } from '@/runtime/index.ts';
|
|
23
23
|
import { emitPermissionModeChanged } from '@pellux/goodvibes-sdk/platform/runtime/emitters';
|
|
@@ -178,10 +178,12 @@ export async function initializeBootstrapCore(
|
|
|
178
178
|
const homeDirectory = options.homeDirectory;
|
|
179
179
|
const configManager = options.configManager;
|
|
180
180
|
|
|
181
|
+
// Gate states derive from domain settings keys (behavior.*, sandbox.*, ...);
|
|
182
|
+
// the featureFlags config category no longer exists. The live bridge keeps
|
|
183
|
+
// config.set changes on those keys flowing into the manager afterwards.
|
|
181
184
|
const featureFlags = createFeatureFlagManager();
|
|
182
|
-
featureFlags.loadFromConfig({
|
|
183
|
-
|
|
184
|
-
});
|
|
185
|
+
featureFlags.loadFromConfig({ flags: deriveFeatureStates(configManager) });
|
|
186
|
+
bindFeatureSettingsBridge(configManager, featureFlags);
|
|
185
187
|
|
|
186
188
|
const userSessionId = `user-${generateUserSessionId()}`;
|
|
187
189
|
// Declared here, before createRuntimeServices, so the checkpoint manager can be
|
|
@@ -367,6 +369,11 @@ export async function initializeBootstrapCore(
|
|
|
367
369
|
// everything set here.
|
|
368
370
|
const agentOrchestratorToolDeps = {
|
|
369
371
|
surfaceRoot: GOODVIBES_AGENT_SURFACE_ROOT,
|
|
372
|
+
// Same instances services.ts wired at construction — setDependencies()
|
|
373
|
+
// fully replaces, so the localhost fetch ask and the announce-once
|
|
374
|
+
// containment receipt must be replayed here or they silently vanish.
|
|
375
|
+
localhostFetchApproval: services.localhostFetchApproval,
|
|
376
|
+
onSandboxedRun: services.onSandboxedRun,
|
|
370
377
|
fileCache,
|
|
371
378
|
projectIndex,
|
|
372
379
|
workingDirectory: services.workingDirectory,
|
|
@@ -585,6 +592,10 @@ export async function initializeBootstrapCore(
|
|
|
585
592
|
policyRuntimeState,
|
|
586
593
|
services.hookDispatcher,
|
|
587
594
|
featureFlags,
|
|
595
|
+
// Durable user-origin permission rules (remembered approvals), mirroring
|
|
596
|
+
// the SDK composition root: "always allow" style decisions persist across
|
|
597
|
+
// restarts and are listable/deletable via permissions.rules.*.
|
|
598
|
+
services.userPermissionRuleStore,
|
|
588
599
|
);
|
|
589
600
|
installPermissionManagerSafetyGuard(permissionManager);
|
|
590
601
|
// Wire permissionManager into the SAME AgentOrchestrator instance that runs
|
|
@@ -630,7 +641,7 @@ export async function initializeBootstrapCore(
|
|
|
630
641
|
},
|
|
631
642
|
}).catch((err) => { logger.debug('session broker create session failed at bootstrap', { err }); });
|
|
632
643
|
|
|
633
|
-
//
|
|
644
|
+
// Mirror the create into the daemon spine (fire-and-forget; the local
|
|
634
645
|
// broker above stays the source of truth). kind:'agent' is the canonical record
|
|
635
646
|
// identity; participant.surfaceKind stays 'service' (the TRANSPORT axis).
|
|
636
647
|
services.sessionSpineClient.register({
|