@pellux/goodvibes-tui 1.17.0 → 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 +49 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +4596 -1732
- package/package.json +7 -7
- package/src/cli/management-commands.ts +82 -15
- package/src/cli/management.ts +0 -2
- package/src/core/consolidation-receipt.ts +41 -0
- package/src/core/conversation-rendering.ts +14 -5
- 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 +21 -0
- package/src/core/turn-budget-outcome.ts +60 -0
- package/src/core/turn-cancellation.ts +39 -0
- package/src/core/turn-event-wiring.ts +6 -0
- package/src/daemon/cli.ts +47 -56
- package/src/input/command-registry.ts +21 -0
- package/src/input/commands/acp-runtime.ts +72 -0
- 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/operator-rpc.ts +32 -11
- package/src/input/commands/power-runtime.ts +56 -0
- package/src/input/commands/qrcode-runtime.ts +14 -19
- package/src/input/commands/queue-runtime.ts +70 -0
- package/src/input/commands/workstream-attempts.ts +6 -1
- package/src/input/commands/workstream-runtime.ts +44 -25
- package/src/input/commands/worktree-runtime.ts +35 -8
- package/src/input/commands.ts +10 -0
- package/src/input/config-modal-types.ts +11 -0
- package/src/input/config-modal.ts +65 -2
- package/src/input/handler-shortcuts.ts +23 -0
- package/src/input/keybindings.ts +19 -1
- package/src/input/settings-modal-data.ts +31 -37
- package/src/input/settings-modal-types.ts +5 -2
- package/src/main.ts +42 -42
- package/src/panels/builtin/operations.ts +54 -1
- package/src/panels/builtin/shared.ts +12 -0
- package/src/panels/builtin-modals.ts +49 -32
- package/src/panels/diff-panel.ts +34 -1
- package/src/panels/fleet-acts.ts +446 -0
- package/src/panels/fleet-gateway.ts +117 -0
- package/src/panels/fleet-observed-render.ts +128 -0
- package/src/panels/fleet-panel-format.ts +120 -4
- package/src/panels/fleet-panel-worktree-detail.ts +14 -0
- package/src/panels/fleet-panel.ts +60 -44
- package/src/panels/fleet-read-model.ts +34 -3
- package/src/panels/fleet-spawn.ts +266 -0
- package/src/panels/fleet-stop.ts +38 -2
- package/src/panels/fleet-tabs.ts +2 -2
- package/src/panels/memory-consolidation-gateway.ts +94 -0
- package/src/panels/modals/devices-modal.ts +161 -0
- package/src/panels/modals/memory-modal.ts +149 -5
- package/src/panels/modals/pairing-modal.ts +198 -68
- package/src/panels/modals/security-modal.ts +6 -0
- package/src/panels/workstream-graph-render.ts +123 -0
- package/src/renderer/composer-fragments.ts +69 -0
- package/src/renderer/settings-modal-helpers.ts +9 -0
- package/src/renderer/settings-modal.ts +4 -2
- package/src/renderer/shell-surface.ts +3 -0
- package/src/renderer/tool-call.ts +12 -6
- package/src/renderer/ui-factory.ts +32 -0
- package/src/runtime/bootstrap-command-context.ts +3 -1
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +6 -6
- package/src/runtime/bootstrap-shell.ts +7 -0
- package/src/runtime/durability-services.ts +36 -1
- package/src/runtime/fleet-services.ts +41 -0
- package/src/runtime/idle-power-services.ts +85 -0
- package/src/runtime/index.ts +2 -0
- package/src/runtime/interaction-seams.ts +109 -0
- package/src/runtime/power-keepawake-remote.ts +75 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +38 -37
- package/src/runtime/ui-services.ts +3 -0
- package/src/shell/blocking-input.ts +21 -141
- package/src/shell/recovery-input-helpers.ts +53 -166
- package/src/version.ts +1 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// workstream-graph-render.ts — render a workstream's task graph (fleet.graph.get)
|
|
3
|
+
// legibly, so the observability layer shows the dependency graph's shape,
|
|
4
|
+
// states, and pool posture WITHOUT opening any transcript.
|
|
5
|
+
//
|
|
6
|
+
// The SDK serves a WorkstreamGraphSnapshot: nodes (id/title/state, plus
|
|
7
|
+
// blockedReason, orphaned, stalled, agentId), edges (from → to dependency
|
|
8
|
+
// links), and the elastic-pool state (ready/running/atCap/maxSize). This module
|
|
9
|
+
// is a pure string renderer; every line is fit to the given width (no clipping,
|
|
10
|
+
// no horizontal overflow), so the same output drives a panel or a command.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
import type { OperatorMethodOutput } from '@pellux/goodvibes-sdk';
|
|
14
|
+
import { truncateDisplay, getDisplayWidth } from '../utils/terminal-width.ts';
|
|
15
|
+
|
|
16
|
+
export type WorkstreamGraphSnapshot = OperatorMethodOutput<'fleet.graph.get'>;
|
|
17
|
+
export type WorkstreamGraphNode = WorkstreamGraphSnapshot['nodes'][number];
|
|
18
|
+
|
|
19
|
+
/** The pool posture line: "N ready, M running[, at cap (fleet.maxSize=N)]". */
|
|
20
|
+
export function renderPoolSummary(pool: WorkstreamGraphSnapshot['pool']): string {
|
|
21
|
+
if (!pool) return 'no pool state';
|
|
22
|
+
const base = `${pool.ready} ready, ${pool.running} running`;
|
|
23
|
+
return pool.atCap ? `${base}, at cap (fleet.maxSize=${pool.maxSize})` : base;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The leading glyph for a node — orphaned/blocked/stalled tells win over the base state. */
|
|
27
|
+
function nodeGlyph(node: WorkstreamGraphNode): string {
|
|
28
|
+
if (node.orphaned) return '⊘';
|
|
29
|
+
if (node.blockedReason) return '⊘';
|
|
30
|
+
if (node.stalled) return '◒';
|
|
31
|
+
switch (node.state) {
|
|
32
|
+
case 'running': return '●';
|
|
33
|
+
case 'ready': return '○';
|
|
34
|
+
case 'done': return '✓';
|
|
35
|
+
case 'failed': return '✕';
|
|
36
|
+
default: return '·';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The state tell for a node: the "waiting on: X" for blocked, running/ready, plus orphaned/stalled markers. */
|
|
41
|
+
function nodeTell(node: WorkstreamGraphNode, needs: readonly string[]): string {
|
|
42
|
+
const parts: string[] = [];
|
|
43
|
+
if (node.blockedReason) {
|
|
44
|
+
parts.push(`blocked — waiting on: ${node.blockedReason}`);
|
|
45
|
+
} else if (node.state === 'running') {
|
|
46
|
+
parts.push(node.agentId ? `running (${node.agentId})` : 'running');
|
|
47
|
+
} else if (node.state === 'ready') {
|
|
48
|
+
parts.push('ready');
|
|
49
|
+
} else {
|
|
50
|
+
parts.push(node.state);
|
|
51
|
+
}
|
|
52
|
+
if (needs.length > 0 && !node.blockedReason) parts.push(`needs: ${needs.join(', ')}`);
|
|
53
|
+
if (node.orphaned) parts.push('orphaned');
|
|
54
|
+
if (node.stalled) parts.push('stalled');
|
|
55
|
+
return parts.join(' · ');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Fit a line to the width without overflowing (truncates with an ellipsis). */
|
|
59
|
+
function fit(line: string, width: number): string {
|
|
60
|
+
return getDisplayWidth(line) > width ? truncateDisplay(line, width) : line;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Wrap content to the width on word boundaries — NO clipping: the whole tell
|
|
65
|
+
* (including a long "waiting on: …" reason) survives onto indented continuation
|
|
66
|
+
* lines rather than being cut off. `firstIndent` leads the first line;
|
|
67
|
+
* `contIndent` leads each continuation. A single word longer than the width is
|
|
68
|
+
* hard-fit defensively so a line never overflows.
|
|
69
|
+
*/
|
|
70
|
+
function wrapContent(content: string, width: number, firstIndent: string, contIndent: string): string[] {
|
|
71
|
+
const words = content.split(/\s+/).filter(Boolean);
|
|
72
|
+
const out: string[] = [];
|
|
73
|
+
let cur = firstIndent;
|
|
74
|
+
let hasWord = false;
|
|
75
|
+
for (const word of words) {
|
|
76
|
+
const candidate = hasWord ? `${cur} ${word}` : `${cur}${word}`;
|
|
77
|
+
if (getDisplayWidth(candidate) > width && hasWord) {
|
|
78
|
+
out.push(cur);
|
|
79
|
+
cur = `${contIndent}${word}`;
|
|
80
|
+
} else {
|
|
81
|
+
cur = candidate;
|
|
82
|
+
}
|
|
83
|
+
hasWord = true;
|
|
84
|
+
}
|
|
85
|
+
if (hasWord) out.push(cur);
|
|
86
|
+
return out.map((l) => (getDisplayWidth(l) > width ? fit(l, width) : l));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Render the whole snapshot as legible lines, each fit to `width`. The header
|
|
91
|
+
* carries the pool posture; every node is one line with its glyph, title, and
|
|
92
|
+
* state tell (ready / running / blocked-with-waiting-on / stalled / orphaned),
|
|
93
|
+
* with its dependency edges named inline ("needs: …"). An empty graph states so
|
|
94
|
+
* honestly rather than rendering a blank surface.
|
|
95
|
+
*/
|
|
96
|
+
export function renderWorkstreamGraphLines(snapshot: WorkstreamGraphSnapshot, width: number): string[] {
|
|
97
|
+
const lines: string[] = [];
|
|
98
|
+
// Title and pool posture on their own lines so neither clips the other at a
|
|
99
|
+
// narrow width — the pool summary ("… at cap (fleet.maxSize=N)") is load-bearing.
|
|
100
|
+
lines.push(fit(snapshot.title, width));
|
|
101
|
+
lines.push(fit(` ${renderPoolSummary(snapshot.pool)}`, width));
|
|
102
|
+
|
|
103
|
+
if (snapshot.nodes.length === 0) {
|
|
104
|
+
lines.push(fit(' (no work items in this workstream)', width));
|
|
105
|
+
return lines;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const titleById = new Map<string, string>();
|
|
109
|
+
for (const node of snapshot.nodes) titleById.set(node.id, node.title);
|
|
110
|
+
const needsByNode = new Map<string, string[]>();
|
|
111
|
+
for (const edge of snapshot.edges) {
|
|
112
|
+
const list = needsByNode.get(edge.to) ?? [];
|
|
113
|
+
list.push(titleById.get(edge.from) ?? edge.from);
|
|
114
|
+
needsByNode.set(edge.to, list);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const node of snapshot.nodes) {
|
|
118
|
+
const glyph = nodeGlyph(node);
|
|
119
|
+
const tell = nodeTell(node, needsByNode.get(node.id) ?? []);
|
|
120
|
+
lines.push(...wrapContent(`${glyph} ${node.title} ${tell}`, width, ' ', ' '));
|
|
121
|
+
}
|
|
122
|
+
return lines;
|
|
123
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// composer-fragments.ts — composer-adjacent transcript fragments extracted from
|
|
3
|
+
// ui-factory.ts (file-size hygiene): the mid-turn queued-message editable list
|
|
4
|
+
// and the optional "used N memories" provenance chip. Pure renderers; the
|
|
5
|
+
// UIFactory static methods are thin wrappers over these.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
import { type Line } from '../types/grid.ts';
|
|
9
|
+
import { renderConversationFragment } from './conversation-surface.ts';
|
|
10
|
+
import { activeUiTones } from './theme.ts';
|
|
11
|
+
import { activeTheme } from './theme.ts';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The mid-turn message queue rendered as an EDITABLE list. Each still-undelivered
|
|
15
|
+
* message shows a 1-based number so it can be named to `/queue edit <n> …` /
|
|
16
|
+
* `/queue delete <n>` (the SDK editQueuedMessage / deleteQueuedMessage verbs). A
|
|
17
|
+
* delivered message has already left the queue, so the list only ever shows what
|
|
18
|
+
* is still editable — delivery is immutability, made visible.
|
|
19
|
+
*/
|
|
20
|
+
export function renderQueuedMessageList(width: number, items: readonly { readonly id: string; readonly text: string }[]): Line[] {
|
|
21
|
+
if (items.length === 0) return [];
|
|
22
|
+
const t = activeUiTones();
|
|
23
|
+
const bodyBg = activeTheme().collapsedBodyBg;
|
|
24
|
+
const lines: Line[] = [];
|
|
25
|
+
const header = `${items.length} queued — /queue edit·delete until delivered`;
|
|
26
|
+
lines.push(...renderConversationFragment(header, width, { prefix: ' ⧗ ', prefixFg: t.state.reasoning, text: t.fg.dim, bodyBg, dim: true }));
|
|
27
|
+
items.forEach((item, index) => {
|
|
28
|
+
lines.push(...renderConversationFragment(item.text, width, { prefix: ` ${index + 1}. `, prefixFg: t.state.reasoning, text: t.fg.dim, bodyBg, dim: true }));
|
|
29
|
+
});
|
|
30
|
+
return lines;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One drilled-in memory-provenance row. `record` is the RESOLVED record summary
|
|
35
|
+
* (undefined = still resolving, null = no longer available) — the drill-in
|
|
36
|
+
* shows a human summary, never a raw record id, matching the webui's detail
|
|
37
|
+
* fetch (per-id, so one missing record never blanks the rest).
|
|
38
|
+
*/
|
|
39
|
+
export interface MemoryProvenanceEntry {
|
|
40
|
+
readonly id: string;
|
|
41
|
+
readonly record?: { readonly summary: string; readonly cls: string } | null | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The optional "used N memories" turn chip. Collapsed: one small line naming the
|
|
46
|
+
* count with the drill-in hint. Expanded (Alt+M): the same line followed by one
|
|
47
|
+
* line per memory — its resolved SUMMARY (with the record class), a "resolving…"
|
|
48
|
+
* placeholder while the fetch is in flight, or an honest "no longer available"
|
|
49
|
+
* for a record that has since been forgotten. Empty when no memories were used.
|
|
50
|
+
*/
|
|
51
|
+
export function renderMemoryProvenanceChip(width: number, count: number, entries: readonly MemoryProvenanceEntry[], expanded: boolean): Line[] {
|
|
52
|
+
if (count <= 0) return [];
|
|
53
|
+
const t = activeUiTones();
|
|
54
|
+
const bodyBg = activeTheme().collapsedBodyBg;
|
|
55
|
+
const noun = count === 1 ? 'memory' : 'memories';
|
|
56
|
+
const header = expanded ? `used ${count} ${noun} — Alt+M to hide` : `used ${count} ${noun} — Alt+M to list`;
|
|
57
|
+
const lines: Line[] = renderConversationFragment(header, width, { prefix: ' ◆ ', prefixFg: t.state.info, text: t.fg.dim, bodyBg, dim: true });
|
|
58
|
+
if (expanded) {
|
|
59
|
+
entries.forEach((entry, index) => {
|
|
60
|
+
const body = entry.record === undefined
|
|
61
|
+
? 'resolving…'
|
|
62
|
+
: entry.record === null
|
|
63
|
+
? 'no longer available'
|
|
64
|
+
: `${entry.record.summary} · ${entry.record.cls}`;
|
|
65
|
+
lines.push(...renderConversationFragment(body, width, { prefix: ` ${index + 1}. `, prefixFg: t.state.info, text: t.fg.dim, bodyBg, dim: true }));
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return lines;
|
|
69
|
+
}
|
|
@@ -77,6 +77,7 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
|
|
|
77
77
|
wrfc: 'WRFC',
|
|
78
78
|
helper: 'Helper',
|
|
79
79
|
tts: 'TTS',
|
|
80
|
+
voice: 'Voice (local engines)',
|
|
80
81
|
service: 'Service',
|
|
81
82
|
daemon: 'Daemon',
|
|
82
83
|
controlPlane: 'Control Plane',
|
|
@@ -95,6 +96,8 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
|
|
|
95
96
|
surfaces: 'Surfaces',
|
|
96
97
|
cloudflare: 'Cloudflare',
|
|
97
98
|
release: 'Release',
|
|
99
|
+
power: 'Power',
|
|
100
|
+
memory: 'Memory',
|
|
98
101
|
danger: 'Danger',
|
|
99
102
|
tools: 'Tools',
|
|
100
103
|
network: 'Network',
|
|
@@ -115,6 +118,12 @@ const SETTING_LABELS: Partial<Record<string, string>> = {
|
|
|
115
118
|
'ui.voiceEnabled': 'Always Speak',
|
|
116
119
|
'behavior.autoCompactThreshold': 'Auto-Compact %',
|
|
117
120
|
'behavior.staleContextWarnings': 'Context Warnings',
|
|
121
|
+
// Blocked-too-long escalation: a fleet node waiting on a human gets a device
|
|
122
|
+
// push once the grace elapses (regardless of an attached surface), then bounded
|
|
123
|
+
// reminders. Real numeric option shapes come from the SDK schema's ranges.
|
|
124
|
+
'notifications.blockedEscalationGraceMs': 'Blocked-Too-Long Grace (ms)',
|
|
125
|
+
'notifications.blockedEscalationFollowUpMs': 'Blocked Reminder Interval (ms)',
|
|
126
|
+
'notifications.blockedEscalationMaxFollowUps': 'Blocked Reminder Limit',
|
|
118
127
|
'behavior.returnContextMode': 'Return Context',
|
|
119
128
|
'behavior.guidanceMode': 'Guidance Mode',
|
|
120
129
|
'storage.secretPolicy': 'Secret Policy',
|
|
@@ -40,6 +40,7 @@ const CATEGORY_INFO: Record<SettingsCategory, string> = {
|
|
|
40
40
|
wrfc: 'Work-review-fix-cycle thresholds, retry limits, and automatic commit behavior.',
|
|
41
41
|
helper: 'Helper model defaults used by helper subsystems when they do not use the main chat route.',
|
|
42
42
|
tts: 'Text-to-speech provider, voice, and optional spoken-turn LLM overrides.',
|
|
43
|
+
voice: 'Free, offline local voice engines (voice.local.*): the STT and TTS engine, binary, and model-path for whisper.cpp/faster-whisper and Piper/Kokoro. All empty by default — configurable-not-configured, nothing auto-downloads, and an unconfigured machine reports honestly (never an error). Once set, "Local engines" appears in the TTS provider picker beside elevenlabs, with no billing dimension.',
|
|
43
44
|
service: 'Background service posture: enabled state, autostart, restart behavior, service name, platform, and logs.',
|
|
44
45
|
daemon: 'Local session daemon. It hosts the shared session broker and companion chat so a session started here is visible and steerable from other surfaces. On by default, bound to loopback (127.0.0.1) only.',
|
|
45
46
|
controlPlane: 'Daemon control-plane settings for local admin/API access.',
|
|
@@ -58,6 +59,8 @@ const CATEGORY_INFO: Record<SettingsCategory, string> = {
|
|
|
58
59
|
surfaces: 'External app surfaces such as Slack, Discord, ntfy, Home Assistant, Telegram, webhooks, chat bridges, and messaging providers.',
|
|
59
60
|
cloudflare: 'Optional Cloudflare control plane, batch queue, Worker, Tunnel, Access, DNS, KV, Durable Objects, Secrets Store, and R2 settings.',
|
|
60
61
|
release: 'Release-channel preference.',
|
|
62
|
+
power: 'Host sleep ownership. Keep-awake holds the host awake (the always-visible "sleep disabled" chip is the safety mechanism — no timer, no AC-only option); automatic inhibition keeps the host awake while real work runs, with an honest hard cap on that work hold.',
|
|
63
|
+
memory: 'Memory provenance surfacing. Whether turns that drew on your standing memories show a small "used N memories" chip with a drill-in listing them. Off by default — when off, nothing is rendered and no context is added.',
|
|
61
64
|
danger: 'High-impact switches for daemon and HTTP listener behavior. These are operational overrides, not normal preferences.',
|
|
62
65
|
tools: 'Tool LLM and helper model routing. Empty provider/model values inherit the active chat route unless a specific helper/tool route is set.',
|
|
63
66
|
network: 'Combined network view for daemon control-plane, HTTP listener, browser web surface, and general outbound network settings.',
|
|
@@ -183,8 +186,7 @@ function buildSettingContext(modal: SettingsModal, entry: SettingEntry): string[
|
|
|
183
186
|
if (entry.locked) lines.push(`Locked: ${entry.lockReason ?? 'This setting is locked by a higher-priority layer.'}`);
|
|
184
187
|
if (entry.conflict) lines.push(`Conflict: resolve with /settings-sync resolve ${entry.setting.key} local|synced.`);
|
|
185
188
|
|
|
186
|
-
// A settings sub-row owned by a feature unit names its feature so "what
|
|
187
|
-
// does this do" is answerable without scrolling back to the header row.
|
|
189
|
+
// A settings sub-row owned by a feature unit names its feature so "what does this do" is answerable without scrolling back to the header row.
|
|
188
190
|
if (entry.ownerFlagId) {
|
|
189
191
|
const owner = FEATURE_SETTINGS_BY_ID.get(entry.ownerFlagId);
|
|
190
192
|
if (owner) lines.push(`Part of feature: ${owner.name} (the header row above).`);
|
|
@@ -67,6 +67,8 @@ export interface ShellFooterBuildOptions {
|
|
|
67
67
|
readonly sessionSpineStatus?: 'online' | 'offline';
|
|
68
68
|
/** The web surface's reachable URL, when that surface is enabled; a persistent footer segment. */
|
|
69
69
|
readonly webSurfaceUrl?: string;
|
|
70
|
+
/** True while power.keepAwake holds — renders the always-visible "sleep disabled" chip. */
|
|
71
|
+
readonly powerKeepAwake?: boolean;
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export interface ShellFooterBuildResult {
|
|
@@ -147,6 +149,7 @@ export function buildShellFooter(
|
|
|
147
149
|
options.sessionSpineStatus,
|
|
148
150
|
options.permissionMode,
|
|
149
151
|
options.webSurfaceUrl,
|
|
152
|
+
options.powerKeepAwake,
|
|
150
153
|
);
|
|
151
154
|
// Compact posture drops the process indicator and context hint entirely so
|
|
152
155
|
// the footer fits within ~5 rows on short terminals.
|
|
@@ -147,7 +147,7 @@ function extractKeyArg(toolCall: ToolCall): string {
|
|
|
147
147
|
* Layout: [margin] [icon] [space] [tool name padded] [key arg] [summary] [duration]
|
|
148
148
|
*
|
|
149
149
|
* @param toolCall - The tool call being executed
|
|
150
|
-
* @param status - 'executing' | 'done' | 'error'
|
|
150
|
+
* @param status - 'executing' | 'done' | 'error' | 'pending' | 'cancelled'
|
|
151
151
|
* @param resultSummary - Optional brief summary (e.g., "3 files", "exit 0")
|
|
152
152
|
* @param width - Terminal width
|
|
153
153
|
* @param durationMs - Optional duration in milliseconds
|
|
@@ -157,7 +157,7 @@ function extractKeyArg(toolCall: ToolCall): string {
|
|
|
157
157
|
*/
|
|
158
158
|
export function renderToolCallBlock(
|
|
159
159
|
toolCall: ToolCall,
|
|
160
|
-
status: 'executing' | 'done' | 'error' | 'pending',
|
|
160
|
+
status: 'executing' | 'done' | 'error' | 'pending' | 'cancelled',
|
|
161
161
|
resultSummary: string | undefined,
|
|
162
162
|
width: number,
|
|
163
163
|
durationMs?: number,
|
|
@@ -174,12 +174,16 @@ export function renderToolCallBlock(
|
|
|
174
174
|
// Status icon. 'pending' means the tool is still awaiting a decision
|
|
175
175
|
// (e.g. an approval prompt) and has NOT run yet — it uses the hollow idle
|
|
176
176
|
// glyph so a not-yet-run tool never shows the completed green ✓ (2c).
|
|
177
|
+
// 'cancelled' means the user stopped THIS call mid-flight — the blocked glyph
|
|
178
|
+
// in the warn tone, distinct from both success and a real error.
|
|
177
179
|
const icon = status === 'done' ? TOOL_STATUS.SUCCESS_ICON
|
|
178
180
|
: status === 'error' ? TOOL_STATUS.FAIL_ICON
|
|
181
|
+
: status === 'cancelled' ? GLYPHS.status.blocked
|
|
179
182
|
: status === 'pending' ? GLYPHS.status.idle
|
|
180
183
|
: TOOL_STATUS.SPINNER_FRAMES[(frameIndex ?? 0) % TOOL_STATUS.SPINNER_FRAMES.length];
|
|
181
184
|
const iconColor = status === 'done' ? t.chrome.good
|
|
182
185
|
: status === 'error' ? t.chrome.bad
|
|
186
|
+
: status === 'cancelled' ? t.chrome.warn
|
|
183
187
|
: '244';
|
|
184
188
|
const rightText = (() => {
|
|
185
189
|
if (durationMs !== undefined && status === 'done') {
|
|
@@ -204,7 +208,7 @@ export function renderToolCallBlock(
|
|
|
204
208
|
let col: number = leftStart;
|
|
205
209
|
|
|
206
210
|
if (col < leftEndExclusive) {
|
|
207
|
-
line[col] = createStyledCell(icon, { fg: iconColor, bold: status === 'done' || status === 'error' });
|
|
211
|
+
line[col] = createStyledCell(icon, { fg: iconColor, bold: status === 'done' || status === 'error' || status === 'cancelled' });
|
|
208
212
|
}
|
|
209
213
|
col += 2; // icon + space
|
|
210
214
|
|
|
@@ -215,9 +219,11 @@ export function renderToolCallBlock(
|
|
|
215
219
|
const keyArg = stripDangerousAnsi(extractKeyArg(toolCall));
|
|
216
220
|
const suffixText = status === 'error' && errorMsg
|
|
217
221
|
? `- ${stripDangerousAnsi(errorMsg).slice(0, 40)}`
|
|
218
|
-
: status === '
|
|
219
|
-
?
|
|
220
|
-
: ''
|
|
222
|
+
: status === 'cancelled'
|
|
223
|
+
? '- cancelled'
|
|
224
|
+
: status === 'done' && resultSummary
|
|
225
|
+
? `(${stripDangerousAnsi(resultSummary)})`
|
|
226
|
+
: '';
|
|
221
227
|
const leftBudget = Math.max(0, leftEndExclusive - col);
|
|
222
228
|
const leftSegments = buildLeftSegments(rawName, keyArg, suffixText, leftBudget);
|
|
223
229
|
for (const segment of leftSegments) {
|
|
@@ -10,6 +10,8 @@ import { formatElapsed } from '../utils/format-elapsed.ts';
|
|
|
10
10
|
import { abbreviateCount } from '../utils/format-number.ts';
|
|
11
11
|
import { computeContextUsage } from '../core/context-usage.ts';
|
|
12
12
|
import { permissionModeLabel, permissionModeTone } from '../core/permission-mode.ts';
|
|
13
|
+
import { SLEEP_DISABLED_CHIP } from '../core/power-status.ts';
|
|
14
|
+
import { renderQueuedMessageList, renderMemoryProvenanceChip, type MemoryProvenanceEntry } from './composer-fragments.ts';
|
|
13
15
|
import { calcSessionCost, isModelPriced } from '../export/cost-utils.ts';
|
|
14
16
|
import { buildFooterTip, isAgentActive } from './footer-tips.ts';
|
|
15
17
|
import type { StreamMetrics } from '../core/stream-event-wiring.ts';
|
|
@@ -173,6 +175,16 @@ export class UIFactory {
|
|
|
173
175
|
});
|
|
174
176
|
}
|
|
175
177
|
|
|
178
|
+
/** The mid-turn queue as an editable list — see composer-fragments.ts. */
|
|
179
|
+
public static createQueuedMessageList(width: number, items: readonly { readonly id: string; readonly text: string }[]): Line[] {
|
|
180
|
+
return renderQueuedMessageList(width, items);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** The optional "used N memories" provenance chip — see composer-fragments.ts. */
|
|
184
|
+
public static createMemoryProvenanceChip(width: number, count: number, entries: readonly MemoryProvenanceEntry[], expanded: boolean): Line[] {
|
|
185
|
+
return renderMemoryProvenanceChip(width, count, entries, expanded);
|
|
186
|
+
}
|
|
187
|
+
|
|
176
188
|
public static createFooter(
|
|
177
189
|
width: number,
|
|
178
190
|
prompt: string,
|
|
@@ -205,6 +217,10 @@ export class UIFactory {
|
|
|
205
217
|
// The web surface's reachable URL, when that surface is enabled — a
|
|
206
218
|
// persistent low-priority context segment; undefined suppresses it.
|
|
207
219
|
webSurfaceUrl?: string,
|
|
220
|
+
// True while power.keepAwake holds — renders the always-visible
|
|
221
|
+
// "sleep disabled" chip in the posture block (the danger-mode idiom: a
|
|
222
|
+
// persistent, colored surface indicator, the safety mechanism itself).
|
|
223
|
+
powerKeepAwake?: boolean,
|
|
208
224
|
): Line[] {
|
|
209
225
|
const lines: Line[] = [];
|
|
210
226
|
const promptLines = prompt.split('\n');
|
|
@@ -368,6 +384,10 @@ export class UIFactory {
|
|
|
368
384
|
: t.chrome.warn;
|
|
369
385
|
composerTokens.push({ text: ` risk:${composerPendingRisk} `, fg: riskColor, bold: true });
|
|
370
386
|
}
|
|
387
|
+
// "sleep disabled" chip — always visible while keep-awake holds, in the
|
|
388
|
+
// danger-mode idiom (a persistent warn-tone indicator; the chip is the
|
|
389
|
+
// safety mechanism, so there is no timer and no AC-only variant).
|
|
390
|
+
if (powerKeepAwake) composerTokens.push({ text: ` ${SLEEP_DISABLED_CHIP} `, fg: t.chrome.warn, bold: true });
|
|
371
391
|
if (composerStatus && composerStatus !== 'idle') composerTokens.push({ text: ` state:${composerStatus} `, fg: '244', dim: true });
|
|
372
392
|
if (composerFlags && composerFlags.length > 0) composerTokens.push({ text: ` flags:${composerFlags.join(',')} `, fg: '244', dim: true });
|
|
373
393
|
if (!compact && composerTokens.length > 0) {
|
|
@@ -391,6 +411,18 @@ export class UIFactory {
|
|
|
391
411
|
if (px >= width) break;
|
|
392
412
|
}
|
|
393
413
|
lines.push(postureLine);
|
|
414
|
+
} else if (compact && powerKeepAwake) {
|
|
415
|
+
// The keep-awake chip is a safety indicator — it survives compact height
|
|
416
|
+
// even though the rest of the posture row (mode/ctx/risk/state/flags) is
|
|
417
|
+
// dropped at height < 30. Its own compact-surviving slot.
|
|
418
|
+
const chipLine = createBaseLine();
|
|
419
|
+
let cx = 2;
|
|
420
|
+
for (const ch of ` ${SLEEP_DISABLED_CHIP} `) {
|
|
421
|
+
if (cx >= width) break;
|
|
422
|
+
chipLine[cx] = { char: ch, fg: t.chrome.warn, bg: '', bold: true, dim: false, underline: false, italic: false, strikethrough: false };
|
|
423
|
+
cx += getDisplayWidth(ch);
|
|
424
|
+
}
|
|
425
|
+
lines.push(chipLine);
|
|
394
426
|
}
|
|
395
427
|
const isRecentlyCopied = Date.now() - lastCopyTime < 2000;
|
|
396
428
|
// Token usage line + running ~$ cost estimate (derived from the usage
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
|
|
57
57
|
export type CreateBootstrapCommandContextOptions = {
|
|
58
58
|
configManager: ConfigManager;
|
|
59
|
+
pairingTokens?: import('@pellux/goodvibes-sdk/platform/pairing').PairingTokenManager;
|
|
59
60
|
featureFlagManager?: import('@/runtime/index.ts').FeatureFlagManager;
|
|
60
61
|
providerRegistry: ProviderRegistry;
|
|
61
62
|
conversation: ConversationManager;
|
|
@@ -144,6 +145,7 @@ export function createBootstrapCommandContext(
|
|
|
144
145
|
const {
|
|
145
146
|
providerRegistry,
|
|
146
147
|
configManager,
|
|
148
|
+
pairingTokens,
|
|
147
149
|
featureFlagManager,
|
|
148
150
|
conversation,
|
|
149
151
|
runtime,
|
|
@@ -288,7 +290,7 @@ export function createBootstrapCommandContext(
|
|
|
288
290
|
projectPlanningProjectId,
|
|
289
291
|
workPlanStore,
|
|
290
292
|
}, shellServices);
|
|
291
|
-
const platform = createBootstrapCommandPlatformSection({ configManager, featureFlagManager, voiceProviderRegistry, voiceService, webSearchService, mediaProviders, artifactStore }, shellServices);
|
|
293
|
+
const platform = createBootstrapCommandPlatformSection({ configManager, pairingTokens, featureFlagManager, voiceProviderRegistry, voiceService, webSearchService, mediaProviders, artifactStore }, shellServices);
|
|
292
294
|
const extensions = createBootstrapCommandExtensionsSection({
|
|
293
295
|
toolRegistry,
|
|
294
296
|
mcpRegistry,
|
|
@@ -74,6 +74,7 @@ export interface BootstrapCommandActionOptions {
|
|
|
74
74
|
|
|
75
75
|
export interface BootstrapCommandSectionOptions {
|
|
76
76
|
readonly configManager: ConfigManager;
|
|
77
|
+
readonly pairingTokens?: import('@pellux/goodvibes-sdk/platform/pairing').PairingTokenManager;
|
|
77
78
|
readonly featureFlagManager?: FeatureFlagManager;
|
|
78
79
|
readonly providerRegistry: ProviderRegistry;
|
|
79
80
|
readonly conversation: ConversationManager;
|
|
@@ -396,13 +397,14 @@ export function createBootstrapCommandWorkspaceSection(
|
|
|
396
397
|
export function createBootstrapCommandPlatformSection(
|
|
397
398
|
options: Pick<
|
|
398
399
|
BootstrapCommandSectionOptions,
|
|
399
|
-
'configManager' | 'featureFlagManager' | 'voiceProviderRegistry' | 'voiceService' | 'webSearchService' | 'mediaProviders' | 'artifactStore'
|
|
400
|
+
'configManager' | 'pairingTokens' | 'featureFlagManager' | 'voiceProviderRegistry' | 'voiceService' | 'webSearchService' | 'mediaProviders' | 'artifactStore'
|
|
400
401
|
>,
|
|
401
402
|
shellServices: BootstrapCommandShellServices,
|
|
402
403
|
): BootstrapCommandPlatformSection {
|
|
403
404
|
return {
|
|
404
405
|
config: getConfigSnapshot(options.configManager),
|
|
405
406
|
configManager: options.configManager,
|
|
407
|
+
...(options.pairingTokens ? { pairingTokens: options.pairingTokens } : {}),
|
|
406
408
|
featureFlagManager: options.featureFlagManager,
|
|
407
409
|
voiceProviderRegistry: options.voiceProviderRegistry,
|
|
408
410
|
voiceService: options.voiceService,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { readExecEnvScrubAllowlist } from '../input/exec-env-scrub-config.ts';
|
|
26
26
|
import { createSandboxExecAsk, sandboxExecAskDepsFromRuntime } from '../permissions/sandbox-exec-gate.ts';
|
|
27
27
|
import { createRuntimeServices, type RuntimeServices } from './services.ts';
|
|
28
|
+
import { createHostPowerSeam } from '@pellux/goodvibes-sdk/platform/power';
|
|
28
29
|
import { runBootMemoryFold } from './memory-fold.ts';
|
|
29
30
|
import { wireCostPricing } from '../export/cost-utils.ts';
|
|
30
31
|
import { createUiRuntimeServices, type UiRuntimeServices } from './ui-services.ts';
|
|
@@ -37,9 +38,7 @@ import { SessionInboundInputPoller, createBootstrapInboundInputPoller } from './
|
|
|
37
38
|
import { trustGatedAsk, type WorkspaceTrustLevel } from './trust/workspace-trust.ts';
|
|
38
39
|
import { syncNotifierQueueIntegrations } from './bootstrap-notifier-sync.ts';
|
|
39
40
|
|
|
40
|
-
//
|
|
41
|
-
// Pre-router buffer
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
41
|
+
// --- Pre-router buffer ---
|
|
43
42
|
|
|
44
43
|
const PRE_ROUTER_BUFFER_MAX = 100;
|
|
45
44
|
|
|
@@ -133,9 +132,7 @@ export interface BootstrapCoreState {
|
|
|
133
132
|
|
|
134
133
|
export type CompanionMessagePayload = Extract<SessionEvent, { type: 'COMPANION_MESSAGE_RECEIVED' }>;
|
|
135
134
|
|
|
136
|
-
//
|
|
137
|
-
// Operator narration of inbound channel events
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
135
|
+
// --- Operator narration of inbound channel events ---
|
|
139
136
|
|
|
140
137
|
/** Narrate an inbound channel event (GitHub, Slack, ntfy, etc.) that triggered an
|
|
141
138
|
* agent turn, via the SystemMessageRouter — null for internal/companion sources. */
|
|
@@ -252,6 +249,9 @@ export async function initializeBootstrapCore(
|
|
|
252
249
|
runtimeStore: store,
|
|
253
250
|
getConversationTitle: () => getConversationTitle(),
|
|
254
251
|
workingDir, homeDirectory,
|
|
252
|
+
// Embedded topology: the in-process runtime IS the daemon and holds its own OS
|
|
253
|
+
// inhibitor, so opt into the real host seam (fork default is non-spawning). See services.ts.
|
|
254
|
+
powerSeam: createHostPowerSeam(),
|
|
255
255
|
});
|
|
256
256
|
await services.workspaceTrustManager.load(); // settle any already-persisted trust decision before any tool runs
|
|
257
257
|
const providerRegistry = services.providerRegistry;
|
|
@@ -206,6 +206,12 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
206
206
|
runtimeStore,
|
|
207
207
|
openPanel: (panelId: string) => { services.panelManager.open(panelId); },
|
|
208
208
|
knowledgeApi,
|
|
209
|
+
// Fleet acts (pick / conflict / discard) surface receipts through the command
|
|
210
|
+
// context's print and reuse its one-key jump affordance — both late-bound
|
|
211
|
+
// (the command context is assigned to commandContextRef after bootstrap, and
|
|
212
|
+
// main.ts patches armFixSessionAttach onto it).
|
|
213
|
+
fleetActsNotify: (message: string) => { commandContextRef?.print?.(message); requestRender(); },
|
|
214
|
+
armFixSessionAttach: (sessionId: string) => { commandContextRef?.armFixSessionAttach?.(sessionId); },
|
|
209
215
|
});
|
|
210
216
|
services.panelManager.prewarmRegistered();
|
|
211
217
|
|
|
@@ -239,6 +245,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
239
245
|
|
|
240
246
|
const commandContext: CommandContext = createBootstrapCommandContext({
|
|
241
247
|
configManager,
|
|
248
|
+
pairingTokens: services.pairingTokens,
|
|
242
249
|
providerRegistry: services.providerRegistry,
|
|
243
250
|
conversation,
|
|
244
251
|
runtime,
|
|
@@ -14,19 +14,36 @@
|
|
|
14
14
|
* - One credential chain (env -> secrets -> subscription): boot applies
|
|
15
15
|
* secrets-backed keys; every secrets write/delete re-registers builtins
|
|
16
16
|
* LIVE (no restart) — badges/picker/chat read the same instances.
|
|
17
|
+
* - Start-time retention janitor: one best-effort append-only sweep over
|
|
18
|
+
* EVERY root the composition knows (working dir, surface root, home,
|
|
19
|
+
* logDir, telemetryDir) — omitting logDir/telemetryDir/home would silently
|
|
20
|
+
* skip the activity-log, telemetry-ledger, and recovery-snapshot stores.
|
|
21
|
+
* - Live config-file watch: external edits to the settings file apply through
|
|
22
|
+
* the same subscribe() pipeline an in-process set() uses — no restart. The
|
|
23
|
+
* underlying watchers are unref'd, so this never pins the event loop.
|
|
17
24
|
*/
|
|
18
25
|
import { join } from 'node:path';
|
|
26
|
+
import { operations } from '@pellux/goodvibes-sdk/platform/runtime';
|
|
19
27
|
import { resolveMemoryVectorDbPath } from '@pellux/goodvibes-sdk/platform/state';
|
|
20
28
|
import { StoreSnapshotScheduler } from '@pellux/goodvibes-sdk/platform/state/store-snapshots';
|
|
21
29
|
import { UserPermissionRuleStore } from '@pellux/goodvibes-sdk/platform/permissions';
|
|
22
30
|
import { logger, summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
23
31
|
|
|
24
32
|
export interface DurabilityServicesInput {
|
|
25
|
-
readonly configManager: {
|
|
33
|
+
readonly configManager: {
|
|
34
|
+
getControlPlaneConfigDir(): string;
|
|
35
|
+
get(key: never): unknown;
|
|
36
|
+
watchConfigFiles(options?: { intervalMs?: number }): () => void;
|
|
37
|
+
};
|
|
26
38
|
readonly secretsManager: { onDidChange(listener: (key: string) => void): () => void };
|
|
27
39
|
readonly providerRegistry: { refreshProviderCredentials(): Promise<void> };
|
|
28
40
|
readonly memoryDbPath: string;
|
|
29
41
|
readonly codeIndexDbPath: string;
|
|
42
|
+
/** Retention-sweep roots (mirrors the SDK's own createRuntimeServices roots). */
|
|
43
|
+
readonly workingDirectory: string;
|
|
44
|
+
readonly surfaceRoot: string;
|
|
45
|
+
readonly homeDirectory: string;
|
|
46
|
+
readonly shellPaths: { resolveUserPath(...segments: string[]): string };
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
export interface DurabilityServices {
|
|
@@ -48,6 +65,24 @@ export function createDurabilityServices(input: DurabilityServicesInput): Durabi
|
|
|
48
65
|
});
|
|
49
66
|
storeSnapshotScheduler.start();
|
|
50
67
|
|
|
68
|
+
// Start-time retention janitor: pass EVERY root the composition knows so the
|
|
69
|
+
// activity-log, telemetry-ledger, and recovery-snapshot stores are all swept.
|
|
70
|
+
// Best-effort — a retention failure never takes startup down (the sweep
|
|
71
|
+
// swallows its own errors).
|
|
72
|
+
operations.runStartupAppendOnlySweep(
|
|
73
|
+
{
|
|
74
|
+
workingDirectory: input.workingDirectory,
|
|
75
|
+
surfaceRoot: input.surfaceRoot,
|
|
76
|
+
homeDirectory: input.homeDirectory,
|
|
77
|
+
logDir: input.shellPaths.resolveUserPath('logs'),
|
|
78
|
+
telemetryDir: input.shellPaths.resolveUserPath('telemetry'),
|
|
79
|
+
},
|
|
80
|
+
(k: string) => configManager.get(k as never),
|
|
81
|
+
);
|
|
82
|
+
// External config edits apply LIVE through the same subscribe() pipeline an
|
|
83
|
+
// in-process set() uses; the underlying watchers are unref'd.
|
|
84
|
+
configManager.watchConfigFiles();
|
|
85
|
+
|
|
51
86
|
const userPermissionRuleStore = new UserPermissionRuleStore(join(configManager.getControlPlaneConfigDir(), 'permission-rules.json'));
|
|
52
87
|
void userPermissionRuleStore.init().catch((error) => logger.warn('user permission rule store init failed; asks will prompt', { error: summarizeError(error) }));
|
|
53
88
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fleet-services.ts — the shared, archive-aware fleet registry construction,
|
|
3
|
+
* lifted out of services.ts so the composition root stays under the file-size
|
|
4
|
+
* cap. Mirrors the sibling create*Services helpers (durability, code-index,
|
|
5
|
+
* workstream): a single dependency-injected call that services.ts makes once.
|
|
6
|
+
*
|
|
7
|
+
* Also owns the daemon-side observed foreign-agent source: when
|
|
8
|
+
* observeExternalAgents is on (the standalone daemon), externally-launched
|
|
9
|
+
* coding-agent sessions observed read-only on the host fold in as
|
|
10
|
+
* 'observed-external' rows (visibility + steer; never counted against
|
|
11
|
+
* fleet.maxSize, never stopped). The interactive process leaves it off and
|
|
12
|
+
* reads the daemon's snapshot, so it never double-scans. Absence ⇒ a quiet
|
|
13
|
+
* empty set. Mirrors the SDK's own createRuntimeServices wiring.
|
|
14
|
+
*/
|
|
15
|
+
import { computeUsageCostUsd, type ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
|
|
16
|
+
import { type ArchivableProcessRegistry } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
|
|
17
|
+
import { ObservedAgentSource } from '@pellux/goodvibes-sdk/platform/runtime/fleet/observed';
|
|
18
|
+
import { createArchivableFleetRegistry, type ProcessRegistryDeps } from '@pellux/goodvibes-terminal-shell';
|
|
19
|
+
|
|
20
|
+
export interface FleetServicesDeps
|
|
21
|
+
extends Omit<ProcessRegistryDeps, 'observedAgents' | 'priceUsage'> {
|
|
22
|
+
/** Turns on the daemon-side observed foreign-agent source (daemon only). */
|
|
23
|
+
readonly observeExternalAgents?: boolean | undefined;
|
|
24
|
+
/** Backs the honest pricing resolver folded into priceUsage below. */
|
|
25
|
+
readonly providerRegistry: Pick<ProviderRegistry, 'resolveModelPricing'>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function createFleetServices(
|
|
29
|
+
deps: FleetServicesDeps,
|
|
30
|
+
): { processRegistry: ArchivableProcessRegistry } {
|
|
31
|
+
const { observeExternalAgents, providerRegistry, ...registryDeps } = deps;
|
|
32
|
+
const observedAgents = observeExternalAgents ? new ObservedAgentSource() : undefined;
|
|
33
|
+
const processRegistry = createArchivableFleetRegistry({
|
|
34
|
+
...registryDeps,
|
|
35
|
+
observedAgents, // Daemon-side observed foreign-agent rows (undefined in the interactive process)
|
|
36
|
+
// Honest-unpriced through the ONE pricing resolver (manual -> registration -> provider-served
|
|
37
|
+
// -> catalog -> unknown); unknown/subscription yields null, never $0. Mirrors the SDK composition.
|
|
38
|
+
priceUsage: (model, usage) => (model ? computeUsageCostUsd(providerRegistry.resolveModelPricing(model), usage) : null),
|
|
39
|
+
});
|
|
40
|
+
return { processRegistry };
|
|
41
|
+
}
|