@pellux/goodvibes-tui 1.25.0 → 1.27.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 +15 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +9605 -3119
- package/package.json +9 -7
- package/scripts/postinstall.js +57 -1
- package/src/audio/activation-sound.ts +117 -0
- package/src/audio/capture.ts +112 -0
- package/src/audio/onnxruntime-assets.d.ts +24 -0
- package/src/audio/voice-capture-wiring.ts +119 -0
- package/src/audio/voice-input-session.ts +181 -0
- package/src/audio/wake-inference.ts +182 -0
- package/src/audio/wake-runtime.ts +341 -0
- package/src/cli/bundle-command.ts +4 -1
- package/src/cli/help.ts +10 -0
- package/src/cli/management-utils.ts +4 -1
- package/src/cli/redaction.ts +68 -3
- package/src/config/goodvibes-home.ts +10 -0
- package/src/config/secret-config.ts +64 -8
- package/src/core/turn-event-wiring.ts +7 -16
- package/src/core/voice-capture-status.ts +53 -0
- package/src/core/voice-stt-gateway.ts +75 -0
- package/src/core/wake-provision-runner.ts +86 -0
- package/src/core/wake-provision-status.ts +170 -0
- package/src/core/workstream-notification.ts +69 -0
- package/src/daemon/cli.ts +86 -5
- package/src/daemon/handlers/credentials.ts +15 -5
- package/src/daemon/provision-wake-model.ts +81 -0
- package/src/daemon/send/channels.ts +200 -0
- package/src/daemon/send/command.ts +332 -0
- package/src/daemon/send/composition.ts +99 -0
- package/src/daemon/send/failure-text.ts +93 -0
- package/src/daemon/send/inert-text.ts +225 -0
- package/src/daemon/send/stdin.ts +24 -0
- package/src/input/command-registry.ts +6 -0
- package/src/input/commands/config.ts +68 -2
- package/src/input/commands/experience-runtime.ts +37 -3
- package/src/input/commands/local-provider-runtime.ts +9 -1
- package/src/input/commands/operator-runtime.ts +6 -1
- package/src/input/commands/payment-card-intake.ts +200 -0
- package/src/input/commands/profile-render.ts +250 -0
- package/src/input/commands/profile-runtime.ts +362 -0
- package/src/input/commands/profile-types.ts +381 -0
- package/src/input/commands.ts +7 -0
- package/src/input/composer-editor.ts +4 -0
- package/src/input/handler-shortcuts.ts +8 -0
- package/src/input/keybindings.ts +10 -1
- package/src/input/onboarding/onboarding-feature-units.ts +5 -5
- package/src/input/onboarding/onboarding-wizard-apply.ts +13 -1
- package/src/input/payments-config.ts +167 -0
- package/src/input/provider-key-intake.ts +10 -2
- package/src/input/settings-modal-commit.ts +146 -0
- package/src/input/settings-modal-data.ts +30 -0
- package/src/input/settings-modal-reset.ts +6 -2
- package/src/input/settings-modal-secrets.ts +9 -2
- package/src/input/settings-modal-types.ts +30 -2
- package/src/input/settings-modal.ts +27 -107
- package/src/main.ts +12 -14
- package/src/panels/modals/marketplace-modal.ts +5 -0
- package/src/panels/modals/skills-modal.ts +5 -0
- package/src/renderer/process-indicator.ts +66 -0
- package/src/renderer/settings-modal-helpers.ts +8 -2
- package/src/renderer/settings-modal.ts +16 -1
- package/src/renderer/shell-surface.ts +22 -3
- package/src/runtime/bootstrap-core.ts +7 -7
- package/src/runtime/bootstrap.ts +3 -3
- package/src/runtime/device-posture-composition.ts +110 -0
- package/src/runtime/disposal-wiring.ts +16 -0
- package/src/runtime/install-self-check-startup.ts +23 -0
- package/src/runtime/onboarding/types.ts +6 -1
- package/src/runtime/path-shadow-startup.ts +243 -0
- package/src/runtime/path-shadow.ts +418 -0
- package/src/runtime/reachability-notice.ts +100 -0
- package/src/runtime/runtime-services-types.ts +260 -0
- package/src/runtime/services.ts +95 -209
- package/src/runtime/voice-setup-services.ts +50 -4
- package/src/shell/voice-capture-shell.ts +69 -0
- package/src/verification/verification-ledger.ts +163 -24
- package/src/version.ts +1 -1
|
@@ -21,19 +21,14 @@ import type { ModelPickerTarget } from './model-picker.ts';
|
|
|
21
21
|
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
22
22
|
import type { SubscriptionManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
23
23
|
import type { ServiceInspectionQuery } from '../runtime/ui-service-queries.ts';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
type SettingsSecretsManager,
|
|
28
|
-
} from './settings-modal-secrets.ts';
|
|
24
|
+
import type { SettingsSecretsManager } from './settings-modal-secrets.ts';
|
|
25
|
+
import { CVV_PROMPT_TRADEOFF_WARNING } from '@pellux/goodvibes-sdk/platform/payments';
|
|
26
|
+
import { PAYMENTS_CVV_HANDLING_CONFIG_KEY } from './payments-config.ts';
|
|
29
27
|
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
30
28
|
import type { McpRegistry } from '@pellux/goodvibes-sdk/platform/mcp';
|
|
31
29
|
import type { GatewayMethodCatalog } from '@pellux/goodvibes-sdk/platform/control-plane';
|
|
32
30
|
import type { ConnectionStatus } from './commands/connection-status.ts';
|
|
33
31
|
import { initialConnectionEntries, refreshConnectionEntries } from './settings-modal-connections.ts';
|
|
34
|
-
import { isWorktreeSetupListConfigKey, parseWorktreeSetupListInput } from './worktree-setup-config.ts';
|
|
35
|
-
import { isSandboxExecListConfigKey, parseSandboxExecListInput } from './sandbox-exec-config.ts';
|
|
36
|
-
import { isExecEnvScrubAllowlistConfigKey, parseExecEnvScrubAllowlistInput } from './exec-env-scrub-config.ts';
|
|
37
32
|
|
|
38
33
|
import {
|
|
39
34
|
SETTINGS_CATEGORIES,
|
|
@@ -74,6 +69,7 @@ import {
|
|
|
74
69
|
handleResetConfirmKey as _handleResetConfirmKey,
|
|
75
70
|
type ResetConfirmKeyResult,
|
|
76
71
|
} from './settings-modal-reset.ts';
|
|
72
|
+
import { commitEditValue as _commitEditValue } from './settings-modal-commit.ts';
|
|
77
73
|
|
|
78
74
|
export interface SettingsModalChange {
|
|
79
75
|
readonly key: ConfigKey;
|
|
@@ -555,105 +551,22 @@ export class SettingsModal {
|
|
|
555
551
|
* Returns true on success, false if validation failed.
|
|
556
552
|
*/
|
|
557
553
|
commitEdit(): boolean {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const nextMode = this.editBuffer.trim() as McpEntry['trustMode'];
|
|
577
|
-
const validModes: McpEntry['trustMode'][] = ['constrained', 'ask-on-risk', 'allow-all', 'blocked'];
|
|
578
|
-
if (!validModes.includes(nextMode)) {
|
|
579
|
-
this.editingMode = false;
|
|
580
|
-
this.editBuffer = '';
|
|
581
|
-
this.mcpAllowAllConfirmationTarget = null;
|
|
582
|
-
return false;
|
|
583
|
-
}
|
|
584
|
-
if (nextMode === 'allow-all' && entry.trustMode !== 'allow-all') {
|
|
585
|
-
this.mcpAllowAllConfirmationTarget = entry.name;
|
|
586
|
-
this.editBuffer = '';
|
|
587
|
-
return false;
|
|
588
|
-
}
|
|
589
|
-
this.mcpRegistry.setServerTrustMode(entry.name, nextMode);
|
|
590
|
-
this.mcpEntries = buildMcpEntries(this.mcpRegistry);
|
|
591
|
-
this.editingMode = false;
|
|
592
|
-
this.editBuffer = '';
|
|
593
|
-
this.mcpAllowAllConfirmationTarget = null;
|
|
594
|
-
return true;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const entry = this.getSelected();
|
|
598
|
-
if (!entry || !this.configManager) return false;
|
|
599
|
-
|
|
600
|
-
const { setting } = entry;
|
|
601
|
-
let parsed: unknown = this.editBuffer;
|
|
602
|
-
|
|
603
|
-
if (setting.type === 'number') {
|
|
604
|
-
parsed = Number(this.editBuffer);
|
|
605
|
-
if (isNaN(parsed as number)) {
|
|
606
|
-
this.editingMode = false;
|
|
607
|
-
this.editBuffer = '';
|
|
608
|
-
return false;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (setting.type === 'object') {
|
|
613
|
-
// Object-typed keys (e.g. pricing.modelPrices) edit as JSON; the
|
|
614
|
-
// schema's validate() below still rules on the parsed shape.
|
|
615
|
-
try {
|
|
616
|
-
parsed = JSON.parse(this.editBuffer);
|
|
617
|
-
} catch {
|
|
618
|
-
this.editingMode = false;
|
|
619
|
-
this.editBuffer = '';
|
|
620
|
-
return false;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
if (setting.validate && !setting.validate(parsed)) {
|
|
625
|
-
this.editingMode = false;
|
|
626
|
-
this.editBuffer = '';
|
|
627
|
-
return false;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
if (isWorktreeSetupListConfigKey(setting.key)) {
|
|
631
|
-
// Comma-separated display/edit convention for the array-backed
|
|
632
|
-
// worktree.setup.* keys — see worktree-setup-config.ts.
|
|
633
|
-
this._setValue(setting.key, parseWorktreeSetupListInput(this.editBuffer));
|
|
634
|
-
} else if (isSandboxExecListConfigKey(setting.key)) {
|
|
635
|
-
// Same comma-separated convention for the array-backed
|
|
636
|
-
// sandbox.egressAllowlist / sandbox.workspaceWritable keys — see
|
|
637
|
-
// sandbox-exec-config.ts.
|
|
638
|
-
this._setValue(setting.key, parseSandboxExecListInput(this.editBuffer));
|
|
639
|
-
} else if (isExecEnvScrubAllowlistConfigKey(setting.key)) {
|
|
640
|
-
// Same comma-separated convention for permissions.execEnvScrubAllowlist
|
|
641
|
-
// — see exec-env-scrub-config.ts.
|
|
642
|
-
this._setValue(setting.key, parseExecEnvScrubAllowlistInput(this.editBuffer));
|
|
643
|
-
} else if (setting.type === 'string' && isSecretConfigKey(setting.key)) {
|
|
644
|
-
setSecretBackedSettingValue({
|
|
645
|
-
key: setting.key,
|
|
646
|
-
value: String(parsed ?? ''),
|
|
647
|
-
configManager: this.configManager,
|
|
648
|
-
secretsManager: this.secretsManager,
|
|
649
|
-
setConfigValue: (key, value) => this._setValue(key, value),
|
|
650
|
-
});
|
|
651
|
-
} else {
|
|
652
|
-
this._setValue(setting.key as ConfigKey, parsed);
|
|
653
|
-
}
|
|
654
|
-
this.editingMode = false;
|
|
655
|
-
this.editBuffer = '';
|
|
656
|
-
return true;
|
|
554
|
+
return _commitEditValue({
|
|
555
|
+
editingMode: this.editingMode,
|
|
556
|
+
currentCategory: this.currentCategory,
|
|
557
|
+
editBuffer: this.editBuffer,
|
|
558
|
+
configManager: this.configManager,
|
|
559
|
+
secretsManager: this.secretsManager,
|
|
560
|
+
mcpRegistry: this.mcpRegistry,
|
|
561
|
+
mcpAllowAllConfirmationTarget: this.mcpAllowAllConfirmationTarget,
|
|
562
|
+
getSelectedMcp: () => this.getSelectedMcp(),
|
|
563
|
+
getSelected: () => this.getSelected(),
|
|
564
|
+
setValue: (key, value) => this._setValue(key, value),
|
|
565
|
+
setEditingMode: (v) => { this.editingMode = v; },
|
|
566
|
+
setEditBuffer: (v) => { this.editBuffer = v; },
|
|
567
|
+
setMcpEntries: (entries) => { this.mcpEntries = entries; },
|
|
568
|
+
setMcpAllowAllConfirmationTarget: (v) => { this.mcpAllowAllConfirmationTarget = v; },
|
|
569
|
+
});
|
|
657
570
|
}
|
|
658
571
|
|
|
659
572
|
/** Cancel inline edit without saving. */
|
|
@@ -781,6 +694,13 @@ export class SettingsModal {
|
|
|
781
694
|
this.lastSettingEffectMessage = result.effectMessage;
|
|
782
695
|
}
|
|
783
696
|
// No-op (result.changed === false, effectMessage === null): leave lastSettingEffectMessage untouched.
|
|
697
|
+
|
|
698
|
+
// Selecting 'prompt' for payments.cvvHandling states the tradeoff at the
|
|
699
|
+
// moment of selection, not just in the documentation pane — see the
|
|
700
|
+
// SDK's own CVV_PROMPT_TRADEOFF_WARNING (platform/payments).
|
|
701
|
+
if (key === PAYMENTS_CVV_HANDLING_CONFIG_KEY && value === 'prompt') {
|
|
702
|
+
this.lastSettingEffectMessage = CVV_PROMPT_TRADEOFF_WARNING;
|
|
703
|
+
}
|
|
784
704
|
}
|
|
785
705
|
|
|
786
706
|
/** Refresh live/persisted/pending state for every feature header bound to `key`. */
|
package/src/main.ts
CHANGED
|
@@ -55,6 +55,7 @@ import { applyInitialTuiCliState, reportFatalStartupError } from './cli/tui-star
|
|
|
55
55
|
import { applyConfiguredHitlMode, applyRuntimeConfigValue, applyTuiRuntimeConfigDefaults } from './cli/config-overrides.ts';
|
|
56
56
|
import { renderToolCallBlock } from './renderer/tool-call.ts';
|
|
57
57
|
import { wireSpokenTurnRuntime } from './audio/spoken-turn-wiring.ts';
|
|
58
|
+
import { installVoiceCapture } from './shell/voice-capture-shell.ts';
|
|
58
59
|
import { attachSpokenTurnModelRouting, createSpokenTurnInputOptions } from './audio/spoken-turn-model-routing.ts';
|
|
59
60
|
import { allowTerminalWrite, installTuiTerminalOutputGuard } from './runtime/terminal-output-guard.ts';
|
|
60
61
|
import { installProcessLifecycle } from './runtime/process-lifecycle.ts';
|
|
@@ -189,6 +190,8 @@ async function main() {
|
|
|
189
190
|
const failoverState = createFailoverTurnState();
|
|
190
191
|
|
|
191
192
|
const getPromptContentWidth = () => computePromptContentWidth(stdout.columns);
|
|
193
|
+
// Live-microphone footer row (push-to-talk or the wake detector); assigned once voice capture is wired below, null until then so pre-wiring frames size correctly.
|
|
194
|
+
let voiceCaptureStatus: () => import('./core/voice-capture-status.ts').VoiceCaptureIndicatorState | null = () => null;
|
|
192
195
|
|
|
193
196
|
const getViewportHeight = (): number => {
|
|
194
197
|
if (input.onboardingWizard.active) return stdout.rows || 24;
|
|
@@ -197,7 +200,7 @@ async function main() {
|
|
|
197
200
|
const contextWindow = providerRegistry.getContextWindowForModel(currentModel);
|
|
198
201
|
const rows = stdout.rows || 24;
|
|
199
202
|
// Compact threshold must match buildShellFooter's `compact: height < 30` posture below, else estimateShellFooterHeight's cached-height fast path answers with the wrong mode.
|
|
200
|
-
return rows - 2 - estimateShellFooterHeight(promptLines, contextWindow, rows < 30);
|
|
203
|
+
return rows - 2 - estimateShellFooterHeight(promptLines, contextWindow, rows < 30, voiceCaptureStatus());
|
|
201
204
|
};
|
|
202
205
|
|
|
203
206
|
const scroll = (delta: number) => {
|
|
@@ -333,8 +336,7 @@ async function main() {
|
|
|
333
336
|
render();
|
|
334
337
|
};
|
|
335
338
|
|
|
336
|
-
commandContext.submitInput = submitInput;
|
|
337
|
-
commandContext.submitSpokenInput = (text, content) => submitInput(text, content, { spokenOutput: true });
|
|
339
|
+
commandContext.submitInput = submitInput; commandContext.submitSpokenInput = (text, content) => submitInput(text, content, { spokenOutput: true });
|
|
338
340
|
commandContext.stopSpokenOutput = () => spokenTurns.stop(); commandContext.pasteFromClipboard = () => input.handlePaste();
|
|
339
341
|
// Read-only view of pending [TEXT: pN, M lines] fold markers, so /pastes
|
|
340
342
|
// can preview a folded paste's actual content before the user submits it.
|
|
@@ -418,19 +420,19 @@ async function main() {
|
|
|
418
420
|
},
|
|
419
421
|
);
|
|
420
422
|
|
|
421
|
-
orchestratorRefs.getViewportHeight = getViewportHeight;
|
|
422
|
-
orchestratorRefs.scrollToEnd = scrollToEnd;
|
|
423
|
+
orchestratorRefs.getViewportHeight = getViewportHeight; orchestratorRefs.scrollToEnd = scrollToEnd;
|
|
423
424
|
|
|
424
425
|
input.setCommandRegistry(commandRegistry, commandContext);
|
|
425
426
|
commandContext.openComposerEditor = makeComposerEditorOpener({ buffer: input, stdin, stdout, writeGuard: allowTerminalWrite, repaint: () => { compositor.resetDiff(); render(); }, cwd: workingDir, env: process.env, notify: (m) => systemMessageRouter.high(m) });
|
|
426
427
|
input.setConversationManager(conversation);
|
|
427
|
-
input.setContentWidth(getPromptContentWidth());
|
|
428
|
-
input.filePicker.setOnUpdate(() => render());
|
|
428
|
+
input.setContentWidth(getPromptContentWidth()); input.filePicker.setOnUpdate(() => render());
|
|
429
429
|
// retirement: agentDetailModal/processModal setOnRefresh wiring removed —
|
|
430
430
|
// those modals were deleted (Fleet subsumes the live process tree via F2).
|
|
431
431
|
|
|
432
432
|
// Model picker callback is handled in bootstrap.ts — do not duplicate here.
|
|
433
433
|
input.setHistory(inputHistory);
|
|
434
|
+
// ONE microphone path, shared by push-to-talk voice input (Alt+V) and wake-word detection; opens no device by itself (shell/voice-capture-shell.ts).
|
|
435
|
+
voiceCaptureStatus = installVoiceCapture({ configManager, shellPaths: ctx.services.shellPaths, homeDirectory, sessionId: ctx.runtime.sessionId, commandContext, unsubs, buffer: input, submitInput, notify: (m) => { systemMessageRouter.high(m); render(); }, render: () => render() });
|
|
434
436
|
|
|
435
437
|
const toolCount = toolRegistry.list().length;
|
|
436
438
|
conversation.splashOptions = {
|
|
@@ -511,7 +513,7 @@ async function main() {
|
|
|
511
513
|
composerMode: composerState.modeLabel,
|
|
512
514
|
composerStatus: composerState.statusLabel,
|
|
513
515
|
composerFlags: composerState.flags,
|
|
514
|
-
composerPendingRisk: composerState.pendingRisk, permissionMode: configManager.get('permissions.mode') as string,
|
|
516
|
+
composerPendingRisk: composerState.pendingRisk, permissionMode: configManager.get('permissions.mode') as string, voiceCapture: voiceCaptureStatus(),
|
|
515
517
|
}).lines;
|
|
516
518
|
|
|
517
519
|
const onboardingOwnsScreen = input.onboardingWizard.active;
|
|
@@ -735,9 +737,7 @@ async function main() {
|
|
|
735
737
|
process.on('exit', exitListener);
|
|
736
738
|
|
|
737
739
|
// --- Terminal setup ---
|
|
738
|
-
stdin.setRawMode(true);
|
|
739
|
-
stdin.resume();
|
|
740
|
-
stdin.setEncoding('utf8');
|
|
740
|
+
stdin.setRawMode(true); stdin.resume(); stdin.setEncoding('utf8');
|
|
741
741
|
allowTerminalWrite(() => stdout.write((cli.flags.noAltScreen ? '' : ALT_SCREEN_ENTER) + CLEAR_SCREEN + CURSOR_HIDE + MOUSE_ENABLE + KEYBOARD_EXT_ENABLE + PASTE_ENABLE + FOCUS_ENABLE));
|
|
742
742
|
// forced dark/light applies before first paint; auto (TTY only) fires the
|
|
743
743
|
// OSC 11 probe and repaints once if light wins. filterInput strips the reply from stdin.
|
|
@@ -771,9 +771,7 @@ async function main() {
|
|
|
771
771
|
|
|
772
772
|
input.feed(data);
|
|
773
773
|
});
|
|
774
|
-
process.on('SIGINT', sigintHandler);
|
|
775
|
-
process.on('unhandledRejection', unhandledRejectionHandler);
|
|
776
|
-
stdout.on('resize', resizeHandler);
|
|
774
|
+
process.on('SIGINT', sigintHandler); process.on('unhandledRejection', unhandledRejectionHandler); stdout.on('resize', resizeHandler);
|
|
777
775
|
|
|
778
776
|
// State restores happen ONLY when the user explicitly asks — a CLI flag
|
|
779
777
|
// (--continue/--resume/--fork), a slash command (/session resume,
|
|
@@ -207,6 +207,11 @@ export function createMarketplaceModalSurface(deps: MarketplaceModalDeps): Confi
|
|
|
207
207
|
* missing paths with existsSync → [], so refresh() finds nothing and renders the
|
|
208
208
|
* honest empty-state copy). The random tmp path never appears in the
|
|
209
209
|
* rendered lines (the empty-state copy is static), so the golden is byte-stable.
|
|
210
|
+
*
|
|
211
|
+
* This is production code (ships in the real binary), not test scratch, so
|
|
212
|
+
* it stays rooted at the real OS temp dir rather than the test-only
|
|
213
|
+
* makeProjectTempDir helper. Created and removed synchronously in the same
|
|
214
|
+
* call, so the on-disk window is negligible.
|
|
210
215
|
*/
|
|
211
216
|
export function marketplaceModalGoldenSurface(): ConfigModalSurface {
|
|
212
217
|
const root = mkdtempSync(join(tmpdir(), 'gv-marketplace-golden-'));
|
|
@@ -180,6 +180,11 @@ export function createSkillsModalSurface(deps: SkillsModalDeps): ConfigModalSurf
|
|
|
180
180
|
* homeDir and removed immediately (scanSkillDirectorySync catches missing dirs →
|
|
181
181
|
* []), so refresh() finds no `.goodvibes/skills` directories and renders the
|
|
182
182
|
* static empty-state copy. The random tmp path never appears in the output.
|
|
183
|
+
*
|
|
184
|
+
* This is production code (ships in the real binary), not test scratch, so
|
|
185
|
+
* it stays rooted at the real OS temp dir rather than the test-only
|
|
186
|
+
* makeProjectTempDir helper. Created and removed synchronously in the same
|
|
187
|
+
* call, so the on-disk window is negligible.
|
|
183
188
|
*/
|
|
184
189
|
export function skillsModalGoldenSurface(): ConfigModalSurface {
|
|
185
190
|
const root = mkdtempSync(join(tmpdir(), 'gv-skills-golden-'));
|
|
@@ -4,6 +4,7 @@ import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
|
4
4
|
import { GLYPHS, UI_TONES } from './ui-primitives.ts';
|
|
5
5
|
import { activeUiTones } from './theme.ts';
|
|
6
6
|
import { formatHints } from './hint-grammar.ts';
|
|
7
|
+
import { voiceCaptureRowVisible, type VoiceCaptureIndicatorState } from '../core/voice-capture-status.ts';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* renderProcessIndicator — shows a one-line summary of active background
|
|
@@ -95,3 +96,68 @@ export function renderProcessIndicator(
|
|
|
95
96
|
const hint = ` ${formatHints([{ key: 'Enter', verb: 'View' }])}`;
|
|
96
97
|
return renderPlainStatus(`${label}${hint}`, { fg: activeUiTones().accent.brand, bold: true });
|
|
97
98
|
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Sentence each capture state renders as. Written out per state rather than
|
|
102
|
+
* assembled from fragments because these are the words that tell a user whether
|
|
103
|
+
* their microphone is open — "listening" and "recording" mean different things
|
|
104
|
+
* and a row that blurred them would be worse than no row.
|
|
105
|
+
*/
|
|
106
|
+
const VOICE_CAPTURE_LABELS: Record<VoiceCaptureIndicatorState['kind'], string> = {
|
|
107
|
+
requesting: 'opening the microphone',
|
|
108
|
+
recording: 'recording — press the voice-input key again to stop',
|
|
109
|
+
transcribing: 'transcribing what you said',
|
|
110
|
+
'wake-listening': 'listening for the wake phrase',
|
|
111
|
+
'wake-capturing': 'wake heard — recording what follows',
|
|
112
|
+
'wake-restarting': 'capture stream ended — restarting',
|
|
113
|
+
'wake-latched': 'wake detection stopped',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* renderVoiceCaptureIndicator — the persistent row shown while a microphone is
|
|
118
|
+
* open, below the input area beside the process indicator.
|
|
119
|
+
*
|
|
120
|
+
* It exists because a held-open capture device is otherwise invisible: wake
|
|
121
|
+
* detection runs for as long as the feature is on, and nothing else on screen
|
|
122
|
+
* would say so. `voice.wake.indicator` chooses between `statusline` (one dim
|
|
123
|
+
* row), `banner` (a highlighted row that is hard to miss) and `off`; a
|
|
124
|
+
* push-to-talk recording always renders, because the user pressed a key one
|
|
125
|
+
* moment ago and is waiting on it.
|
|
126
|
+
*
|
|
127
|
+
* Returns no lines when nothing is captured, or when the wake rows are turned
|
|
128
|
+
* off — the caller splices whatever comes back, so an empty array is "no row".
|
|
129
|
+
*/
|
|
130
|
+
export function renderVoiceCaptureIndicator(
|
|
131
|
+
width: number,
|
|
132
|
+
state: VoiceCaptureIndicatorState | null,
|
|
133
|
+
): Line[] {
|
|
134
|
+
if (!voiceCaptureRowVisible(state) || state === null) return [];
|
|
135
|
+
const tones = activeUiTones();
|
|
136
|
+
const marker = state.kind === 'wake-latched' ? GLYPHS.status.blocked : GLYPHS.status.active;
|
|
137
|
+
const device = state.deviceLabel !== null ? ` ${GLYPHS.navigation.pipeSeparator} ${state.deviceLabel}` : '';
|
|
138
|
+
const extra = state.detail !== undefined && state.detail.length > 0
|
|
139
|
+
? ` ${GLYPHS.navigation.pipeSeparator} ${state.detail}`
|
|
140
|
+
: '';
|
|
141
|
+
const body = `${marker} Voice: ${VOICE_CAPTURE_LABELS[state.kind]}${device}${extra}`;
|
|
142
|
+
const isWakeRow = state.kind.startsWith('wake-');
|
|
143
|
+
const fg = state.kind === 'wake-latched' || state.kind === 'wake-restarting'
|
|
144
|
+
? tones.chrome.warn
|
|
145
|
+
: tones.accent.control;
|
|
146
|
+
|
|
147
|
+
if (isWakeRow && state.indicator === 'banner') {
|
|
148
|
+
// The prominent variant: the row is filled to the terminal width on the
|
|
149
|
+
// footer background so it reads as a standing condition, not a passing note.
|
|
150
|
+
const line = UIFactory.stringToLine(' '.repeat(width), width, { fg: tones.chrome.faint });
|
|
151
|
+
const text = ` ${truncateDisplay(body, Math.max(0, width - 4), '…')} `;
|
|
152
|
+
for (let i = 0; i < text.length && 1 + i < width - 1; i++) {
|
|
153
|
+
const cell = line[1 + i]!;
|
|
154
|
+
cell.char = text[i]!;
|
|
155
|
+
cell.fg = fg;
|
|
156
|
+
cell.bg = tones.bg.footer;
|
|
157
|
+
cell.bold = true;
|
|
158
|
+
cell.dim = false;
|
|
159
|
+
}
|
|
160
|
+
return [line];
|
|
161
|
+
}
|
|
162
|
+
return [UIFactory.stringToLine(` ${truncateDisplay(body, Math.max(0, width - 4), '…')}`, width, { fg, bold: true })];
|
|
163
|
+
}
|
|
@@ -77,7 +77,7 @@ export const CATEGORY_INFO: Record<SettingsCategory, string> = {
|
|
|
77
77
|
wrfc: 'Work-review-fix-cycle thresholds, retry limits, and automatic commit behavior.',
|
|
78
78
|
helper: 'Helper model defaults used by helper subsystems when they do not use the main chat route.',
|
|
79
79
|
tts: 'Text-to-speech provider, voice, and optional spoken-turn LLM overrides.',
|
|
80
|
-
voice: 'Two independent voice capabilities. Local engines (voice.local.*): the STT and TTS engine, binary, and model-path for whisper.cpp/faster-whisper and Piper/Kokoro — all empty by default, configurable-not-configured, nothing auto-downloads, and an unconfigured machine reports honestly (never an error); once set, "Local engines" appears in the TTS provider picker beside elevenlabs, with no billing dimension. Wake-word detection (voice.wake.*, rendered as its own unit below): listening continuously for a spoken wake phrase and handing the utterance that follows to speech-to-text — off by default, because holding a microphone open is meant to be an explicit act. Its published recall figures are measured on synthesised speech only, so no human recording of the phrase is behind them.',
|
|
80
|
+
voice: 'Two independent voice capabilities. Local engines (voice.local.*): the STT and TTS engine, binary, and model-path for whisper.cpp/faster-whisper and Piper/Kokoro — all empty by default, configurable-not-configured, nothing auto-downloads, and an unconfigured machine reports honestly (never an error); once set, "Local engines" appears in the TTS provider picker beside elevenlabs, with no billing dimension. Wake-word detection (voice.wake.*, rendered as its own unit below): listening continuously for a spoken wake phrase and handing the utterance that follows to speech-to-text — off by default, because holding a microphone open is meant to be an explicit act. It is live on this terminal: enabling it starts a recorder subprocess (voice.wake.captureCommand), scores every frame with the pinned classifier through a WASM runtime, plays voice.wake.activationSound at a confirmed wake, shows a persistent footer row per voice.wake.indicator, and sends what follows to the same speech-to-text call — into the composer, or submitted outright when voice.wake.autoSubmit is on. The same capture path backs push-to-talk voice input (Alt+V by default), which always lands in the composer. voice.wake.noiseSuppression: speex runs here — the filter travels with the platform, so there is nothing to install for it — and voice.wake.vadThreshold above 0 screens frames with the pinned speech gate once provisioning has fetched it, refusing to start before that rather than scoring frames it claims to be screening. The model is already on this machine: installing goodvibes downloads and checksum-verifies it, and a running daemon retries at boot if the install could not reach the network — so switching this on normally needs no setup step. Switching it on never downloads anything itself; a host whose artifacts are missing or fail verification says which and names /voice wake setup rather than pretending to listen. Its published recall figures are measured on synthesised speech only, so no human recording of the phrase is behind them.',
|
|
81
81
|
service: 'Background service posture: enabled state, autostart, restart behavior, service name, platform, and logs.',
|
|
82
82
|
daemon: 'Local session daemon. It hosts the shared session broker and companion chat so a session started here is visible and steerable from other surfaces. On by default, bound to loopback (127.0.0.1) only.',
|
|
83
83
|
controlPlane: 'Daemon control-plane settings for local admin/API access.',
|
|
@@ -86,6 +86,7 @@ export const CATEGORY_INFO: Record<SettingsCategory, string> = {
|
|
|
86
86
|
batch: 'Batch execution settings, including local vs Cloudflare queue behavior.',
|
|
87
87
|
automation: 'Scheduled and automated run settings, concurrency, timeout, catch-up, cooldown, and retention behavior.',
|
|
88
88
|
checkin: 'Proactive check-in: on a cadence, a briefing is judged and you are contacted only when something warrants it. Off by default; every run — scheduled or manual — leaves a receipt (see /checkin) recording whether it stayed quiet, delivered a message, or was skipped.',
|
|
89
|
+
occasions: 'Proactive occasions and plans: dated, usually-recurring things that need an action (birthdays, anniversaries) and dated ranges that need only to be known about (a vacation). On by default. Lead time before a date, active hours nudges are allowed to arrive in, the nudge channel (empty makes it pull-only through /occasions), cadence while a window is open and the daily final stretch before it closes, whether an away plan moves a nudge earlier, whether a confirmed occasion mirrors to the calendar and whether a mirrored one suppresses its own nudge, how many questions the gift interview asks, how many years of gift history it keeps, and how often the sweep pass runs. The nudge itself never names the date — only how soon.',
|
|
89
90
|
watchers: 'File/process watcher heartbeat, polling, and recovery-window behavior, plus the trigger family (watchers.triggers.*, rendered as its own unit): stream watchers that regex-filter and batch a long-lived command\'s output, model-free condition checks running a probe/extract/rule pipeline with no LLM in the loop, and one-shot on-exit triggers that fire exactly one payload when a launched command terminates. The trigger family is off by default — a trigger supervises real processes with nobody watching — and its rows tune the backoff ladder, the strike breaker, retention bounds, batching, and process caps.',
|
|
90
91
|
runtime: 'Runtime guardrails such as companion chat limiter and event bus listener caps.',
|
|
91
92
|
telemetry: 'Telemetry payload policy.',
|
|
@@ -94,7 +95,7 @@ export const CATEGORY_INFO: Record<SettingsCategory, string> = {
|
|
|
94
95
|
mcp: 'MCP server trust and scope review. Trust changes can expose local files, tools, databases, browsers, or remote automation depending on the server.',
|
|
95
96
|
sandbox: 'Isolation strategy for REPL, MCP, Windows, and QEMU-backed execution. Use these settings to separate risky tools from the host shell.',
|
|
96
97
|
surfaces: 'External app surfaces such as Slack, Discord, ntfy, Home Assistant, Telegram, webhooks, chat bridges, and messaging providers.',
|
|
97
|
-
device: 'Posture for using a PAIRED phone as a tool — its cameras, its screen, its location, its clipboard, and a small set of device effects.
|
|
98
|
+
device: 'Posture for using a PAIRED phone as a tool — its cameras, its screen, its location, its clipboard, and a small set of device effects. Every row here changes behaviour, and every change applies to the next request without a restart. device.capabilities.mode is the master switch: off stops every capability request reaching any paired device (nothing is dispatched and nobody is asked), ask-every-time asks on every single request and never consults or offers a durable grant, and honor-grants (stock) asks the first time and every time after unless you chose "always allow" for that one capability on that one phone. allowAlwaysOffer decides which capabilities may offer that durable grant at all: every-capability (stock), standard-only (the elevated ones — front camera, screen capture, precise location, clipboard read — keep asking and an "always" answer is honoured for that one request only), or never. requestTimeoutSeconds is the deadline on three things at once: how long the phone has to answer, the deadline written into the request the phone receives, and how long the confirmation prompt itself stays open. device.location.precision gates the street-level fix (coarse-only refuses it and still serves approximate; ask-precise serves it but never remembers it), and device.clipboard.readMode gates reading the clipboard the same way — putting text ON the clipboard is a different capability and is unaffected. Captures from the camera and the screen are kept for capture.retentionHours and capped at capture.maxArtifacts, whichever bites first; housekeeping runs at every start and then every capture.sweepIntervalMinutes, deleting expired grants and captures and recording what it removed and why. A grant lasts grants.expiryDays and is never honoured past it, one phone holds at most grants.maxPerNode grants (the oldest are reaped past the cap), and the ledger of grants given, used, revoked and expired is kept for grants.auditRetentionDays. device.nodes.maxPaired is enforced at the moment a device pairs: at the cap a NEW device is refused with a message naming the setting, the cap and how many are already paired, while a phone that is already in the list simply supersedes its own record and keeps working. Lowering the cap below the number already paired unpairs no one — existing devices keep working and only the next new pairing is refused; a device moving off the legacy shared token to its own is exempt so it cannot be stranded. A nonsensical value anywhere here (zero, negative, not a number, an enum value that is not on the list) is read as the stock value rather than passed through, so a broken setting can neither lock you out of your own daemon nor widen the posture.',
|
|
98
99
|
conversationGate: 'What a message arriving from one of those channels does. By default it gets a conversational answer, and work is proposed and waits for your agreement rather than starting on its own; you can instead confirm every run, or restore the old behavior where a message starts work immediately. Also how long a pending proposal stays answerable and how many can wait at once. This governs messages from channels only — what you type here always runs, and schedules and triggers were authorized when created.',
|
|
99
100
|
cloudflare: 'Optional Cloudflare control plane, batch queue, Worker, Tunnel, Access, DNS, KV, Durable Objects, Secrets Store, and R2 settings.',
|
|
100
101
|
release: 'Release-channel preference.',
|
|
@@ -112,6 +113,8 @@ export const CATEGORY_INFO: Record<SettingsCategory, string> = {
|
|
|
112
113
|
notifications: 'Notification router burst-suppression tuning: burst window, threshold, and cooldown. Gated by the Adaptive Notification Suppression feature.',
|
|
113
114
|
relay: 'Outbound zero-knowledge relay reachability, for reaching this daemon from outside the LAN without opening an inbound port. Off by default — relay.enabled is the switch. The relay operator sees only ciphertext and connection metadata — self-host your own relay for full control.',
|
|
114
115
|
learning: 'Idle-time memory consolidation: merging duplicate standing memory records and decaying/archiving stale ones. Off by default — nothing runs until enabled.',
|
|
116
|
+
payments: 'Payment card on file for daemon-initiated purchases: card number, expiry, CVV, cardholder name, billing and shipping address, and whether the CVV is stored or requested at purchase time. Card fields are entered through /payments card (concealed input) and are secret-tier — never rendered in plaintext.',
|
|
117
|
+
profile: 'What the platform knows about you — your name, how to reach you, where you are, how you like answers written, the people and places you mention — kept as one Markdown file at daemon scope that you can open and edit by hand at any time; your edits win and are never rewritten. These rows decide whether it is loaded at all, whether the assistant records facts it learns from things you say directly to it, whether it tells you in one line each time it does, whether the harmless part (how you are addressed, your city, your timezone, your formatting preferences, your style) is put in the model\'s context so it stops guessing, whether using a private detail such as your address is announced in the reply, whether settings left unset read their value from the profile, and where the file lives. Everything private — your name, contact details, home address, billing and shipping details, quiet hours, and the People, Places, Work and Notes sections — is never put in context in bulk and is reachable only by a named lookup that is disclosed when it happens. Run /profile to read the whole thing, /profile where <field> to see the surface, date and your exact words behind any recorded line, and /profile forget <field> to delete one.',
|
|
115
118
|
};
|
|
116
119
|
|
|
117
120
|
export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], string> = {
|
|
@@ -139,6 +142,7 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
|
|
|
139
142
|
batch: 'Batch',
|
|
140
143
|
automation: 'Automation',
|
|
141
144
|
checkin: 'Check-in',
|
|
145
|
+
occasions: 'Occasions & Plans',
|
|
142
146
|
watchers: 'Watchers',
|
|
143
147
|
runtime: 'Runtime',
|
|
144
148
|
telemetry: 'Telemetry',
|
|
@@ -165,6 +169,8 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
|
|
|
165
169
|
connections: 'Connections',
|
|
166
170
|
policy: 'Policy-as-Code',
|
|
167
171
|
notifications: 'Notifications',
|
|
172
|
+
payments: 'Payments',
|
|
173
|
+
profile: 'Owner Profile',
|
|
168
174
|
};
|
|
169
175
|
|
|
170
176
|
const SETTING_LABELS: Partial<Record<string, string>> = {
|
|
@@ -13,6 +13,8 @@ import { getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
|
13
13
|
import { CATEGORY_INFO, CATEGORY_LABELS, describeUiRouting, formatValue, getSettingLabel, inferSubscriptionRouteReason, valueColor } from './settings-modal-helpers.ts';
|
|
14
14
|
import { buildConnectionContext, renderConnectionRows } from './settings-modal-connections.ts';
|
|
15
15
|
import { isSecretConfigKey } from '../config/secret-config.ts';
|
|
16
|
+
import { maskConcealedText } from '../input/concealed-input.ts';
|
|
17
|
+
import { CVV_PROMPT_TRADEOFF_WARNING } from '@pellux/goodvibes-sdk/platform/payments';
|
|
16
18
|
import { GLYPHS } from './ui-primitives.ts';
|
|
17
19
|
import { formatHints, joinHints } from './hint-grammar.ts';
|
|
18
20
|
import {
|
|
@@ -105,6 +107,10 @@ const ENUM_VALUE_DESCRIPTIONS: Record<string, Record<string, string>> = {
|
|
|
105
107
|
'meta-cloud': 'Use Meta Cloud API credentials and identifiers.',
|
|
106
108
|
bridge: 'Use a bridge service URL/token flow instead of direct Meta Cloud API delivery.',
|
|
107
109
|
},
|
|
110
|
+
'payments.cvvHandling': {
|
|
111
|
+
stored: 'Store the CVV through the secret manager so the daemon can complete a purchase unattended.',
|
|
112
|
+
prompt: CVV_PROMPT_TRADEOFF_WARNING,
|
|
113
|
+
},
|
|
108
114
|
};
|
|
109
115
|
|
|
110
116
|
function paddedWrapped(text: string, width: number, prefix = ''): string[] {
|
|
@@ -121,7 +127,16 @@ function formatDefaultValue(value: unknown): string {
|
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
function currentSettingValue(modal: SettingsModal, entry: SettingEntry, selected: boolean): string {
|
|
124
|
-
if (selected && modal.editingMode)
|
|
130
|
+
if (selected && modal.editingMode) {
|
|
131
|
+
// Secret-backed keys (surfaces.*.botToken, .signingSecret, etc. — see
|
|
132
|
+
// secret-config.ts) must never echo the in-progress plaintext buffer:
|
|
133
|
+
// not in the row, not in the "Current: ..." context line, not in search
|
|
134
|
+
// results. Reuse the composer's concealed-input mask (concealed-input.ts)
|
|
135
|
+
// rather than a second masking implementation — same bullet-per-character
|
|
136
|
+
// shape, so keystrokes still visibly register without revealing content.
|
|
137
|
+
const buffer = isSecretConfigKey(entry.setting.key) ? maskConcealedText(modal.editBuffer) : modal.editBuffer;
|
|
138
|
+
return `${buffer}${GLYPHS.surface.cursor}`;
|
|
139
|
+
}
|
|
125
140
|
return formatValue(entry);
|
|
126
141
|
}
|
|
127
142
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import { renderProcessIndicator } from './process-indicator.ts';
|
|
2
|
+
import { renderProcessIndicator, renderVoiceCaptureIndicator } from './process-indicator.ts';
|
|
3
3
|
import { UIFactory } from './ui-factory.ts';
|
|
4
|
+
import { voiceCaptureRowVisible, type VoiceCaptureIndicatorState } from '../core/voice-capture-status.ts';
|
|
4
5
|
|
|
5
6
|
export interface ShellFooterBuildOptions {
|
|
6
7
|
readonly width: number;
|
|
@@ -84,6 +85,14 @@ export interface ShellFooterBuildOptions {
|
|
|
84
85
|
readonly webSurfaceUrl?: string;
|
|
85
86
|
/** True while power.keepAwake holds — renders the always-visible "sleep disabled" chip. */
|
|
86
87
|
readonly powerKeepAwake?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Live microphone state — a push-to-talk recording, or the wake detector for
|
|
90
|
+
* as long as it runs. Rendered as a persistent row beside the process
|
|
91
|
+
* indicator, because a capture device held open with nothing on screen saying
|
|
92
|
+
* so is the one state a voice feature must never be in. Null (or a wake state
|
|
93
|
+
* with `voice.wake.indicator: off`) renders nothing.
|
|
94
|
+
*/
|
|
95
|
+
readonly voiceCapture?: VoiceCaptureIndicatorState | null;
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
export interface ShellFooterBuildResult {
|
|
@@ -98,6 +107,8 @@ export interface ShellFooterBuildResult {
|
|
|
98
107
|
const FOOTER_BASE_ROWS = 5;
|
|
99
108
|
const CONTEXT_PROGRESS_ROWS = 1;
|
|
100
109
|
const PROCESS_INDICATOR_ROWS = 1;
|
|
110
|
+
/** The live-microphone row, when one is showing (see renderVoiceCaptureIndicator). */
|
|
111
|
+
const VOICE_CAPTURE_ROWS = 1;
|
|
101
112
|
// Compact posture drops the context-info line (and never shows the context
|
|
102
113
|
// bar or process indicator), leaving just: prompt-box top border, bottom
|
|
103
114
|
// border, token-usage line, and the help/exit line.
|
|
@@ -151,6 +162,7 @@ export function estimateShellFooterHeight(
|
|
|
151
162
|
promptLineCount: number,
|
|
152
163
|
contextWindow?: number,
|
|
153
164
|
compact = false,
|
|
165
|
+
voiceCapture: VoiceCaptureIndicatorState | null = null,
|
|
154
166
|
): number {
|
|
155
167
|
if (lastRenderedFooterHeight !== null && lastRenderedFooterHeight.compact === compact) {
|
|
156
168
|
return lastRenderedFooterHeight.height;
|
|
@@ -160,7 +172,11 @@ export function estimateShellFooterHeight(
|
|
|
160
172
|
return COMPACT_FOOTER_BASE_ROWS + safePromptLines;
|
|
161
173
|
}
|
|
162
174
|
const progressRows = contextWindow && contextWindow > 0 ? CONTEXT_PROGRESS_ROWS : 0;
|
|
163
|
-
|
|
175
|
+
// Counted on the cold-start path too: a shell launched with the wake detector
|
|
176
|
+
// already listening renders that row in its very first frame, and a viewport
|
|
177
|
+
// sized one row too tall would draw the transcript's last line under it.
|
|
178
|
+
const voiceRows = voiceCaptureRowVisible(voiceCapture) ? VOICE_CAPTURE_ROWS : 0;
|
|
179
|
+
return FOOTER_BASE_ROWS + safePromptLines + progressRows + PROCESS_INDICATOR_ROWS + voiceRows;
|
|
164
180
|
}
|
|
165
181
|
|
|
166
182
|
export function buildShellFooter(
|
|
@@ -206,7 +222,10 @@ export function buildShellFooter(
|
|
|
206
222
|
options.runningAgentProgress,
|
|
207
223
|
);
|
|
208
224
|
const inputBoxRows = Math.max(1, options.promptLineCount) + 2;
|
|
209
|
-
|
|
225
|
+
// The voice row sits directly under the prompt box, ABOVE the process
|
|
226
|
+
// indicator: an open microphone is a live condition the user is acting inside,
|
|
227
|
+
// while the process indicator is a background summary.
|
|
228
|
+
lines.splice(inputBoxRows, 0, ...renderVoiceCaptureIndicator(options.width, options.voiceCapture ?? null), ...processIndicator);
|
|
210
229
|
// Scriptable status line — dim informational line above the prompt. Unshifted
|
|
211
230
|
// before the context hint so the context hint (if any) sits above it.
|
|
212
231
|
if (options.scriptableStatusLine) {
|
|
@@ -20,11 +20,11 @@ import { createRuntimeStore, createDomainDispatch, type RuntimeStore } from './s
|
|
|
20
20
|
import {
|
|
21
21
|
type MutableRuntimeState, type SessionEvent, bindFeatureSettingsBridge, createFeatureFlagManager, deriveFeatureStates, RuntimeEventBus, ForensicsCollector,
|
|
22
22
|
ForensicsRegistry, generateUserSessionId, loadBootstrapSystemPrompt, syncConfiguredServices,
|
|
23
|
-
registerBootstrapHookBridge, registerBootstrapRuntimeEvents,
|
|
23
|
+
registerBootstrapHookBridge, registerBootstrapRuntimeEvents, configureRuntimeEventBusDefaults, runtimeEventBusOptionsFrom,
|
|
24
24
|
} from '@/runtime/index.ts';
|
|
25
25
|
import { readExecEnvScrubAllowlist } from '../input/exec-env-scrub-config.ts';
|
|
26
26
|
import { createSandboxExecAsk, sandboxExecAskDepsFromRuntime } from '../permissions/sandbox-exec-gate.ts';
|
|
27
|
-
import { createRuntimeServices, type RuntimeServices } from './services.ts';
|
|
27
|
+
import { createRuntimeServices, installDevicePosture, type RuntimeServices } from './services.ts';
|
|
28
28
|
import { createHostPowerSeam } from '@pellux/goodvibes-sdk/platform/power';
|
|
29
29
|
import { runBootMemoryFold } from './memory-fold.ts';
|
|
30
30
|
import { wireCostPricing } from '../export/cost-utils.ts';
|
|
@@ -238,7 +238,7 @@ export async function initializeBootstrapCore(
|
|
|
238
238
|
bindFeatureSettingsBridge(configManager, featureFlags);
|
|
239
239
|
|
|
240
240
|
const userSessionId = `user-${generateUserSessionId()}`;
|
|
241
|
-
const runtimeBus = new RuntimeEventBus();
|
|
241
|
+
configureRuntimeEventBusDefaults(runtimeEventBusOptionsFrom((key) => configManager.get(key))); const runtimeBus = new RuntimeEventBus(); // cap from runtime.eventBus.maxListeners before the first bus exists
|
|
242
242
|
const store = createRuntimeStore();
|
|
243
243
|
const domainDispatch = createDomainDispatch(store);
|
|
244
244
|
let getConversationTitle = (): string | undefined => undefined;
|
|
@@ -249,9 +249,9 @@ export async function initializeBootstrapCore(
|
|
|
249
249
|
runtimeStore: store,
|
|
250
250
|
getConversationTitle: () => getConversationTitle(),
|
|
251
251
|
workingDir, homeDirectory, ...(options.daemonHomeDirectory === undefined ? {} : { daemonHomeDirectory: options.daemonHomeDirectory }),
|
|
252
|
-
// Embedded topology: the in-process runtime IS the daemon
|
|
253
|
-
//
|
|
254
|
-
powerSeam: createHostPowerSeam(),
|
|
252
|
+
// Embedded topology: the in-process runtime IS the daemon, so it holds its own OS inhibitor (fork
|
|
253
|
+
// default is non-spawning) and owns the wake-model boot retry a standalone daemon would. See services.ts.
|
|
254
|
+
powerSeam: createHostPowerSeam(), provisionWakeModelsAtBoot: true,
|
|
255
255
|
});
|
|
256
256
|
await services.workspaceTrustManager.load(); // settle any already-persisted trust decision before any tool runs
|
|
257
257
|
const providerRegistry = services.providerRegistry;
|
|
@@ -381,7 +381,7 @@ export async function initializeBootstrapCore(
|
|
|
381
381
|
contextAccountingHolder: services.contextAccountingHolder,
|
|
382
382
|
// First contained (sandboxed) command run announces "commands now run contained" once — recorded and surfaced now.
|
|
383
383
|
onSandboxedRun: createSandboxContainmentNotice({ configManager, notify: (text) => conversation.log(`[Sandbox] ${text}`, { fg: '135' }) }),
|
|
384
|
-
});
|
|
384
|
+
}); installDevicePosture(toolRegistry, services.devicePosture); // the `phone` tool is the only path that reaches the device capability service, so without this every `device.*` posture key governs nothing a session can observe; the housekeeping timer starts here (not at composition) so building a runtime in a test sweeps nothing
|
|
385
385
|
// Note: installWrfcAgentToolGuard is called after routeOrBuffer is defined (further below) so the onTrace callback routes guard decisions through the pre-router buffer.
|
|
386
386
|
services.agentOrchestrator.setDependencies({
|
|
387
387
|
surfaceRoot: services.surface.surfaceRoot,
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -41,7 +41,7 @@ import { initializeBootstrapCore } from './bootstrap-core.ts';
|
|
|
41
41
|
import { ensureBootModelResolvable } from './provider-fallback.ts';
|
|
42
42
|
import { createBootstrapShell } from './bootstrap-shell.ts';
|
|
43
43
|
import { announceResumeState } from './resume-notice.ts';
|
|
44
|
-
import {
|
|
44
|
+
import { announceInstallHealth } from './install-self-check-startup.ts';
|
|
45
45
|
import { buildSharedOrchestratorCoreServices, refreshMemoryRecallSnapshot } from './orchestrator-core-services.ts';
|
|
46
46
|
import { consumeDaemonAttachNotices, consumeExternalDaemonAttachNotices } from './daemon-attach-notices.ts';
|
|
47
47
|
import { wireContextAccountingSource } from './context-accounting-source.ts';
|
|
@@ -264,7 +264,7 @@ export async function bootstrapRuntime(
|
|
|
264
264
|
}, ACP_TASK_SYNC_INTERVAL_MS);
|
|
265
265
|
bootstrapUnsubs.push(() => clearInterval(acpTaskSyncInterval));
|
|
266
266
|
orchestrator.registerDelegateTool(acpManager);
|
|
267
|
-
const opsTaskManager = createTaskManager(store, runtimeBus, userSessionId);
|
|
267
|
+
const opsTaskManager = createTaskManager(store, runtimeBus, userSessionId, services.featureFlags); // featureFlags required: without it runtime.unifiedTasks configured nothing (permissive fallback); SDK default matches shipped behaviour, so this is a no-op for existing installs.
|
|
268
268
|
// Operator interventions ride the control-plane gateway capability
|
|
269
269
|
// (controlPlane.gateway, on by default; the old never-registered gate id shipped this dead).
|
|
270
270
|
const opsControlPlane = services.featureFlags.isEnabled('control-plane-gateway')
|
|
@@ -322,7 +322,7 @@ export async function bootstrapRuntime(
|
|
|
322
322
|
}).catch(() => {
|
|
323
323
|
// Best-effort — never let the resume notice block or crash boot.
|
|
324
324
|
});
|
|
325
|
-
|
|
325
|
+
announceInstallHealth(systemMessageRouter);
|
|
326
326
|
const { gitStatusProvider, inputHistory, lastGitInfoRef } = shell;
|
|
327
327
|
// FIX 2: dispose the header's live-repo-state poll (git-status.ts
|
|
328
328
|
// startPolling) on shutdown, same pattern as acpTaskSyncInterval above.
|