@pellux/goodvibes-tui 1.16.1 → 1.18.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 +138 -9
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +5490 -1806
- package/package.json +7 -7
- package/src/cli/completions/generate.ts +5 -4
- package/src/cli/config-overrides.ts +51 -11
- package/src/cli/doctor.ts +4 -5
- package/src/cli/entrypoint.ts +13 -8
- package/src/cli/help.ts +7 -6
- package/src/cli/launch-auto-update.ts +214 -0
- package/src/cli/management-commands.ts +82 -15
- package/src/cli/management.ts +0 -2
- package/src/cli/parser.ts +5 -0
- package/src/cli/status.ts +13 -0
- package/src/cli/surface-command.ts +5 -5
- package/src/cli/tui-startup.ts +60 -97
- package/src/cli/types.ts +2 -0
- package/src/config/index.ts +1 -1
- package/src/config/tui-extension-settings.ts +33 -0
- package/src/core/composer-state.ts +1 -1
- package/src/core/consolidation-receipt.ts +41 -0
- package/src/core/conversation-line-cache.ts +3 -3
- package/src/core/conversation-rendering.ts +18 -9
- package/src/core/conversation.ts +2 -2
- package/src/core/long-task-notifier.ts +7 -7
- package/src/core/memory-provenance.ts +67 -0
- package/src/core/pairing-devices.ts +49 -0
- package/src/core/pairing-handoff.ts +101 -0
- package/src/core/pairing-offers.ts +83 -0
- package/src/core/pairing-origin.ts +78 -0
- package/src/core/pairing-tailscale-gateway.ts +56 -0
- package/src/core/power-chip-source.ts +91 -0
- package/src/core/power-status.ts +95 -0
- package/src/core/stable-host.ts +171 -0
- package/src/core/stream-event-wiring.ts +29 -6
- package/src/core/system-message-noise.ts +1 -1
- package/src/core/system-message-router.ts +6 -6
- package/src/core/turn-budget-outcome.ts +60 -0
- package/src/core/turn-cancellation.ts +40 -1
- package/src/core/turn-event-wiring.ts +14 -8
- package/src/daemon/cli.ts +64 -58
- package/src/daemon/lifecycle.ts +41 -0
- package/src/daemon/service-commands.ts +5 -5
- package/src/export/cost-utils.ts +128 -6
- package/src/input/autocomplete.ts +2 -2
- package/src/input/command-registry.ts +55 -9
- package/src/input/commands/acp-runtime.ts +72 -0
- package/src/input/commands/ci-runtime.ts +5 -1
- package/src/input/commands/codebase-runtime.ts +6 -6
- package/src/input/commands/cost-runtime.ts +1 -1
- package/src/input/commands/devices-runtime.ts +109 -0
- package/src/input/commands/git-runtime.ts +28 -4
- package/src/input/commands/graph-runtime.ts +45 -0
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/intelligence-runtime.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +1 -1
- package/src/input/commands/local-provider-runtime.ts +49 -10
- package/src/input/commands/local-runtime.ts +53 -5
- package/src/input/commands/memory.ts +2 -2
- package/src/input/commands/operator-panel-runtime.ts +5 -5
- package/src/input/commands/operator-rpc.ts +32 -11
- package/src/input/commands/permissions-provenance.ts +1 -1
- package/src/input/commands/permissions-runtime.ts +99 -8
- package/src/input/commands/planning-runtime.ts +3 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -1
- package/src/input/commands/power-runtime.ts +56 -0
- package/src/input/commands/provider-accounts-runtime.ts +71 -6
- package/src/input/commands/provider.ts +7 -5
- package/src/input/commands/qrcode-runtime.ts +14 -19
- package/src/input/commands/queue-runtime.ts +70 -0
- package/src/input/commands/recall-files-git.ts +13 -51
- package/src/input/commands/recall-files.ts +8 -11
- package/src/input/commands/recall-review.ts +2 -2
- package/src/input/commands/relay-runtime.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/runtime-services.ts +4 -4
- package/src/input/commands/schedule-runtime.ts +14 -7
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +2 -2
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +46 -68
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/update-runtime.ts +153 -96
- package/src/input/commands/work-plan-runtime.ts +1 -1
- package/src/input/commands/workstream-attempts.ts +6 -1
- package/src/input/commands/workstream-runtime.ts +46 -27
- package/src/input/commands/worktree-runtime.ts +35 -8
- package/src/input/commands.ts +10 -2
- package/src/input/config-modal-types.ts +14 -3
- package/src/input/config-modal.ts +75 -13
- package/src/input/feature-unit-layout.ts +93 -82
- package/src/input/feed-context-factory.ts +2 -2
- package/src/input/handler-command-route.ts +2 -2
- package/src/input/handler-content-actions.ts +1 -1
- package/src/input/handler-feed-routes.ts +7 -7
- package/src/input/handler-feed.ts +4 -4
- package/src/input/handler-interactions.ts +3 -2
- package/src/input/handler-modal-routes.ts +12 -4
- package/src/input/handler-modal-token-routes.ts +1 -1
- package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
- package/src/input/handler-onboarding.ts +3 -2
- package/src/input/handler-picker-routes.ts +26 -13
- package/src/input/handler-shortcuts.ts +29 -6
- package/src/input/handler.ts +2 -2
- package/src/input/keybindings.ts +21 -3
- package/src/input/onboarding/onboarding-feature-units.ts +34 -27
- package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
- package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
- package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
- package/src/input/panel-integration-actions.ts +2 -2
- package/src/input/panel-mouse-geometry.ts +1 -1
- package/src/input/panel-paste-flood-guard.ts +1 -1
- package/src/input/provider-key-intake.ts +167 -0
- package/src/input/sandbox-exec-config.ts +2 -2
- package/src/input/selection-modal.ts +1 -1
- package/src/input/settings-modal-activation.ts +5 -0
- package/src/input/settings-modal-adjustment.ts +4 -31
- package/src/input/settings-modal-data.ts +52 -57
- package/src/input/settings-modal-mutations.ts +12 -68
- package/src/input/settings-modal-types.ts +27 -17
- package/src/input/settings-modal.ts +74 -55
- package/src/main.ts +90 -90
- package/src/panels/agent-inspector-shared.ts +4 -4
- package/src/panels/base-panel.ts +2 -2
- package/src/panels/builtin/agent.ts +4 -4
- package/src/panels/builtin/development.ts +4 -4
- package/src/panels/builtin/knowledge.ts +1 -1
- package/src/panels/builtin/operations.ts +83 -11
- package/src/panels/builtin/session.ts +5 -5
- package/src/panels/builtin/shared.ts +16 -4
- package/src/panels/builtin-modals.ts +59 -42
- package/src/panels/builtin-panels.ts +1 -1
- package/src/panels/cost-tracker-panel.ts +124 -7
- package/src/panels/diff-panel.ts +39 -6
- package/src/panels/eval-registry.ts +2 -2
- package/src/panels/fleet-acts.ts +446 -0
- package/src/panels/fleet-deep-link.ts +1 -1
- package/src/panels/fleet-gateway.ts +117 -0
- package/src/panels/fleet-observed-render.ts +128 -0
- package/src/panels/fleet-panel-format.ts +163 -11
- package/src/panels/fleet-panel-worktree-detail.ts +14 -0
- package/src/panels/fleet-panel.ts +73 -57
- package/src/panels/fleet-read-model.ts +84 -14
- package/src/panels/fleet-spawn.ts +266 -0
- package/src/panels/fleet-steer.ts +2 -2
- package/src/panels/fleet-stop.ts +41 -5
- package/src/panels/fleet-tabs.ts +15 -16
- package/src/panels/fleet-transcript.ts +5 -5
- package/src/panels/git-panel.ts +2 -2
- package/src/panels/index.ts +4 -1
- package/src/panels/local-auth-panel.ts +1 -1
- package/src/panels/memory-consolidation-gateway.ts +94 -0
- package/src/panels/modals/devices-modal.ts +161 -0
- package/src/panels/modals/hooks-modal.ts +1 -1
- package/src/panels/modals/keybindings-modal.ts +2 -2
- package/src/panels/modals/knowledge-modal.ts +1 -1
- package/src/panels/modals/marketplace-modal.ts +4 -4
- package/src/panels/modals/memory-modal.ts +153 -9
- package/src/panels/modals/modal-surface-helpers.ts +1 -1
- package/src/panels/modals/modal-theme.ts +1 -1
- package/src/panels/modals/pairing-modal.ts +198 -68
- package/src/panels/modals/planning-modal.ts +2 -2
- package/src/panels/modals/plugins-modal.ts +1 -1
- package/src/panels/modals/policy-modal.ts +1 -1
- package/src/panels/modals/security-modal.ts +7 -1
- package/src/panels/modals/skills-modal.ts +1 -1
- package/src/panels/modals/work-plan-modal.ts +3 -3
- package/src/panels/notifications-feed.ts +127 -0
- package/src/panels/notifications-panel.ts +160 -0
- package/src/panels/panel-manager.ts +9 -9
- package/src/panels/plugins-panel.ts +4 -4
- package/src/panels/project-planning-answer-actions.ts +1 -1
- package/src/panels/provider-health-routes.ts +2 -2
- package/src/panels/skills-panel.ts +4 -5
- package/src/panels/token-budget-panel.ts +11 -3
- package/src/panels/types.ts +5 -5
- package/src/panels/workstream-graph-render.ts +123 -0
- package/src/permissions/broker-approval-card.ts +201 -0
- package/src/permissions/hunk-selection.ts +28 -4
- package/src/permissions/prompt.ts +244 -29
- package/src/renderer/autocomplete-overlay.ts +107 -54
- package/src/renderer/code-block.ts +2 -2
- package/src/renderer/compaction-history-modal.ts +1 -1
- package/src/renderer/compaction-preview.ts +1 -1
- package/src/renderer/compaction-quality.ts +1 -1
- package/src/renderer/composer-fragments.ts +69 -0
- package/src/renderer/compositor.ts +2 -2
- package/src/renderer/config-modal.ts +1 -1
- package/src/renderer/conversation-overlays.ts +1 -1
- package/src/renderer/footer-tips.ts +3 -3
- package/src/renderer/hint-grammar.ts +1 -1
- package/src/renderer/modal-factory.ts +1 -1
- package/src/renderer/model-picker-overlay.ts +1 -1
- package/src/renderer/model-workspace.ts +7 -2
- package/src/renderer/panel-composite.ts +2 -2
- package/src/renderer/panel-workspace-bar.ts +1 -1
- package/src/renderer/process-indicator.ts +1 -1
- package/src/renderer/selection-modal-overlay.ts +1 -1
- package/src/renderer/session-picker-modal.ts +2 -2
- package/src/renderer/settings-modal-helpers.ts +23 -5
- package/src/renderer/settings-modal.ts +129 -39
- package/src/renderer/shell-surface.ts +6 -0
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/theme.ts +3 -3
- package/src/renderer/tool-call.ts +13 -7
- package/src/renderer/tool-result-summary.ts +1 -1
- package/src/renderer/turn-injection.ts +7 -6
- package/src/renderer/ui-factory.ts +38 -1
- package/src/runtime/bootstrap-command-context.ts +7 -2
- package/src/runtime/bootstrap-command-parts.ts +9 -5
- package/src/runtime/bootstrap-core.ts +41 -41
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap-shell.ts +13 -5
- package/src/runtime/bootstrap.ts +34 -32
- package/src/runtime/channel-composition.ts +46 -0
- package/src/runtime/code-index-services.ts +3 -4
- package/src/runtime/daemon-attach-notices.ts +120 -0
- package/src/runtime/durability-services.ts +90 -0
- package/src/runtime/embedded-service-factories.ts +40 -0
- package/src/runtime/feature-settings.ts +147 -0
- package/src/runtime/fleet-services.ts +41 -0
- package/src/runtime/idle-power-services.ts +85 -0
- package/src/runtime/index.ts +6 -21
- package/src/runtime/interaction-seams.ts +109 -0
- package/src/runtime/legacy-daemon-migration.ts +29 -19
- package/src/runtime/memory-fold.ts +1 -1
- package/src/runtime/notification-dispatch.ts +110 -0
- package/src/runtime/onboarding/apply.ts +3 -29
- package/src/runtime/onboarding/fast-path.ts +39 -6
- package/src/runtime/onboarding/provider-key-capture.ts +126 -0
- package/src/runtime/onboarding/snapshot.ts +0 -1
- package/src/runtime/onboarding/types.ts +9 -3
- package/src/runtime/power-keepawake-remote.ts +75 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/provider-fallback.ts +55 -0
- package/src/runtime/provider-optimizer-wiring.ts +55 -0
- package/src/runtime/release-artifacts.ts +19 -103
- package/src/runtime/resume-notice.ts +1 -1
- package/src/runtime/services.ts +113 -113
- package/src/runtime/session-continuity-hints.ts +38 -0
- package/src/runtime/surface-feature-flags.ts +40 -9
- package/src/runtime/terminal-output-guard.ts +2 -2
- package/src/runtime/trust/workspace-registration.ts +10 -2
- package/src/runtime/trust/workspace-trust.ts +58 -31
- package/src/runtime/ui-services.ts +6 -1
- package/src/runtime/update-check.ts +15 -69
- package/src/runtime/workstream-services.ts +2 -2
- package/src/shell/blocking-input.ts +108 -132
- package/src/shell/recovery-input-helpers.ts +53 -166
- package/src/shell/ui-openers.ts +38 -6
- package/src/utils/format-duration.ts +1 -1
- package/src/verification/verification-ledger.ts +21 -4
- package/src/version.ts +1 -1
- package/src/input/commands/flags-runtime.ts +0 -328
package/src/cli/parser.ts
CHANGED
|
@@ -87,6 +87,7 @@ function createDefaultFlags(): GoodVibesCliFlags {
|
|
|
87
87
|
fork: undefined,
|
|
88
88
|
yes: false,
|
|
89
89
|
nonInteractive: false,
|
|
90
|
+
strict: false,
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
93
|
|
|
@@ -260,6 +261,10 @@ export function parseGoodVibesCli(
|
|
|
260
261
|
flags = withFlag(withFlag(flags, 'nonInteractive', true), 'yes', true);
|
|
261
262
|
continue;
|
|
262
263
|
}
|
|
264
|
+
if (name === '--strict') {
|
|
265
|
+
flags = withFlag(flags, 'strict', true);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
263
268
|
|
|
264
269
|
if (name === '--provider') {
|
|
265
270
|
const consumed = getValue(argv, index, inlineValue, name, errors);
|
package/src/cli/status.ts
CHANGED
|
@@ -502,6 +502,19 @@ export function buildCliDoctorFindings(options: CliStatusOptions): readonly CliD
|
|
|
502
502
|
return findings;
|
|
503
503
|
}
|
|
504
504
|
|
|
505
|
+
/**
|
|
506
|
+
* The doctor command's exit code. Advisory findings (`severity: 'warning'`)
|
|
507
|
+
* are notes on an otherwise-usable install and must never make a healthy
|
|
508
|
+
* install report failure — only a must-fix (`severity: 'risk'`) finding
|
|
509
|
+
* exits non-zero. `strict` (for CI) flips warnings to failures too, so a
|
|
510
|
+
* pipeline can require a fully clean report instead of just "usable".
|
|
511
|
+
*/
|
|
512
|
+
export function resolveDoctorExitCode(findings: readonly CliDoctorFinding[], strict = false): number {
|
|
513
|
+
if (findings.some((finding) => finding.severity === 'risk')) return 1;
|
|
514
|
+
if (strict && findings.length > 0) return 1;
|
|
515
|
+
return 0;
|
|
516
|
+
}
|
|
517
|
+
|
|
505
518
|
export function buildCliStatusSnapshot(options: CliStatusOptions): CliStatusSnapshot {
|
|
506
519
|
const config = options.configManager;
|
|
507
520
|
const controlPlaneBinding = resolveRuntimeEndpointBinding(config, 'controlPlane');
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
GOODVIBES_NTFY_REMOTE_TOPIC,
|
|
6
6
|
resolveGoodVibesNtfyTopics,
|
|
7
7
|
} from '@pellux/goodvibes-sdk/platform/integrations';
|
|
8
|
-
import { enableFeatureFlags, getMissingSurfaceFeatureFlags, getServerSurfaceFeatureFlags } from '../runtime/surface-feature-flags.ts';
|
|
8
|
+
import { enableFeatureFlags, getMissingSurfaceFeatureFlags, getServerSurfaceFeatureFlags, surfaceFeatureGateSettingsKeys } from '../runtime/surface-feature-flags.ts';
|
|
9
9
|
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
10
10
|
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
11
11
|
import type { CliCommandRuntime } from './types.ts';
|
|
@@ -147,7 +147,7 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
|
|
|
147
147
|
readinessIssues.push(`${surface.label} is enabled but missing ${surface.missing.join(', ')}.`);
|
|
148
148
|
}
|
|
149
149
|
if (surface.missingFeatureFlags.length > 0) {
|
|
150
|
-
readinessIssues.push(`${surface.label} is enabled but
|
|
150
|
+
readinessIssues.push(`${surface.label} is enabled but these settings are off: ${surfaceFeatureGateSettingsKeys(surface.missingFeatureFlags).join(', ')}.`);
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
const value = {
|
|
@@ -185,7 +185,7 @@ export async function handleSurfacesCommand(runtime: CliCommandRuntime): Promise
|
|
|
185
185
|
` http-listener: ${yesNo(value.httpListener.enabled)} (${value.httpListener.hostMode} ${value.httpListener.host}:${value.httpListener.port})${includeProbe ? ` reachable=${yesNo(value.httpListener.reachable)}` : ''}`,
|
|
186
186
|
'',
|
|
187
187
|
'External surfaces:',
|
|
188
|
-
...value.surfaces.map((surface) => ` ${surface.label.padEnd(16)} enabled=${yesNo(surface.enabled)} ready=${yesNo(surface.ready)}${surface.enabled && surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}${surface.enabled && surface.missingFeatureFlags.length > 0 ? `
|
|
188
|
+
...value.surfaces.map((surface) => ` ${surface.label.padEnd(16)} enabled=${yesNo(surface.enabled)} ready=${yesNo(surface.ready)}${surface.enabled && surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}${surface.enabled && surface.missingFeatureFlags.length > 0 ? ` settingsOff=${surfaceFeatureGateSettingsKeys(surface.missingFeatureFlags).join(',')}` : ''}`),
|
|
189
189
|
...(filteredSurfaces.some((surface) => surface.id === 'ntfy') ? [
|
|
190
190
|
'',
|
|
191
191
|
'ntfy inbound topics:',
|
|
@@ -260,7 +260,7 @@ export async function buildListenerTestResult(runtime: CliCommandRuntime): Promi
|
|
|
260
260
|
if (isNetworkFacing(enabled, binding) && auth.bootstrapCredentialPresent) issues.push('Network-facing listener still has a bootstrap credential file.');
|
|
261
261
|
for (const surface of surfaces) {
|
|
262
262
|
if (surface.missing.length > 0) issues.push(`${surface.label} is enabled but missing ${surface.missing.join(', ')}.`);
|
|
263
|
-
if (surface.missingFeatureFlags.length > 0) issues.push(`${surface.label} is enabled but
|
|
263
|
+
if (surface.missingFeatureFlags.length > 0) issues.push(`${surface.label} is enabled but these settings are off: ${surfaceFeatureGateSettingsKeys(surface.missingFeatureFlags).join(', ')}.`);
|
|
264
264
|
}
|
|
265
265
|
return { enabled, ...binding, posture, reachable, service, auth, surfaces, issues };
|
|
266
266
|
}
|
|
@@ -276,7 +276,7 @@ export function formatListenerTestResult(runtime: CliCommandRuntime, value: List
|
|
|
276
276
|
` local auth users: ${value.auth.userStorePresent ? 'present' : 'missing'}`,
|
|
277
277
|
` bootstrap credential: ${value.auth.bootstrapCredentialPresent ? 'present' : 'missing'}`,
|
|
278
278
|
value.surfaces.length === 0 ? ' enabled webhook surfaces: none' : ' enabled webhook surfaces:',
|
|
279
|
-
...value.surfaces.map((surface) => ` ${surface.label}: ready=${yesNo(surface.ready)}${surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}${surface.missingFeatureFlags.length > 0 ? `
|
|
279
|
+
...value.surfaces.map((surface) => ` ${surface.label}: ready=${yesNo(surface.ready)}${surface.missing.length > 0 ? ` missing=${surface.missing.join(',')}` : ''}${surface.missingFeatureFlags.length > 0 ? ` settingsOff=${surfaceFeatureGateSettingsKeys(surface.missingFeatureFlags).join(',')}` : ''}`),
|
|
280
280
|
value.issues.length === 0 ? ' readiness: ready' : ' readiness: needs attention',
|
|
281
281
|
...value.issues.map((issue) => ` - ${issue}`),
|
|
282
282
|
].join('\n'));
|
package/src/cli/tui-startup.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommandContext, CommandRegistry } from '../input/command-registry.ts';
|
|
2
2
|
import type { InputHandler } from '../input/handler.ts';
|
|
3
3
|
import type { SelectionItem } from '../input/selection-modal.ts';
|
|
4
|
+
import type { WorkspaceRegistrationManager } from '../runtime/trust/workspace-registration.ts';
|
|
4
5
|
import { hasResumableWizardProgress, readOnboardingCheckMarker, readWizardProgress } from '../runtime/onboarding/index.ts';
|
|
5
6
|
import { startOnboardingFastPath } from '../runtime/onboarding/fast-path.ts';
|
|
6
7
|
import { readLastSessionPointer } from '@/runtime/index.ts';
|
|
@@ -13,111 +14,62 @@ export type TuiStartupShellPaths = Parameters<typeof readOnboardingCheckMarker>[
|
|
|
13
14
|
|
|
14
15
|
export type FirstOpenTrustLevel = 'trusted' | 'restricted';
|
|
15
16
|
|
|
16
|
-
/** The decisions a first-open prompt choice resolves to. Absent halves were not asked. */
|
|
17
|
-
export interface FirstOpenDecision {
|
|
18
|
-
readonly trust?: FirstOpenTrustLevel;
|
|
19
|
-
readonly register?: 'yes' | 'no';
|
|
20
|
-
}
|
|
21
|
-
|
|
22
17
|
/**
|
|
23
|
-
* decodeFirstOpenChoice — pure mapping from a chosen selection-item id (or
|
|
24
|
-
* on Escape/enter-through) to the trust
|
|
25
|
-
*
|
|
26
|
-
* are unit-testable
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* settles to 'restricted' (matching the standalone trust prompt), and an offered
|
|
30
|
-
* register half defaults to NO — recorded as a subtree-scoped decline so the
|
|
31
|
-
* directory is not re-asked every startup.
|
|
18
|
+
* decodeFirstOpenChoice — pure mapping from a chosen selection-item id (or
|
|
19
|
+
* null on Escape/enter-through) to the trust decision. Kept pure so the
|
|
20
|
+
* consequence-time trust prompt's semantics (raised by trustGatedAsk, wired
|
|
21
|
+
* in main.ts's trustPromptRef.requestTrustDecision) are unit-testable
|
|
22
|
+
* without a live modal. Escape/enter-through (id === null) is the safe
|
|
23
|
+
* default: 'restricted'.
|
|
32
24
|
*/
|
|
33
|
-
export function decodeFirstOpenChoice(
|
|
34
|
-
id
|
|
35
|
-
offered: { readonly trustNeeded: boolean; readonly registerNeeded: boolean },
|
|
36
|
-
): FirstOpenDecision {
|
|
37
|
-
const decision: { trust?: FirstOpenTrustLevel; register?: 'yes' | 'no' } = {};
|
|
38
|
-
if (offered.trustNeeded && offered.registerNeeded) {
|
|
39
|
-
// Combined 2x2 choice.
|
|
40
|
-
const trust: FirstOpenTrustLevel = id === 'trust-register' || id === 'trust-only' ? 'trusted' : 'restricted';
|
|
41
|
-
const register: 'yes' | 'no' = id === 'trust-register' || id === 'restrict-register' ? 'yes' : 'no';
|
|
42
|
-
decision.trust = trust;
|
|
43
|
-
decision.register = register;
|
|
44
|
-
} else if (offered.trustNeeded) {
|
|
45
|
-
decision.trust = id === 'trusted' ? 'trusted' : 'restricted';
|
|
46
|
-
} else if (offered.registerNeeded) {
|
|
47
|
-
decision.register = id === 'register' ? 'yes' : 'no';
|
|
48
|
-
}
|
|
49
|
-
return decision;
|
|
25
|
+
export function decodeFirstOpenChoice(id: string | null): FirstOpenTrustLevel {
|
|
26
|
+
return id === 'trusted' ? 'trusted' : 'restricted';
|
|
50
27
|
}
|
|
51
28
|
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
items: [
|
|
61
|
-
{ id: 'trust-register', label: 'Trust & register this workspace', detail: 'Full capability, and track it in your workspace registry', primaryAction: 'select' },
|
|
62
|
-
{ id: 'trust-only', label: 'Trust, don\'t register', detail: 'Full capability; leave it out of the registry', primaryAction: 'select' },
|
|
63
|
-
{ id: 'restrict-register', label: 'Keep restricted, register', detail: 'Read-only for now; still track it in the registry', primaryAction: 'select' },
|
|
64
|
-
{ id: 'restrict-only', label: 'Keep restricted, don\'t register', detail: 'Read-only exploration; nothing recorded (default)', primaryAction: 'select' },
|
|
65
|
-
],
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
if (offered.trustNeeded) {
|
|
69
|
-
return {
|
|
70
|
-
title: 'New workspace — choose a trust level',
|
|
71
|
-
items: [
|
|
72
|
-
{ id: 'trusted', label: 'Trust this workspace', detail: 'Full capability — all tools may run', primaryAction: 'select' },
|
|
73
|
-
{ id: 'restricted', label: 'Keep restricted (read-only)', detail: 'Explore safely; writes and commands are denied until trusted', primaryAction: 'select' },
|
|
74
|
-
],
|
|
75
|
-
};
|
|
76
|
-
}
|
|
29
|
+
/**
|
|
30
|
+
* Build the selection rows for the trust prompt. This used to also offer a
|
|
31
|
+
* "register this directory?" half (a combined 2x2 when both were needed) —
|
|
32
|
+
* that half is gone: registration self-records (see
|
|
33
|
+
* selfRecordWorkspaceRegistration below) rather than ever asking a question
|
|
34
|
+
* about a registry the user hasn't met yet.
|
|
35
|
+
*/
|
|
36
|
+
export function buildFirstOpenItems(): { readonly title: string; readonly items: SelectionItem[] } {
|
|
77
37
|
return {
|
|
78
|
-
title: '
|
|
38
|
+
title: 'New workspace — choose a trust level',
|
|
79
39
|
items: [
|
|
80
|
-
{ id: '
|
|
81
|
-
{ id: '
|
|
40
|
+
{ id: 'trusted', label: 'Trust this workspace', detail: 'Full capability — all tools may run', primaryAction: 'select' },
|
|
41
|
+
{ id: 'restricted', label: 'Keep restricted (read-only)', detail: 'Explore safely; writes and commands are denied until trusted', primaryAction: 'select' },
|
|
82
42
|
],
|
|
83
43
|
};
|
|
84
44
|
}
|
|
85
45
|
|
|
86
46
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
47
|
+
* Registration self-records: a workspace the user actually works in becomes
|
|
48
|
+
* its own registry entry with no question ever asked (the former "Register
|
|
49
|
+
* this directory as a workspace?" prompt — sometimes a four-option 2x2 when
|
|
50
|
+
* trust was undecided too — is gone entirely).
|
|
51
|
+
*
|
|
52
|
+
* "Actually works in" is anchored to TRUST, not mere directory-open: this is
|
|
53
|
+
* only ever called for a workspace that is (or has just become) trusted,
|
|
54
|
+
* never for one that's merely been glanced at read-only. Labeled 'via TUI'
|
|
55
|
+
* in the shared registry so its provenance is honest.
|
|
91
56
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
57
|
+
* OWNER-BOUNDARY RIDER (binding): self-recording must not widen the
|
|
58
|
+
* checkpoint-owning consumer's boundary. That boundary now reads the SAME
|
|
59
|
+
* shared registration store this writes to, gated by each record's
|
|
60
|
+
* `checkpointEligible` flag (absent means false). So the separation is no
|
|
61
|
+
* longer "a separate list the agent owns" — it is one store, and this self
|
|
62
|
+
* record stays out of scope by stamping its `origin` ('tui') for honest
|
|
63
|
+
* provenance while NEVER setting `checkpointEligible`. Only the consumer that
|
|
64
|
+
* owns checkpointing re-stamps its own roots eligible on boot.
|
|
96
65
|
*/
|
|
97
|
-
async function
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const evaluation =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const offered = { trustNeeded, registerNeeded };
|
|
106
|
-
const { title, items } = buildFirstOpenItems(offered);
|
|
107
|
-
commandContext.openSelection?.(
|
|
108
|
-
title,
|
|
109
|
-
items,
|
|
110
|
-
{ allowSearch: false, primaryVerbLabel: 'Choose' },
|
|
111
|
-
(result) => {
|
|
112
|
-
const decision = decodeFirstOpenChoice(result?.item.id ?? null, offered);
|
|
113
|
-
const apply = async () => {
|
|
114
|
-
if (decision.trust && trustManager) await trustManager.setLevel(decision.trust);
|
|
115
|
-
if (decision.register === 'yes' && registrationManager) await registrationManager.register();
|
|
116
|
-
else if (decision.register === 'no' && registrationManager) await registrationManager.decline();
|
|
117
|
-
};
|
|
118
|
-
void apply().finally(render);
|
|
119
|
-
},
|
|
120
|
-
);
|
|
66
|
+
export async function selfRecordWorkspaceRegistration(
|
|
67
|
+
registrationManager: Pick<WorkspaceRegistrationManager, 'evaluate' | 'register'> | undefined,
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
if (!registrationManager) return;
|
|
70
|
+
const evaluation = await registrationManager.evaluate();
|
|
71
|
+
if (!evaluation.offerRegister) return;
|
|
72
|
+
await registrationManager.register('via TUI', 'tui');
|
|
121
73
|
}
|
|
122
74
|
|
|
123
75
|
export function applyInitialTuiCliState(options: {
|
|
@@ -131,6 +83,16 @@ export function applyInitialTuiCliState(options: {
|
|
|
131
83
|
const { cli, input, commandRegistry, commandContext, shellPaths, render } = options;
|
|
132
84
|
const globalOnboardingMarker = readOnboardingCheckMarker(shellPaths, 'user');
|
|
133
85
|
|
|
86
|
+
// Registration self-records on every launch of an already-trusted
|
|
87
|
+
// workspace (fire-and-forget — never blocks or gates startup, never a
|
|
88
|
+
// modal). A workspace that is undecided or restricted is never
|
|
89
|
+
// self-registered here; for one that becomes trusted just now (via the
|
|
90
|
+
// consequence-time trust prompt), main.ts's trustPromptRef wiring calls
|
|
91
|
+
// this same helper right after the decision.
|
|
92
|
+
if (commandContext.workspace?.workspaceTrustManager?.isTrusted()) {
|
|
93
|
+
void selfRecordWorkspaceRegistration(commandContext.workspace.workspaceRegistrationManager);
|
|
94
|
+
}
|
|
95
|
+
|
|
134
96
|
// Seeded prompt is always applied synchronously, regardless of session branch.
|
|
135
97
|
const seededPrompt = cli.flags.prompt ?? (cli.rawCommand === undefined && cli.positionals.length > 0 ? cli.positionals.join(' ') : undefined);
|
|
136
98
|
if (seededPrompt) {
|
|
@@ -183,6 +145,9 @@ export function applyInitialTuiCliState(options: {
|
|
|
183
145
|
} else if (!globalOnboardingMarker.exists) {
|
|
184
146
|
// Fast path: get a brand-new user to a working session in the fewest steps.
|
|
185
147
|
// Falls back to the full wizard when the surface can't detect providers.
|
|
148
|
+
// Trust stays undecided here on purpose — the first non-read tool
|
|
149
|
+
// request is what raises the trust question now (see main.ts's
|
|
150
|
+
// trustPromptRef), not this startup branch.
|
|
186
151
|
startOnboardingFastPath({ input, commandContext, shellPaths, render });
|
|
187
152
|
} else {
|
|
188
153
|
// User has completed onboarding before but left a wizard session in progress.
|
|
@@ -202,11 +167,9 @@ export function applyInitialTuiCliState(options: {
|
|
|
202
167
|
},
|
|
203
168
|
});
|
|
204
169
|
}
|
|
205
|
-
} else {
|
|
206
|
-
// Returning user, no wizard to resume: this is the moment to ask, in one
|
|
207
|
-
// surface, about a genuinely new workspace's trust level and whether to
|
|
208
|
-
// register it — before they run anything here.
|
|
209
|
-
void promptWorkspaceFirstOpen(commandContext, render);
|
|
210
170
|
}
|
|
171
|
+
// Returning user, no wizard to resume, nothing else to do at startup:
|
|
172
|
+
// trust (if still undecided) is asked at the first non-read tool
|
|
173
|
+
// request, not here — see main.ts's trustPromptRef wiring.
|
|
211
174
|
}
|
|
212
175
|
}
|
package/src/cli/types.ts
CHANGED
|
@@ -60,6 +60,8 @@ export interface GoodVibesCliFlags {
|
|
|
60
60
|
readonly fork: string | true | undefined;
|
|
61
61
|
readonly yes: boolean;
|
|
62
62
|
readonly nonInteractive: boolean;
|
|
63
|
+
/** `doctor --strict`: advisory findings also fail (exit 1), for CI. Ignored by every other command. */
|
|
64
|
+
readonly strict: boolean;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
export interface GoodVibesCliParseResult {
|
package/src/config/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
export { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
10
10
|
export type { DeepReadonly } from '@pellux/goodvibes-sdk/platform/config';
|
|
11
|
-
export type { GoodVibesConfig, ConfigKey, ConfigValue, ConfigSetting, PermissionMode, PermissionAction, PermissionsToolConfig, NotificationsConfig
|
|
11
|
+
export type { GoodVibesConfig, ConfigKey, ConfigValue, ConfigSetting, PermissionMode, PermissionAction, PermissionsToolConfig, NotificationsConfig } from '@pellux/goodvibes-sdk/platform/config';
|
|
12
12
|
export { DEFAULT_CONFIG, CONFIG_SCHEMA } from '@pellux/goodvibes-sdk/platform/config';
|
|
13
13
|
export { ConfigError } from '@pellux/goodvibes-sdk/platform/types';
|
|
14
14
|
|
|
@@ -99,6 +99,39 @@ export function readStatuslineSettings(configManager: Pick<ConfigManager, 'getRa
|
|
|
99
99
|
return out;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// ─── update.* — launch-time self-update behavior ─────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* User-settable launch-time self-update behavior. The feature itself (check at
|
|
106
|
+
* launch, install via the checksum-verified download/verify/swap path, restart
|
|
107
|
+
* onto the new binary) defaults ON for binary installs; the defaults live in
|
|
108
|
+
* the consumer (src/cli/launch-auto-update.ts), matching this file's rule that
|
|
109
|
+
* readers return only what the user actually set.
|
|
110
|
+
*/
|
|
111
|
+
export interface UpdateSettings {
|
|
112
|
+
/** Check for a newer release at TUI launch and install it before starting. Default: true. */
|
|
113
|
+
readonly autoUpdateAtLaunch?: boolean;
|
|
114
|
+
/** How long the launch-time version check may take before it is skipped. Defaults to 2500; clamped to [250, 30000]. */
|
|
115
|
+
readonly launchCheckTimeoutMs?: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const LAUNCH_CHECK_MIN_TIMEOUT_MS = 250;
|
|
119
|
+
const LAUNCH_CHECK_MAX_TIMEOUT_MS = 30_000;
|
|
120
|
+
|
|
121
|
+
/** Read `update.*` from settings.json, validating and clamping the timeout. */
|
|
122
|
+
export function readUpdateSettings(configManager: Pick<ConfigManager, 'getRaw'>): UpdateSettings {
|
|
123
|
+
const src = readNamespace(configManager, 'update');
|
|
124
|
+
if (!src) return {};
|
|
125
|
+
const out: { autoUpdateAtLaunch?: boolean; launchCheckTimeoutMs?: number } = {};
|
|
126
|
+
const autoUpdateAtLaunch = readBoolean(src, 'autoUpdateAtLaunch');
|
|
127
|
+
if (autoUpdateAtLaunch !== undefined) out.autoUpdateAtLaunch = autoUpdateAtLaunch;
|
|
128
|
+
const launchCheckTimeoutMs = readPositiveInt(src, 'launchCheckTimeoutMs');
|
|
129
|
+
if (launchCheckTimeoutMs !== undefined) {
|
|
130
|
+
out.launchCheckTimeoutMs = Math.min(LAUNCH_CHECK_MAX_TIMEOUT_MS, Math.max(LAUNCH_CHECK_MIN_TIMEOUT_MS, launchCheckTimeoutMs));
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
102
135
|
// ─── checkpoints.* — workspace checkpoint root-guard options ─────────────────
|
|
103
136
|
|
|
104
137
|
/**
|
|
@@ -24,7 +24,7 @@ export function deriveComposerState(input: ComposerStateInput): ComposerState {
|
|
|
24
24
|
// status tag: `risk:approval-wait`. It is therefore NOT also duplicated as a
|
|
25
25
|
// `approval` flag, and (below) it suppresses the competing `state:` turn tag —
|
|
26
26
|
// the turn is blocked on the user, not really streaming. Precedence:
|
|
27
|
-
// approval-wait > live turn state. (
|
|
27
|
+
// approval-wait > live turn state. (item 4.)
|
|
28
28
|
const approvalWait = input.pendingApproval === true;
|
|
29
29
|
if (approvalWait) {
|
|
30
30
|
pendingRisk = 'approval-wait';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// consolidation-receipt.ts — the one-line notice for an idle-time memory
|
|
3
|
+
// consolidation run.
|
|
4
|
+
//
|
|
5
|
+
// The SDK's MemoryConsolidationScheduler runs at idle and produces a
|
|
6
|
+
// MemoryConsolidationRunReceipt (merged / archived / decayed / proposed). This
|
|
7
|
+
// formats it into the same one-line shape every other attach-time receipt
|
|
8
|
+
// uses, so a run that changed something surfaces on the next attach like a
|
|
9
|
+
// crash/update/migration receipt. A pure no-op run yields null — no noise.
|
|
10
|
+
//
|
|
11
|
+
// A run that PROPOSED something (a contradiction or cross-scope-duplicate
|
|
12
|
+
// awaiting human judgment — never auto-applied) names where to actually look:
|
|
13
|
+
// the Memory modal's Proposals tab (`/memory`, then the Proposals tab —
|
|
14
|
+
// memory-modal.ts's proposalsTab()), which lists each proposal's kind,
|
|
15
|
+
// reason, and affected record ids and jumps to them in the Review Queue.
|
|
16
|
+
// Without this pointer the receipt line was the only trace a proposal ever
|
|
17
|
+
// existed — a count with nowhere to go.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
import type { MemoryConsolidationRunReceipt } from '@pellux/goodvibes-sdk/platform/state';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A one-line receipt for a consolidation run that changed something, or null
|
|
24
|
+
* when the run merged/archived/decayed/proposed nothing (a quiet idle pass —
|
|
25
|
+
* no notice, matching the check-in "stayed quiet" discipline).
|
|
26
|
+
*/
|
|
27
|
+
export function formatConsolidationReceipt(receipt: MemoryConsolidationRunReceipt): string | null {
|
|
28
|
+
const merged = receipt.merged.length;
|
|
29
|
+
const archived = receipt.archived.length;
|
|
30
|
+
const decayed = receipt.decayed.length;
|
|
31
|
+
const proposed = receipt.proposed.length;
|
|
32
|
+
if (merged + archived + decayed + proposed === 0) return null;
|
|
33
|
+
const parts: string[] = [];
|
|
34
|
+
if (merged) parts.push(`${merged} merged`);
|
|
35
|
+
if (archived) parts.push(`${archived} archived`);
|
|
36
|
+
if (decayed) parts.push(`${decayed} decayed`);
|
|
37
|
+
if (proposed) parts.push(`${proposed} proposed`);
|
|
38
|
+
const base = `Memory consolidation: ${parts.join(', ')} (scanned ${receipt.scanned}).`;
|
|
39
|
+
if (proposed === 0) return base;
|
|
40
|
+
return `${base} Review the ${proposed} proposed change${proposed === 1 ? '' : 's'} with /memory (Proposals tab).`;
|
|
41
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* conversation-line-cache.ts — per-message Line[] production cache
|
|
2
|
+
* conversation-line-cache.ts — per-message Line[] production cache.
|
|
3
3
|
*
|
|
4
4
|
* The measured defect (perf baseline 2026-07-03, transcript.build_1k): appending
|
|
5
5
|
* ONE message to an N-message conversation re-rendered all N messages
|
|
@@ -98,7 +98,7 @@ interface KeyMeta {
|
|
|
98
98
|
/**
|
|
99
99
|
* True when this (assistant) message has a tool call with no result yet — it
|
|
100
100
|
* renders as pending rather than done. Keyed so the entry invalidates and
|
|
101
|
-
* re-renders ✓ once the tool result arrives. (
|
|
101
|
+
* re-renders ✓ once the tool result arrives. (item 2c.)
|
|
102
102
|
*/
|
|
103
103
|
readonly hasPendingTool: boolean;
|
|
104
104
|
}
|
|
@@ -286,7 +286,7 @@ export class MessageLineCache {
|
|
|
286
286
|
};
|
|
287
287
|
|
|
288
288
|
// Tool calls with no matching tool-result message are still pending; the
|
|
289
|
-
// render context and the cache key both depend on this. (
|
|
289
|
+
// render context and the cache key both depend on this. (item 2c.)
|
|
290
290
|
const completedToolCallIds = collectCompletedToolCallIds(messages);
|
|
291
291
|
const renderContext: ConversationRenderContext = { ...context, completedToolCallIds };
|
|
292
292
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { UIFactory } from '../renderer/ui-factory.ts';
|
|
2
2
|
import { renderMarkdownTracked } from '../renderer/markdown.ts';
|
|
3
3
|
import { renderDiffView } from '../renderer/diff-view.ts';
|
|
4
|
-
import { activeTheme } from '../renderer/theme.ts';
|
|
4
|
+
import { activeTheme, activeUiTones } from '../renderer/theme.ts';
|
|
5
5
|
import { renderToolCallBlock } from '../renderer/tool-call.ts';
|
|
6
6
|
import { summarizeToolResult } from '../renderer/tool-result-summary.ts';
|
|
7
7
|
import type { ToolCall } from '@pellux/goodvibes-sdk/platform/types';
|
|
@@ -46,7 +46,7 @@ type Message = ConversationMessageSnapshot;
|
|
|
46
46
|
* Collect the ids of tool calls that have a matching tool-result message in the
|
|
47
47
|
* given slice — i.e. the tools that actually ran. Used to decide whether an
|
|
48
48
|
* assistant tool call still shows as pending (awaiting approval) or done.
|
|
49
|
-
* (
|
|
49
|
+
* (item 2c.)
|
|
50
50
|
*/
|
|
51
51
|
export function collectCompletedToolCallIds(messages: readonly Message[]): Set<string> {
|
|
52
52
|
const ids = new Set<string>();
|
|
@@ -79,7 +79,7 @@ export interface ConversationRenderContext {
|
|
|
79
79
|
* awaiting a decision (e.g. an approval prompt) and renders with a pending
|
|
80
80
|
* glyph instead of the completed ✓. When undefined (single-message callers
|
|
81
81
|
* without sibling context), every tool call renders as done — the prior
|
|
82
|
-
* behaviour. (
|
|
82
|
+
* behaviour. (item 2c.)
|
|
83
83
|
*/
|
|
84
84
|
readonly completedToolCallIds?: ReadonlySet<string>;
|
|
85
85
|
}
|
|
@@ -273,7 +273,7 @@ export function renderConversationToolMessage(
|
|
|
273
273
|
// Human one-line summary for tool results (write/read/exec/edit): shown as the
|
|
274
274
|
// collapsed line so the transcript reads "wrote foo.txt (532 B)" instead of a
|
|
275
275
|
// raw JSON blob; the full payload stays behind the expand toggle. Only for
|
|
276
|
-
// 'tool' blocks (diffs render their own view). (
|
|
276
|
+
// 'tool' blocks (diffs render their own view). (item 3.)
|
|
277
277
|
const resultSummary = blockType === 'tool'
|
|
278
278
|
? summarizeToolResult(message.toolName, message.content)
|
|
279
279
|
: null;
|
|
@@ -291,11 +291,20 @@ export function renderConversationToolMessage(
|
|
|
291
291
|
context.collapseState.set(collapseKey, isShort ? false : true);
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
// A per-call user cancellation settles as a tool result whose content leads
|
|
295
|
+
// with "Error: cancelled by user" (the SDK's structured cancelled shape; any
|
|
296
|
+
// partial output the tool produced before it stopped follows on later lines).
|
|
297
|
+
// Render it structurally as a distinct "cancelled" block in the warn tone —
|
|
298
|
+
// not a generic tool result, and not a hard error — so the transcript reads
|
|
299
|
+
// the user's decision honestly while the partial output stays visible below.
|
|
300
|
+
const isCancelled = blockType === 'tool' && /^Error: cancelled by user\b/.test(contentLines[0] ?? '');
|
|
301
|
+
const warnTone = activeUiTones().chrome.warn;
|
|
302
|
+
|
|
294
303
|
context.history.addLine(renderConversationEventLine(width, {
|
|
295
|
-
marker: blockType === 'diff' ? GLYPHS.status.dualPane : GLYPHS.status.active,
|
|
296
|
-
markerFg: blockType === 'diff' ? T.diffAccent : T.toolAccent,
|
|
297
|
-
label: blockType === 'diff' ? 'diff' : 'tool result',
|
|
298
|
-
labelFg: blockType === 'diff' ? T.diffAccent : T.toolAccent,
|
|
304
|
+
marker: isCancelled ? GLYPHS.status.blocked : (blockType === 'diff' ? GLYPHS.status.dualPane : GLYPHS.status.active),
|
|
305
|
+
markerFg: isCancelled ? warnTone : (blockType === 'diff' ? T.diffAccent : T.toolAccent),
|
|
306
|
+
label: isCancelled ? 'cancelled' : (blockType === 'diff' ? 'diff' : 'tool result'),
|
|
307
|
+
labelFg: isCancelled ? warnTone : (blockType === 'diff' ? T.diffAccent : T.toolAccent),
|
|
299
308
|
detailFg: '244',
|
|
300
309
|
}, [
|
|
301
310
|
...(message.toolName
|
|
@@ -379,7 +388,7 @@ export function appendConversationMessages(
|
|
|
379
388
|
const lineNumberMode = context.configManager?.get('display.lineNumbers') ?? 'off';
|
|
380
389
|
const collapseThreshold = context.configManager?.get('display.collapseThreshold') ?? 30;
|
|
381
390
|
// Derive pending vs done for tool calls from sibling tool-result messages,
|
|
382
|
-
// unless the caller already supplied the set. (
|
|
391
|
+
// unless the caller already supplied the set. (item 2c.)
|
|
383
392
|
const renderContext: ConversationRenderContext = context.completedToolCallIds !== undefined
|
|
384
393
|
? context
|
|
385
394
|
: { ...context, completedToolCallIds: collectCompletedToolCallIds(messages) };
|
package/src/core/conversation.ts
CHANGED
|
@@ -67,7 +67,7 @@ export interface OrchestratorUsageTotals {
|
|
|
67
67
|
* TUI-side counterpart to SDK Orchestrator.usage: after a session resume
|
|
68
68
|
* replays historical messages into a freshly-constructed Orchestrator (whose
|
|
69
69
|
* `usage` starts at all zeros — bootstrap.ts always constructs a fresh
|
|
70
|
-
* instance, see
|
|
70
|
+
* instance, see), this lets the caller hydrate the footer's token
|
|
71
71
|
* counters from messages that already carry real usage data instead of
|
|
72
72
|
* waiting for the first new turn to populate them.
|
|
73
73
|
*/
|
|
@@ -98,7 +98,7 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
98
98
|
/** When true the buffer needs to be rebuilt before the next display. */
|
|
99
99
|
private dirty = true;
|
|
100
100
|
/**
|
|
101
|
-
* Per-message Line[] cache
|
|
101
|
+
* Per-message Line[] cache. rebuildHistory() reuses cached lines for
|
|
102
102
|
* messages whose complete render inputs are unchanged, so appending one message
|
|
103
103
|
* to an N-message conversation no longer re-renders all N. A cache-served
|
|
104
104
|
* rebuild is byte-identical to a cold one (pure memoisation).
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
* When neither target is available the function is an honest no-op (debug log
|
|
19
19
|
* only; no user-facing error spam).
|
|
20
20
|
*
|
|
21
|
-
* Focus tracking
|
|
21
|
+
* Focus tracking: when `focusTracker` is supplied, notifications are
|
|
22
22
|
* gated the same way as the other unfocused-user alert classes (see
|
|
23
23
|
* alert-gating.ts) — they fire when the terminal is unfocused or focus state
|
|
24
24
|
* was never observed, and are suppressed when it's known to be focused,
|
|
25
25
|
* unless `behavior.notifyOnlyWhenUnfocused` is turned off. `focusTracker` is
|
|
26
|
-
* optional and defaults to "always fire" (the
|
|
26
|
+
* optional and defaults to "always fire" (the behavior before focus gating existed) when
|
|
27
27
|
* omitted, so existing callers that don't have a FocusTracker in scope are
|
|
28
28
|
* unaffected.
|
|
29
29
|
*/
|
|
@@ -82,11 +82,11 @@ export interface MaybeNotifyLongTaskOptions {
|
|
|
82
82
|
readonly webhookNotifier?: WebhookNotifier | null;
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
|
-
* Terminal focus tracker
|
|
85
|
+
* Terminal focus tracker. When supplied together with `configGet`,
|
|
86
86
|
* the notification additionally respects behavior.notifyOnlyWhenUnfocused
|
|
87
87
|
* (default on): suppressed when the terminal is known to be focused, fires
|
|
88
88
|
* when unfocused or when focus was never observed. Omit both to preserve
|
|
89
|
-
*
|
|
89
|
+
* the behavior before focus gating existed (always fire once the threshold is met).
|
|
90
90
|
*/
|
|
91
91
|
readonly focusTracker?: Pick<FocusTracker, 'shouldAlertWhenUnfocused'> | null;
|
|
92
92
|
|
|
@@ -121,9 +121,9 @@ export function maybeNotifyLongTask(opts: MaybeNotifyLongTaskOptions): boolean {
|
|
|
121
121
|
return false;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
// Focus gate
|
|
125
|
-
// are supplied. Absent either one, behavior is unchanged from
|
|
126
|
-
// (always fire once the threshold is met).
|
|
124
|
+
// Focus gate: only applied when both a tracker and a config reader
|
|
125
|
+
// are supplied. Absent either one, behavior is unchanged from before focus
|
|
126
|
+
// gating existed (always fire once the threshold is met).
|
|
127
127
|
if (focusTracker && configGet && readNotifyOnlyWhenUnfocused(configGet) && !focusTracker.shouldAlertWhenUnfocused()) {
|
|
128
128
|
logger.debug('long-task-notifier: suppressed — terminal focused');
|
|
129
129
|
return false;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// memory-provenance.ts — the optional "used N memories" turn chip (owner-ruled
|
|
3
|
+
// 2026-07-13, default OFF).
|
|
4
|
+
//
|
|
5
|
+
// The SDK tags each turn payload with the memory-sourced injection ids it drew
|
|
6
|
+
// on: `metadata.memory.recordIds: string[]` (absent when none) — the SAME
|
|
7
|
+
// convention the webui's chip reads, so both surfaces name the same records.
|
|
8
|
+
// When the memory-provenance setting is ON, a turn that used memories shows a
|
|
9
|
+
// small chip naming how many, with a drill-in listing them. When OFF (default),
|
|
10
|
+
// NOTHING is rendered and the metadata is never even read.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
14
|
+
|
|
15
|
+
/** TUI-local setting key: show the per-turn memory-provenance chip. Default OFF. */
|
|
16
|
+
export const MEMORY_SHOW_PROVENANCE_CONFIG_KEY = 'memory.showProvenance';
|
|
17
|
+
export const MEMORY_SHOW_PROVENANCE_DEFAULT = false;
|
|
18
|
+
export const MEMORY_SHOW_PROVENANCE_DESCRIPTION =
|
|
19
|
+
'Show a "used N memories" chip on turns that drew on your standing memories, with a drill-in ' +
|
|
20
|
+
'listing them (Alt+M to expand). Off by default; when off, nothing is rendered and no metadata is read.';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read the memory-provenance toggle defensively. `memory.*` is a TUI-local
|
|
24
|
+
* section with no SDK DEFAULT_CONFIG entry, so ConfigManager.get() throws for
|
|
25
|
+
* it until something has been written — an unset key means the default (OFF),
|
|
26
|
+
* not an error (same pattern as memory.projection.dir).
|
|
27
|
+
*/
|
|
28
|
+
export function readMemoryShowProvenance(configManager: Pick<ConfigManager, 'get'>): boolean {
|
|
29
|
+
try {
|
|
30
|
+
return configManager.get(MEMORY_SHOW_PROVENANCE_CONFIG_KEY as Parameters<ConfigManager['get']>[0]) === true;
|
|
31
|
+
} catch {
|
|
32
|
+
return MEMORY_SHOW_PROVENANCE_DEFAULT;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract the memory-sourced record ids a turn drew on, from the SDK's
|
|
38
|
+
* `metadata.memory.recordIds` convention. Structural (not a named import) so it
|
|
39
|
+
* reads the field whether or not the pinned SDK's TurnEvent type surfaces it
|
|
40
|
+
* yet, and yields an empty list for any turn without the field — never a guess,
|
|
41
|
+
* never a throw.
|
|
42
|
+
*/
|
|
43
|
+
export function memoryRecordIdsFromTurn(turn: unknown): readonly string[] {
|
|
44
|
+
const metadata = (turn as { metadata?: unknown } | null | undefined)?.metadata;
|
|
45
|
+
const memory = (metadata as { memory?: unknown } | null | undefined)?.memory;
|
|
46
|
+
const recordIds = (memory as { recordIds?: unknown } | null | undefined)?.recordIds;
|
|
47
|
+
if (!Array.isArray(recordIds)) return [];
|
|
48
|
+
return recordIds.filter((id): id is string => typeof id === 'string');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build the settings-modal synthetic entry for memory.showProvenance (TUI-local,
|
|
53
|
+
* default OFF). Read defensively — memory.* has no SDK DEFAULT_CONFIG entry, so
|
|
54
|
+
* an unset key means the OFF default, not an error.
|
|
55
|
+
*/
|
|
56
|
+
export function buildMemoryProvenanceSyntheticEntry(configManager: Pick<ConfigManager, 'get'>): {
|
|
57
|
+
setting: { key: string; type: 'boolean'; default: boolean; description: string };
|
|
58
|
+
currentValue: boolean;
|
|
59
|
+
isDefault: boolean;
|
|
60
|
+
} {
|
|
61
|
+
const currentValue = readMemoryShowProvenance(configManager);
|
|
62
|
+
return {
|
|
63
|
+
setting: { key: MEMORY_SHOW_PROVENANCE_CONFIG_KEY, type: 'boolean', default: MEMORY_SHOW_PROVENANCE_DEFAULT, description: MEMORY_SHOW_PROVENANCE_DESCRIPTION },
|
|
64
|
+
currentValue,
|
|
65
|
+
isDefault: currentValue === MEMORY_SHOW_PROVENANCE_DEFAULT,
|
|
66
|
+
};
|
|
67
|
+
}
|