@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
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// fleet-spawn.ts
|
|
3
|
+
//
|
|
4
|
+
// The Fleet panel's ACP spawn affordance: 'n' on the fleet surface lists the
|
|
5
|
+
// third-party coding agents the daemon discovered (acp.agents.list — read-only,
|
|
6
|
+
// quiet when none), you pick one, then pick a working directory from the known
|
|
7
|
+
// candidates (the registered workspaces + the current dir — no free-text path
|
|
8
|
+
// retyping where a known dir exists), and acp.sessions.create hosts it as a
|
|
9
|
+
// long-lived daemon session. The new row shows up on the next fleet snapshot as
|
|
10
|
+
// kind 'acp-agent' and steers/stops/flags-for-attention like any other row.
|
|
11
|
+
//
|
|
12
|
+
// A structured spawn failure ({binary, stage, message}) is surfaced verbatim —
|
|
13
|
+
// never a hung row: the daemon bounds the handshake and returns a 'failed'
|
|
14
|
+
// record, and this controller reports its three honest fields.
|
|
15
|
+
//
|
|
16
|
+
// The controller owns the fleet view + input while a pick is in flight (mirrors
|
|
17
|
+
// FleetActs' pick mode), so fleet-panel.ts stays a thin delegator under the
|
|
18
|
+
// architecture line cap. The gateway is injectable so the flow round-trips
|
|
19
|
+
// against a mocked daemon in tests; the live builder is createAcpSpawnGateway.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
23
|
+
import type { OperatorMethodOutput } from '@pellux/goodvibes-sdk';
|
|
24
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
25
|
+
import type { Line } from '../types/grid.ts';
|
|
26
|
+
import { resolveOperatorRpc } from '../input/commands/operator-rpc.ts';
|
|
27
|
+
import {
|
|
28
|
+
buildKeyboardHints,
|
|
29
|
+
buildPanelWorkspace,
|
|
30
|
+
buildPanelLine,
|
|
31
|
+
DEFAULT_PANEL_PALETTE,
|
|
32
|
+
type PanelPalette,
|
|
33
|
+
} from './polish.ts';
|
|
34
|
+
|
|
35
|
+
/** One discovered third-party ACP agent (acp.agents.list). */
|
|
36
|
+
export type AcpDiscoveredAgent = OperatorMethodOutput<'acp.agents.list'>['agents'][number];
|
|
37
|
+
/** acp.sessions.create output — the hosted record + whether it started. */
|
|
38
|
+
export type AcpSpawnResult = OperatorMethodOutput<'acp.sessions.create'>;
|
|
39
|
+
/** One registered workspace root (workspaces.registrations.list). */
|
|
40
|
+
export type AcpWorkspaceRegistration = OperatorMethodOutput<'workspaces.registrations.list'>['workspaces'][number];
|
|
41
|
+
|
|
42
|
+
/** A working-directory candidate offered in the picker: a known dir, never retyped. */
|
|
43
|
+
export interface AcpDirCandidate {
|
|
44
|
+
readonly path: string;
|
|
45
|
+
/** A short human label (a workspace label, or "current directory"). */
|
|
46
|
+
readonly label: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The async verb surface the spawn flow drives — real daemon round-trips in
|
|
51
|
+
* production (createAcpSpawnGateway), a mocked shape in tests.
|
|
52
|
+
*/
|
|
53
|
+
export interface AcpSpawnGateway {
|
|
54
|
+
/** Discovered third-party agents (read-only; empty is a quiet, honest absence). */
|
|
55
|
+
listAgents(): Promise<readonly AcpDiscoveredAgent[]>;
|
|
56
|
+
/** The registered workspace roots — the known-dir candidates for the picker. */
|
|
57
|
+
listWorkspaces(): Promise<readonly AcpWorkspaceRegistration[]>;
|
|
58
|
+
/** Host a discovered agent in a directory as a long-lived daemon session. */
|
|
59
|
+
createSession(input: { readonly agentId: string; readonly cwd: string }): Promise<AcpSpawnResult>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type AcpSpawnGatewayResolution =
|
|
63
|
+
| { readonly available: true; readonly gateway: AcpSpawnGateway }
|
|
64
|
+
| { readonly available: false; readonly reason: string };
|
|
65
|
+
|
|
66
|
+
export interface AcpSpawnGatewayDeps {
|
|
67
|
+
readonly configManager: ConfigManager;
|
|
68
|
+
readonly homeDirectory: string | (() => string);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Build the live ACP spawn gateway over the generic operator invoke path — the
|
|
73
|
+
* same daemon resolution the fleet acts use. Honest unavailable reason when no
|
|
74
|
+
* daemon is reachable, so the spawn key refuses cleanly instead of throwing.
|
|
75
|
+
*/
|
|
76
|
+
export function createAcpSpawnGateway(deps: AcpSpawnGatewayDeps): AcpSpawnGatewayResolution {
|
|
77
|
+
const rpc = resolveOperatorRpc(deps);
|
|
78
|
+
if (!rpc.available) return { available: false, reason: rpc.reason };
|
|
79
|
+
const { sdk } = rpc;
|
|
80
|
+
const gateway: AcpSpawnGateway = {
|
|
81
|
+
listAgents: async () => (await sdk.operator.invoke('acp.agents.list', {})).agents,
|
|
82
|
+
listWorkspaces: async () => (await sdk.operator.invoke('workspaces.registrations.list', {})).workspaces,
|
|
83
|
+
createSession: (input) => sdk.operator.invoke('acp.sessions.create', input),
|
|
84
|
+
};
|
|
85
|
+
return { available: true, gateway };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface FleetSpawnDeps {
|
|
89
|
+
/** Resolve a live gateway per action (a daemon that comes up mid-session is seen). */
|
|
90
|
+
readonly resolveGateway: () => AcpSpawnGatewayResolution;
|
|
91
|
+
/** The current working directory — always a candidate dir (labeled "current directory"). */
|
|
92
|
+
readonly currentDirectory: () => string;
|
|
93
|
+
/** Surface a result/receipt/error line to the operator (system message). */
|
|
94
|
+
readonly notify: (message: string) => void;
|
|
95
|
+
/** Repaint request when the controller's own mode state changes. */
|
|
96
|
+
readonly markDirty: () => void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Pick a third-party agent to host. */
|
|
100
|
+
interface AgentStep {
|
|
101
|
+
readonly step: 'agent';
|
|
102
|
+
readonly agents: readonly AcpDiscoveredAgent[];
|
|
103
|
+
index: number;
|
|
104
|
+
}
|
|
105
|
+
/** Pick the working directory the hosted agent runs in. */
|
|
106
|
+
interface DirStep {
|
|
107
|
+
readonly step: 'dir';
|
|
108
|
+
readonly agent: AcpDiscoveredAgent;
|
|
109
|
+
readonly candidates: readonly AcpDirCandidate[];
|
|
110
|
+
index: number;
|
|
111
|
+
}
|
|
112
|
+
type SpawnMode = AgentStep | DirStep | null;
|
|
113
|
+
|
|
114
|
+
const C = DEFAULT_PANEL_PALETTE;
|
|
115
|
+
|
|
116
|
+
export class FleetSpawn {
|
|
117
|
+
private mode: SpawnMode = null;
|
|
118
|
+
/** True while a create round-trip is in flight (absorb keys, show a spinner line). */
|
|
119
|
+
private creating = false;
|
|
120
|
+
|
|
121
|
+
public constructor(private readonly deps: FleetSpawnDeps) {}
|
|
122
|
+
|
|
123
|
+
/** True while the spawn picker owns the fleet view + input. */
|
|
124
|
+
public spawnModeActive(): boolean {
|
|
125
|
+
return this.mode !== null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Begin the spawn flow: list discovered agents. Quiet, honest absence when the
|
|
130
|
+
* daemon reports none (no mode entered) rather than an empty picker.
|
|
131
|
+
*/
|
|
132
|
+
public async begin(): Promise<void> {
|
|
133
|
+
if (this.mode !== null || this.creating) return;
|
|
134
|
+
const gateway = this.requireGateway();
|
|
135
|
+
if (!gateway) return;
|
|
136
|
+
let agents: readonly AcpDiscoveredAgent[];
|
|
137
|
+
try {
|
|
138
|
+
agents = await gateway.listAgents();
|
|
139
|
+
} catch (err) {
|
|
140
|
+
this.deps.notify(`Could not list ACP agents: ${summarizeError(err)}`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (agents.length === 0) {
|
|
144
|
+
this.deps.notify('No third-party ACP agents discovered (Claude Code, Codex, opencode). Install one on PATH to host it here.');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.mode = { step: 'agent', agents, index: 0 };
|
|
148
|
+
this.deps.markDirty();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Input while the spawn picker is active. Absorbs every key it owns. */
|
|
152
|
+
public handleSpawnInput(key: string): boolean {
|
|
153
|
+
if (!this.mode || this.creating) return true;
|
|
154
|
+
if (key === 'escape' || key === 'esc') { this.mode = null; this.deps.markDirty(); return true; }
|
|
155
|
+
const list = this.mode.step === 'agent' ? this.mode.agents : this.mode.candidates;
|
|
156
|
+
if (key === 'up' || key === 'k') { this.mode.index = (this.mode.index - 1 + list.length) % list.length; this.deps.markDirty(); return true; }
|
|
157
|
+
if (key === 'down' || key === 'j') { this.mode.index = (this.mode.index + 1) % list.length; this.deps.markDirty(); return true; }
|
|
158
|
+
if (key === 'enter' || key === 'return') { void this.advance(); return true; }
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Enter on the current step: agent → dir picker; dir → create the session. */
|
|
163
|
+
private async advance(): Promise<void> {
|
|
164
|
+
if (!this.mode) return;
|
|
165
|
+
if (this.mode.step === 'agent') {
|
|
166
|
+
const agent = this.mode.agents[this.mode.index];
|
|
167
|
+
if (!agent) return;
|
|
168
|
+
const candidates = await this.resolveDirCandidates();
|
|
169
|
+
this.mode = { step: 'dir', agent, candidates, index: 0 };
|
|
170
|
+
this.deps.markDirty();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const cwd = this.mode.candidates[this.mode.index]?.path;
|
|
174
|
+
const agentId = this.mode.agent.id;
|
|
175
|
+
if (!cwd) return;
|
|
176
|
+
await this.createSession(agentId, cwd);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The known-dir candidates: the current directory first, then registered workspaces (deduped). */
|
|
180
|
+
private async resolveDirCandidates(): Promise<AcpDirCandidate[]> {
|
|
181
|
+
const current = this.deps.currentDirectory();
|
|
182
|
+
const candidates: AcpDirCandidate[] = [{ path: current, label: 'current directory' }];
|
|
183
|
+
const gateway = this.requireGateway();
|
|
184
|
+
if (gateway) {
|
|
185
|
+
try {
|
|
186
|
+
for (const ws of await gateway.listWorkspaces()) {
|
|
187
|
+
if (ws.root === current) continue; // already offered as "current directory"
|
|
188
|
+
candidates.push({ path: ws.root, label: ws.label ?? 'workspace' });
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
// A workspaces read failure is non-fatal: the current dir is still offered.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return candidates;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Drive acp.sessions.create and surface the honest outcome (success or structured failure). */
|
|
198
|
+
private async createSession(agentId: string, cwd: string): Promise<void> {
|
|
199
|
+
const gateway = this.requireGateway();
|
|
200
|
+
if (!gateway) { this.mode = null; this.deps.markDirty(); return; }
|
|
201
|
+
this.creating = true;
|
|
202
|
+
this.deps.markDirty();
|
|
203
|
+
try {
|
|
204
|
+
const result = await gateway.createSession({ agentId, cwd });
|
|
205
|
+
const hosted = result.hosted;
|
|
206
|
+
if (hosted.error) {
|
|
207
|
+
// Structured failure rendered verbatim — never a hung row.
|
|
208
|
+
this.deps.notify(`[Fleet] Could not host ${hosted.title || agentId}: ${hosted.error.stage} stage failed for ${hosted.error.binary} — ${hosted.error.message}`);
|
|
209
|
+
} else {
|
|
210
|
+
this.deps.notify(`[Fleet] Hosting ${hosted.title || agentId} in ${cwd} — it appears as an acp-agent row; steer and stop it like any agent.`);
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
this.deps.notify(`ACP session create failed: ${summarizeError(err)}`);
|
|
214
|
+
} finally {
|
|
215
|
+
this.creating = false;
|
|
216
|
+
this.mode = null;
|
|
217
|
+
this.deps.markDirty();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The spawn picker view (replaces the tree while a pick is in flight). */
|
|
222
|
+
public renderSpawnMode(width: number, height: number, palette: PanelPalette = C): Line[] {
|
|
223
|
+
const P = palette;
|
|
224
|
+
if (!this.mode) return [];
|
|
225
|
+
const lines: Line[] = [];
|
|
226
|
+
if (this.creating) {
|
|
227
|
+
lines.push(buildPanelLine(width, [[' Hosting the agent…', P.dim]]));
|
|
228
|
+
return buildPanelWorkspace(width, height, { title: 'Fleet — Host an agent', sections: [{ lines }], footerLines: [], palette: P });
|
|
229
|
+
}
|
|
230
|
+
if (this.mode.step === 'agent') {
|
|
231
|
+
lines.push(buildPanelLine(width, [[' Host a third-party coding agent — pick one, then a directory.', P.dim]]));
|
|
232
|
+
lines.push(buildPanelLine(width, [['', P.dim]]));
|
|
233
|
+
this.mode.agents.forEach((agent, i) => {
|
|
234
|
+
const selected = i === (this.mode as AgentStep).index;
|
|
235
|
+
lines.push(buildPanelLine(width, [
|
|
236
|
+
[selected ? ' ▸ ' : ' ', selected ? P.info : P.dim],
|
|
237
|
+
[agent.title, selected ? P.value : P.dim],
|
|
238
|
+
[` — ${agent.binaryPath}`, P.dim],
|
|
239
|
+
]));
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
lines.push(buildPanelLine(width, [[` Directory for ${this.mode.agent.title} — a known dir, no path to type.`, P.dim]]));
|
|
243
|
+
lines.push(buildPanelLine(width, [['', P.dim]]));
|
|
244
|
+
this.mode.candidates.forEach((cand, i) => {
|
|
245
|
+
const selected = i === (this.mode as DirStep).index;
|
|
246
|
+
lines.push(buildPanelLine(width, [
|
|
247
|
+
[selected ? ' ▸ ' : ' ', selected ? P.info : P.dim],
|
|
248
|
+
[cand.label, selected ? P.value : P.dim],
|
|
249
|
+
[` — ${cand.path}`, P.dim],
|
|
250
|
+
]));
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const footerLines = [buildKeyboardHints(width, [
|
|
254
|
+
{ keys: '↑↓', label: 'choose' },
|
|
255
|
+
{ keys: 'Enter', label: this.mode.step === 'agent' ? 'pick agent' : 'host here' },
|
|
256
|
+
{ keys: 'Esc', label: 'cancel' },
|
|
257
|
+
], P)];
|
|
258
|
+
return buildPanelWorkspace(width, height, { title: 'Fleet — Host an agent', sections: [{ lines }], footerLines, palette: P });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private requireGateway(): AcpSpawnGateway | null {
|
|
262
|
+
const resolution = this.deps.resolveGateway();
|
|
263
|
+
if (!resolution.available) { this.deps.notify(`[Fleet] ${resolution.reason}`); return null; }
|
|
264
|
+
return resolution.gateway;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -34,7 +34,7 @@ export function steerBadgeTone(status: SteerBadgeStatus, palette: PanelPalette):
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
/** Labels of the currently live, steerable agent nodes other than `excludeNodeId` — offered when a steer target has gone inactive (WO
|
|
37
|
+
/** Labels of the currently live, steerable agent nodes other than `excludeNodeId` — offered when a steer target has gone inactive (WO item 4). */
|
|
38
38
|
export function liveSteerableLabels(nodes: readonly ProcessNode[], excludeNodeId: string): string[] {
|
|
39
39
|
return nodes
|
|
40
40
|
.filter((node) => node.id !== excludeNodeId && node.kind === 'agent' && node.capabilities.steerable && !isTerminalProcessState(node.state))
|
|
@@ -49,7 +49,7 @@ export function steerRefusalMessage(reason: string, siblingLabels: readonly stri
|
|
|
49
49
|
return `${reason}.${suggestion}`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/** One-line honest status for a tab's steer badge; the queued line names the target and points at the ⧗ delivery badge (WO
|
|
52
|
+
/** One-line honest status for a tab's steer badge; the queued line names the target and points at the ⧗ delivery badge (WO item 4). */
|
|
53
53
|
export function renderSteerBadgeLine(badge: SteerBadge, width: number, palette: PanelPalette, targetLabel?: string): Line {
|
|
54
54
|
const glyph = steerBadgeGlyph(badge.status);
|
|
55
55
|
const tone = steerBadgeTone(badge.status, palette);
|
package/src/panels/fleet-stop.ts
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// fleet-stop.ts
|
|
3
3
|
//
|
|
4
|
-
// d1
|
|
4
|
+
// d1 — the "stopping…" write-window overlay, split out of fleet-panel.ts
|
|
5
5
|
// to keep that file under the architecture 800-line gate (same rationale as
|
|
6
6
|
// fleet-steer.ts). FleetPanel owns nothing here beyond a FleetStopTracker
|
|
7
7
|
// instance; all the state->display logic lives in these pure helpers.
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
9
|
|
|
10
10
|
import type { ProcessNode, ProcessState } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
11
|
-
import
|
|
11
|
+
import type { WorkItem } from '@pellux/goodvibes-sdk/platform/orchestration';
|
|
12
|
+
import { fleetNodeAttention, fleetStateGlyph, fleetStateTone, isTerminalProcessState, type FleetStateTone, type FleetTreeRow } from './fleet-read-model.ts';
|
|
13
|
+
|
|
14
|
+
/** True when a work-item node owns a worktree that worktrees.discard can act on (D from the tree). */
|
|
15
|
+
function ownsWorktree(node: ProcessNode): boolean {
|
|
16
|
+
return Boolean((node.raw as { item?: WorkItem } | undefined)?.item?.worktreePath);
|
|
17
|
+
}
|
|
12
18
|
|
|
13
19
|
/** How long 'stopping…' lingers after a stop keypress before the true state is shown regardless (never masks a stuck kill). */
|
|
14
20
|
export const STOP_SETTLE_MS = 1500;
|
|
@@ -80,7 +86,7 @@ export interface FleetPauseDeps {
|
|
|
80
86
|
}
|
|
81
87
|
|
|
82
88
|
/**
|
|
83
|
-
* d2
|
|
89
|
+
* d2: 'p' toggles pause<->resume by the node's state. A `paused`
|
|
84
90
|
* resumable node is resumed (interrupt's inverse); a live pausable node is
|
|
85
91
|
* paused via the registry's interrupt() (its disable path). Honest refusal for
|
|
86
92
|
* non-resumable/non-pausable kinds. Returns true when the key is consumed,
|
|
@@ -123,9 +129,15 @@ export function buildFleetTreeHints(
|
|
|
123
129
|
): FleetHint[] {
|
|
124
130
|
const live = selected !== undefined && !isTerminalProcessState(selected.state);
|
|
125
131
|
const isPaused = selected !== undefined && selected.state === 'paused';
|
|
132
|
+
// Enter is context-sensitive: on a flagged pick/conflict row it acts, else it
|
|
133
|
+
// attaches. Name the act so the key is discoverable from the tree.
|
|
134
|
+
const attention = selected ? fleetNodeAttention(selected) : null;
|
|
135
|
+
const enterLabel = attention?.reason === 'pick' ? 'pick'
|
|
136
|
+
: attention?.reason === 'conflict' ? 'resolve conflict'
|
|
137
|
+
: 'attach';
|
|
126
138
|
const hints: FleetHint[] = [
|
|
127
139
|
{ keys: 'j/k', label: 'navigate' },
|
|
128
|
-
{ keys: 'Enter', label:
|
|
140
|
+
{ keys: 'Enter', label: enterLabel },
|
|
129
141
|
];
|
|
130
142
|
// Jump-to-blocked is offered whenever something is waiting on the operator,
|
|
131
143
|
// in either view (the blocked nodes live in the active fleet, and the jump
|
|
@@ -144,15 +156,17 @@ export function buildFleetTreeHints(
|
|
|
144
156
|
if (live && selected.capabilities.killable) hints.push({ keys: 'K', label: 'kill' });
|
|
145
157
|
if (live && !isPaused && selected.capabilities.pausable) hints.push({ keys: 'p', label: 'pause' });
|
|
146
158
|
if (isPaused && selected.capabilities.resumable) hints.push({ keys: 'p', label: 'resume' });
|
|
159
|
+
if (selected && ownsWorktree(selected)) hints.push({ keys: 'D', label: 'discard worktree' });
|
|
147
160
|
if (selected && !live) hints.push({ keys: 'a', label: 'archive' });
|
|
148
161
|
hints.push({ keys: 'A', label: 'archive finished' });
|
|
149
162
|
hints.push({ keys: 'v', label: 'archived' });
|
|
163
|
+
hints.push({ keys: 'n', label: 'host agent' });
|
|
150
164
|
hints.push({ keys: 'f', label: follow ? 'follow:on' : 'follow' });
|
|
151
165
|
if (hasTabs) hints.push({ keys: '[ ]', label: 'tabs' });
|
|
152
166
|
return hints;
|
|
153
167
|
}
|
|
154
168
|
|
|
155
|
-
/** K-confirm descendant stats (
|
|
169
|
+
/** K-confirm descendant stats (item 6): total = every non-terminal descendant (what a cascade kill takes down); active = the individually-killable subset. Was "active only" in an earlier version, undercounting a mixed subtree. */
|
|
156
170
|
export function countDescendantStats(rows: readonly FleetTreeRow[], nodeId: string): { total: number; active: number } {
|
|
157
171
|
const byParent = new Map<string, string[]>();
|
|
158
172
|
for (const row of rows) {
|
|
@@ -178,3 +192,25 @@ export function countDescendantStats(rows: readonly FleetTreeRow[], nodeId: stri
|
|
|
178
192
|
}
|
|
179
193
|
return { total, active };
|
|
180
194
|
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Build the confirm-overlay arm options for a cascade Kill of one node — the
|
|
198
|
+
* label carries the descendant count so the operator sees what a confirm takes
|
|
199
|
+
* down. Extracted from FleetPanel to keep that file under the 800-line cap.
|
|
200
|
+
*/
|
|
201
|
+
export function fleetKillConfirmArgs(
|
|
202
|
+
node: ProcessNode,
|
|
203
|
+
items: readonly FleetTreeRow[],
|
|
204
|
+
deps: { readonly kill: (id: string) => void; readonly markStopping: (id: string) => void },
|
|
205
|
+
): { id: string; label: string; verb: string; onConfirm: () => void } {
|
|
206
|
+
const shortId = node.id.length > 8 ? node.id.slice(-8) : node.id;
|
|
207
|
+
const stats = countDescendantStats(items, node.id);
|
|
208
|
+
const suffix = stats.total > 0 ? ` (+${stats.total} descendant${stats.total === 1 ? '' : 's'}, ${stats.active} active)` : '';
|
|
209
|
+
return {
|
|
210
|
+
id: node.id,
|
|
211
|
+
label: `${node.kind} ${shortId}${suffix}`,
|
|
212
|
+
verb: 'Kill',
|
|
213
|
+
// Mark 'stopping…' only once the kill is confirmed, not while merely armed.
|
|
214
|
+
onConfirm: () => { deps.kill(node.id); deps.markStopping(node.id); },
|
|
215
|
+
};
|
|
216
|
+
}
|
package/src/panels/fleet-tabs.ts
CHANGED
|
@@ -25,10 +25,10 @@ import { MessageLineCache } from '../core/conversation-line-cache.ts';
|
|
|
25
25
|
import { fleetKindTag } from './fleet-read-model.ts';
|
|
26
26
|
|
|
27
27
|
/** Node kinds that can be attached as a session tab. */
|
|
28
|
-
export type FleetAttachableKind = 'agent' | 'wrfc-chain';
|
|
28
|
+
export type FleetAttachableKind = 'agent' | 'wrfc-chain' | 'acp-agent';
|
|
29
29
|
|
|
30
30
|
export function isAttachableFleetKind(kind: ProcessKind): kind is FleetAttachableKind {
|
|
31
|
-
return kind === 'agent' || kind === 'wrfc-chain';
|
|
31
|
+
return kind === 'agent' || kind === 'wrfc-chain' || kind === 'acp-agent';
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
@@ -91,17 +91,16 @@ export function appendSteerText(draft: string, ch: string): string {
|
|
|
91
91
|
* One attached session tab. `agentId` is the SDK attach handle for
|
|
92
92
|
* `AgentManager.getConversationSnapshot(agentId)` — populated only for
|
|
93
93
|
* 'agent' tabs (agent.ts sets ProcessNode.id = record.id, so node.id IS the
|
|
94
|
-
* agentId
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
94
|
+
* agentId). 'wrfc-chain' tabs render a live member-summary instead of a
|
|
95
|
+
* transcript (a chain has no single conversation of its own — see
|
|
96
|
+
* fleet-transcript.ts renderFleetChainSummary) and carry an empty agentId,
|
|
97
|
+
* unused.
|
|
98
98
|
*
|
|
99
|
-
* `lineCache` is a PER-TAB `MessageLineCache
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* Line[] are not retained (brief risk #8).
|
|
99
|
+
* `lineCache` is a PER-TAB `MessageLineCache`: switching tabs must never
|
|
100
|
+
* invalidate or thrash the main session's own cache, and a background
|
|
101
|
+
* (non-focused) tab must render nothing beyond its existing tree-row, so its
|
|
102
|
+
* cache simply stays empty (size 0) until the tab is focused. Disposed
|
|
103
|
+
* (`.clear()`) on detach so a closed tab's rendered Line[] are not retained.
|
|
105
104
|
*/
|
|
106
105
|
export interface FleetTab {
|
|
107
106
|
readonly nodeId: string;
|
|
@@ -111,10 +110,10 @@ export interface FleetTab {
|
|
|
111
110
|
readonly lineCache: MessageLineCache;
|
|
112
111
|
/**
|
|
113
112
|
* Cached parsed ledger fallback for a terminal agent whose conversation
|
|
114
|
-
* snapshot has been evicted from the SDK's retention ring (
|
|
115
|
-
* path). `null` = not yet loaded/attempted; `[]` = loaded and
|
|
116
|
-
* the load failed) — both render the same honest "no transcript"
|
|
117
|
-
* once loaded is true.
|
|
113
|
+
* snapshot has been evicted from the SDK's retention ring (the degraded
|
|
114
|
+
* fallback path). `null` = not yet loaded/attempted; `[]` = loaded and
|
|
115
|
+
* empty (or the load failed) — both render the same honest "no transcript"
|
|
116
|
+
* state once loaded is true.
|
|
118
117
|
*/
|
|
119
118
|
ledgerEntries: Record<string, unknown>[] | null;
|
|
120
119
|
ledgerLoadStarted: boolean;
|
|
@@ -59,7 +59,7 @@ function toneColor(tone: FleetStateTone, palette: PanelPalette): string {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// ---------------------------------------------------------------------------
|
|
62
|
-
// Agent transcript (
|
|
62
|
+
// Agent transcript (live/frozen split)
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
64
|
|
|
65
65
|
export type FleetTranscriptMode = 'live' | 'frozen' | 'unavailable';
|
|
@@ -70,7 +70,7 @@ export interface FleetTranscriptRender {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
73
|
+
* (design point 4) — a 'frozen' transcript is a static capture of a
|
|
74
74
|
* completed process, not something that will change on the next render. The
|
|
75
75
|
* distinction matters because this wave also introduced kill/interrupt
|
|
76
76
|
* display bugs (elapsed/usage briefly climbing after a terminal state
|
|
@@ -157,7 +157,7 @@ export function renderFleetAgentTranscript(
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
// ---------------------------------------------------------------------------
|
|
160
|
-
// Chain summary — 'wrfc-chain' tabs have no single conversation
|
|
160
|
+
// Chain summary — 'wrfc-chain' tabs have no single conversation
|
|
161
161
|
// ---------------------------------------------------------------------------
|
|
162
162
|
|
|
163
163
|
/**
|
|
@@ -197,7 +197,7 @@ export function renderFleetChainSummary(
|
|
|
197
197
|
|
|
198
198
|
// ---------------------------------------------------------------------------
|
|
199
199
|
// Ledger fallback — degraded activity view for a terminal agent whose
|
|
200
|
-
// full-fidelity snapshot is unavailable (
|
|
200
|
+
// full-fidelity snapshot is unavailable (an honest fallback)
|
|
201
201
|
// ---------------------------------------------------------------------------
|
|
202
202
|
|
|
203
203
|
/**
|
|
@@ -257,7 +257,7 @@ function renderLedgerEntry(width: number, entry: Record<string, unknown>, palett
|
|
|
257
257
|
const toolName = typeof entry['toolName'] === 'string' ? entry['toolName'] : 'tool';
|
|
258
258
|
const success = entry['success'] !== false;
|
|
259
259
|
const bad = palette.bad ?? DEFAULT_PANEL_PALETTE.bad;
|
|
260
|
-
//
|
|
260
|
+
// the writer already truncates this to 500 chars (session.ts's
|
|
261
261
|
// resultPreview field) — passed through verbatim (collapsed to one
|
|
262
262
|
// line), never re-summarized or fabricated, then tail-truncated again
|
|
263
263
|
// to fit the row.
|
package/src/panels/git-panel.ts
CHANGED
|
@@ -77,7 +77,7 @@ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
|
77
77
|
commitAuthor: '244',
|
|
78
78
|
selected: '#1c1c1c',
|
|
79
79
|
selectedFg: '#ffffff',
|
|
80
|
-
diffMeta: DIFF_TONES.hunk, //
|
|
80
|
+
diffMeta: DIFF_TONES.hunk, // shared diff-hunk token, was a local literal
|
|
81
81
|
diffNeutral: '250',
|
|
82
82
|
// Reuses the existing workflow accent token rather than adding a new hex
|
|
83
83
|
// literal (architecture gate ratchets the raw-hex-literal count).
|
|
@@ -626,7 +626,7 @@ export class GitPanel extends BasePanel {
|
|
|
626
626
|
{ keys: 'r', label: 'refresh' },
|
|
627
627
|
], DEFAULT_PANEL_PALETTE),
|
|
628
628
|
];
|
|
629
|
-
const emptyStateLines = buildEmptyState(width, ' No git rows', 'This repository has no staged or unstaged changes and no commits to display yet.', [{ command: 'r', summary: 'refresh working-tree status in this panel, or stage changes to populate this view' }], DEFAULT_PANEL_PALETTE); //
|
|
629
|
+
const emptyStateLines = buildEmptyState(width, ' No git rows', 'This repository has no staged or unstaged changes and no commits to display yet.', [{ command: 'r', summary: 'refresh working-tree status in this panel, or stage changes to populate this view' }], DEFAULT_PANEL_PALETTE); // was '/git status' (chat-only, never touches this panel)
|
|
630
630
|
const rows: Line[] = [];
|
|
631
631
|
for (let i = 0; i < this.items.length; i++) {
|
|
632
632
|
const item = this.items[i];
|
package/src/panels/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { CostTrackerPanel } from './cost-tracker-panel.ts';
|
|
|
7
7
|
export { GitPanel } from './git-panel.ts';
|
|
8
8
|
export { registerBuiltinPanels } from './builtin-panels.ts';
|
|
9
9
|
export type { BuiltinPanelDeps } from './builtin-panels.ts';
|
|
10
|
-
//
|
|
10
|
+
// (the purge) — group B: plugins/skills panels are register-retired but
|
|
11
11
|
// their modules are retained for shared non-class exports (PluginManagerControls;
|
|
12
12
|
// discoverSkills + SkillRecord/SkillOrigin, consumed by command runtimes and the
|
|
13
13
|
// skills modal). The other group-A and group-B panel classes are deleted outright
|
|
@@ -16,3 +16,6 @@ export { PluginsPanel } from './plugins-panel.ts';
|
|
|
16
16
|
export { SkillsPanel } from './skills-panel.ts';
|
|
17
17
|
export { FleetPanel } from './fleet-panel.ts';
|
|
18
18
|
export type { FleetActionCallbacks } from './fleet-panel.ts';
|
|
19
|
+
export { NotificationsPanel } from './notifications-panel.ts';
|
|
20
|
+
export { PanelNotificationFeed, getSharedNotificationFeed } from './notifications-feed.ts';
|
|
21
|
+
export type { PanelFeedEntry } from './notifications-feed.ts';
|
|
@@ -21,7 +21,7 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
|
21
21
|
import { type ConfirmState, handleConfirmInput } from './confirm-state.ts';
|
|
22
22
|
|
|
23
23
|
// Base chrome only — state colors and text tokens come straight from
|
|
24
|
-
// DEFAULT_PANEL_PALETTE
|
|
24
|
+
// DEFAULT_PANEL_PALETTE.
|
|
25
25
|
const C = DEFAULT_PANEL_PALETTE;
|
|
26
26
|
|
|
27
27
|
function formatRoles(roles: readonly string[]): string {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// memory-consolidation-gateway.ts
|
|
3
|
+
//
|
|
4
|
+
// The async, daemon-backed verb the Memory modal's Proposals tab reads:
|
|
5
|
+
// memory.consolidation.receipts (GET /api/memory/consolidation/receipts) — the
|
|
6
|
+
// retained memory-consolidation run receipts plus the flattened
|
|
7
|
+
// pendingProposals (contradiction / cross-scope-duplicate / stale-delete)
|
|
8
|
+
// awaiting human review. The records a proposal references are already
|
|
9
|
+
// marked into the review queue by the daemon (reviewState 'contradicted' with
|
|
10
|
+
// a staleReason, or 'fresh' for cross-scope duplicates); this gateway is only
|
|
11
|
+
// how the Memory modal learns WHAT was proposed and WHY, so it can show that
|
|
12
|
+
// reason next to the affected records and offer a jump to them.
|
|
13
|
+
//
|
|
14
|
+
// No named facade exists on the in-process OperatorClient for this verb, so —
|
|
15
|
+
// exactly like the Fleet gateway (fleet-gateway.ts) — it goes over the
|
|
16
|
+
// generic operator invoke path (operator-rpc.ts's resolveOperatorRpc ->
|
|
17
|
+
// sdk.operator.invoke), reaching the SAME daemon the command layer does. A
|
|
18
|
+
// runtime without the consolidation scheduler answers an honest 501; an older
|
|
19
|
+
// daemon that has not wired this route yet answers 404 — both are "verb
|
|
20
|
+
// unavailable", not "no proposals" (see classifyConsolidationFetchError).
|
|
21
|
+
//
|
|
22
|
+
// The interface is injectable so the modal's proposals fetch round-trips
|
|
23
|
+
// against a mocked daemon in tests; the live builder
|
|
24
|
+
// (createMemoryConsolidationGateway) is wired in builtin-modals.ts, resolved
|
|
25
|
+
// lazily (a fresh factory call per fetch) exactly like the Fleet gateway.
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
29
|
+
import type { OperatorMethodOutput } from '@pellux/goodvibes-sdk';
|
|
30
|
+
import { GoodVibesSdkError } from '@pellux/goodvibes-sdk';
|
|
31
|
+
import { resolveOperatorRpc, describeOperatorRpcError } from '../input/commands/operator-rpc.ts';
|
|
32
|
+
|
|
33
|
+
/** memory.consolidation.receipts output — retained receipts + the flattened pending proposals. */
|
|
34
|
+
export type MemoryConsolidationReceiptsResult = OperatorMethodOutput<'memory.consolidation.receipts'>;
|
|
35
|
+
/** One pending judgment proposal (contradiction / cross-scope-duplicate / stale-delete) awaiting human review. */
|
|
36
|
+
export type MemoryConsolidationProposal = MemoryConsolidationReceiptsResult['pendingProposals'][number];
|
|
37
|
+
|
|
38
|
+
/** The narrow async verb surface the Memory modal's Proposals tab drives. */
|
|
39
|
+
export interface MemoryConsolidationGateway {
|
|
40
|
+
/** Fetch the retained consolidation receipts + flattened pending proposals. */
|
|
41
|
+
fetchReceipts(): Promise<MemoryConsolidationReceiptsResult>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Why the gateway could not be built (daemon disabled / no control-plane URL),
|
|
46
|
+
* surfaced verbatim so the modal can print an honest "unavailable" line
|
|
47
|
+
* rather than guessing — mirrors FleetGatewayResolution.
|
|
48
|
+
*/
|
|
49
|
+
export type MemoryConsolidationGatewayResolution =
|
|
50
|
+
| { readonly available: true; readonly gateway: MemoryConsolidationGateway }
|
|
51
|
+
| { readonly available: false; readonly reason: string };
|
|
52
|
+
|
|
53
|
+
export interface MemoryConsolidationGatewayDeps {
|
|
54
|
+
readonly configManager: ConfigManager;
|
|
55
|
+
readonly homeDirectory: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build the live consolidation-receipts gateway over the generic operator
|
|
60
|
+
* invoke path — the same daemon resolution the command layer and the Fleet
|
|
61
|
+
* gateway use. Returns an honest unavailable reason when no daemon is
|
|
62
|
+
* reachable, so the modal can refuse cleanly instead of throwing mid-render.
|
|
63
|
+
*/
|
|
64
|
+
export function createMemoryConsolidationGateway(deps: MemoryConsolidationGatewayDeps): MemoryConsolidationGatewayResolution {
|
|
65
|
+
const rpc = resolveOperatorRpc({ configManager: deps.configManager, homeDirectory: deps.homeDirectory });
|
|
66
|
+
if (!rpc.available) return { available: false, reason: rpc.reason };
|
|
67
|
+
const { sdk } = rpc;
|
|
68
|
+
return {
|
|
69
|
+
available: true,
|
|
70
|
+
gateway: { fetchReceipts: () => sdk.operator.invoke('memory.consolidation.receipts', {}) },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** How a `fetchReceipts()` rejection should render in the Proposals tab. */
|
|
75
|
+
export type MemoryConsolidationFetchFailure =
|
|
76
|
+
| { readonly kind: 'unavailable'; readonly reason: string }
|
|
77
|
+
| { readonly kind: 'error'; readonly message: string };
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Classify a `fetchReceipts()` rejection: a 501 (the documented "no
|
|
81
|
+
* consolidation scheduler on this runtime" response) or a 404 (an older
|
|
82
|
+
* daemon that has not wired this route yet) are both "verb unavailable" —
|
|
83
|
+
* distinct from a generic request failure (network error, 401/403, 500),
|
|
84
|
+
* which renders as an honest fetch error instead. Reuses
|
|
85
|
+
* describeOperatorRpcError's wording so the two call sites (command layer,
|
|
86
|
+
* Fleet gateway, this gateway) never diverge on how they describe the same
|
|
87
|
+
* daemon response.
|
|
88
|
+
*/
|
|
89
|
+
export function classifyConsolidationFetchError(error: unknown): MemoryConsolidationFetchFailure {
|
|
90
|
+
if (error instanceof GoodVibesSdkError && (error.status === 501 || error.status === 404)) {
|
|
91
|
+
return { kind: 'unavailable', reason: describeOperatorRpcError(error) };
|
|
92
|
+
}
|
|
93
|
+
return { kind: 'error', message: describeOperatorRpcError(error) };
|
|
94
|
+
}
|