@marcoscale98/pi-extensible-workflows 5.19.1-fork.1 → 6.2.1-fork.1
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 +77 -0
- package/README.md +2 -2
- package/dist/src/agent-execution.d.ts +43 -10
- package/dist/src/agent-execution.js +365 -127
- package/dist/src/background-widget.d.ts +0 -1
- package/dist/src/background-widget.js +4 -9
- package/dist/src/decoders.d.ts +2 -3
- package/dist/src/decoders.js +33 -37
- package/dist/src/execution.d.ts +2 -0
- package/dist/src/execution.js +5 -5
- package/dist/src/host-delivery.js +4 -8
- package/dist/src/host-navigator.js +5 -2
- package/dist/src/host-phases.d.ts +1 -0
- package/dist/src/host-recovery.d.ts +6 -6
- package/dist/src/host-recovery.js +14 -22
- package/dist/src/host-runtime.js +56 -5
- package/dist/src/host-view.d.ts +1 -1
- package/dist/src/host-view.js +17 -15
- package/dist/src/host.js +62 -87
- package/dist/src/index.d.ts +1 -2
- package/dist/src/index.js +1523 -1630
- package/dist/src/index.js.map +3 -3
- package/dist/src/paths.d.ts +7 -6
- package/dist/src/paths.js +24 -15
- package/dist/src/persistence.d.ts +1 -0
- package/dist/src/persistence.js +1 -0
- package/dist/src/pi-runtime-runner.js +4 -1
- package/dist/src/registry.d.ts +9 -7
- package/dist/src/registry.js +28 -70
- package/dist/src/runtime/agent-runner.d.ts +2 -0
- package/dist/src/session-lease.d.ts +1 -1
- package/dist/src/session-lease.js +45 -11
- package/dist/src/settings.d.ts +6 -3
- package/dist/src/settings.js +36 -47
- package/dist/src/store.d.ts +3 -1
- package/dist/src/store.js +17 -1
- package/dist/src/trajectory.d.ts +0 -1
- package/dist/src/trajectory.js +2 -3
- package/dist/src/types.d.ts +100 -44
- package/dist/src/types.js +4 -7
- package/dist/src/utils.d.ts +3 -2
- package/dist/src/utils.js +20 -10
- package/dist/src/validation.d.ts +8 -2
- package/dist/src/validation.js +122 -86
- package/dist/starter/index.js +8 -9
- package/dist/starter/index.js.map +1 -1
- package/dist/starter/review-loop.js +8 -7
- package/dist/subagents/index.js +654 -814
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +4 -3
- package/dist/subagents/src/contracts.js +2 -2
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +1 -2
- package/dist/subagents/src/manager.js +49 -70
- package/dist/subagents/src/navigator.js +3 -14
- package/dist/subagents/src/view.d.ts +0 -1
- package/dist/subagents/src/view.js +2 -16
- package/dist/subagents/src/worktree.js +1 -1
- package/dist/trajectory/assets/index.html +8 -8
- package/dist/trajectory/index.js +487 -891
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +8 -8
- package/dist/trajectory/src/index.js +126 -86
- package/dist/trajectory/src/server.js +264 -25
- package/dist/trajectory/src/server.js.map +3 -3
- package/dist/trajectory/src/startup-mutex.d.ts +17 -0
- package/dist/trajectory/src/startup-mutex.js +172 -0
- package/examples/workflow-extension-template/README.md +4 -7
- package/examples/workflow-extension-template/extension.test.mjs +4 -8
- package/examples/workflow-extension-template/index.js +0 -3
- package/package.json +5 -5
- package/skills/pi-extensible-workflows/SKILL.md +10 -8
- package/src/agent-execution.ts +339 -127
- package/src/background-widget.ts +4 -10
- package/src/decoders.ts +31 -37
- package/src/execution.ts +5 -5
- package/src/host-delivery.ts +4 -8
- package/src/host-navigator.ts +4 -2
- package/src/host-phases.ts +1 -1
- package/src/host-recovery.ts +20 -26
- package/src/host-runtime.ts +44 -6
- package/src/host-view.ts +18 -11
- package/src/host.ts +62 -113
- package/src/index.ts +1 -2
- package/src/paths.ts +18 -12
- package/src/persistence.ts +1 -0
- package/src/pi-runtime-runner.ts +3 -1
- package/src/registry.ts +27 -63
- package/src/runtime/agent-runner.ts +2 -0
- package/src/session-lease.ts +34 -6
- package/src/settings.ts +35 -47
- package/src/store.ts +18 -2
- package/src/trajectory.ts +2 -4
- package/src/types.ts +71 -30
- package/src/utils.ts +18 -9
- package/src/validation.ts +102 -79
- package/starter/index.ts +0 -1
- package/starter/review-loop.ts +8 -7
- package/starter/roles/developer.md +0 -1
- package/starter/roles/oracle.md +0 -1
- package/starter/roles/researcher.md +0 -1
- package/starter/roles/reviewer.md +0 -1
- package/starter/roles/scout.md +0 -1
- package/subagents/README.md +8 -14
- package/subagents/src/contracts.ts +3 -4
- package/subagents/src/index.ts +2 -3
- package/subagents/src/manager.ts +45 -125
- package/subagents/src/navigator.ts +4 -13
- package/subagents/src/view.ts +4 -14
- package/subagents/src/worktree.ts +1 -1
- package/trajectory/src/assets/index.html +8 -8
- package/trajectory/src/index.ts +106 -82
- package/trajectory/src/server.ts +44 -8
- package/trajectory/src/startup-mutex.ts +127 -0
- package/dist/src/roles.d.ts +0 -70
- package/dist/src/roles.js +0 -326
- package/dist/starter/roles/developer.md +0 -6
- package/dist/starter/roles/oracle.md +0 -18
- package/dist/starter/roles/researcher.md +0 -20
- package/dist/starter/roles/reviewer.md +0 -19
- package/dist/starter/roles/scout.md +0 -18
- package/examples/workflow-extension-template/roles/reviewer.md +0 -4
- package/src/roles.ts +0 -327
package/src/host-runtime.ts
CHANGED
|
@@ -2,10 +2,11 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import { type AgentProviderFailure, type AgentProviderRecovery, WorkflowAgentExecutor } from "./agent-execution.js";
|
|
3
3
|
import { type WorkflowBudgetRuntime } from "./budget.js";
|
|
4
4
|
import { type PersistedRun, type RunStore, type WorktreeReference } from "./persistence.js";
|
|
5
|
-
import {
|
|
5
|
+
import { FUNCTION_HANDLE_PREFIX } from "./execution.js";
|
|
6
|
+
import { deepFreeze, errorCode, errorText, fail, isWorkflowErrorCode, jsonDigest, jsonValue, object } from "./utils.js";
|
|
6
7
|
import { validateAgentOptions, validateShellOptions, workflowPrompt } from "./validation.js";
|
|
7
8
|
import { type WorkflowRegistryApi } from "./registry.js";
|
|
8
|
-
import { HARD_TERMINAL_RUN_STATES, WORKFLOW_AGENT_STATE_CHANGED_EVENT, WORKFLOW_BUDGET_EVENT, WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENT, WORKFLOW_PHASE_CHANGED_EVENT, WORKFLOW_RUN_COMPLETED_EVENT, WORKFLOW_RUN_FAILED_EVENT, WORKFLOW_RUN_RESUMED_EVENT, WORKFLOW_RUN_STARTED_EVENT, WORKFLOW_RUN_STATE_CHANGED_EVENT, WORKFLOW_WORKTREE_CREATED_EVENT, WorkflowError, type AgentOptions, type AgentRecord, type BudgetEvent, type FunctionIdentity, type JsonValue, type ModelSpec, type ParallelResult, type ParallelTasks, type PipelineItems, type PipelineResult, type PipelineStages, type RunState, type WorkflowBridge, type WorkflowCheckpointState, type WorkflowErrorShape, type WorkflowEventBase, type WorkflowExecution, type WorkflowExtensionSettings, type WorkflowFunctionContext, type WorkflowMetadata, type WorkflowRunContext, type WorkflowWorktreeCallback, type WorkflowWorktreeReference } from "./types.js";
|
|
9
|
+
import { HARD_TERMINAL_RUN_STATES, WORKFLOW_AGENT_STATE_CHANGED_EVENT, WORKFLOW_BUDGET_EVENT, WORKFLOW_CHECKPOINT_STATE_CHANGED_EVENT, WORKFLOW_PHASE_CHANGED_EVENT, WORKFLOW_RUN_COMPLETED_EVENT, WORKFLOW_RUN_FAILED_EVENT, WORKFLOW_RUN_RESUMED_EVENT, WORKFLOW_RUN_STARTED_EVENT, WORKFLOW_RUN_STATE_CHANGED_EVENT, WORKFLOW_WORKTREE_CREATED_EVENT, WorkflowError, type AgentHandle, type AgentOptions, type AgentRecord, type BudgetEvent, type FunctionIdentity, type JsonValue, type ModelSpec, type ParallelResult, type ParallelTasks, type PipelineItems, type PipelineResult, type PipelineStages, type RunState, type WorkflowBridge, type WorkflowCheckpointState, type WorkflowErrorShape, type WorkflowEventBase, type WorkflowExecution, type WorkflowExtensionSettings, type WorkflowFunctionContext, type WorkflowMetadata, type WorkflowRunContext, type WorkflowWorktreeCallback, type WorkflowWorktreeReference } from "./types.js";
|
|
9
10
|
import { structuralPath as operationPath } from "./persistence.js";
|
|
10
11
|
|
|
11
12
|
export type WorkflowEventSink = { emit: (name: string, payload: unknown) => unknown };
|
|
@@ -150,7 +151,7 @@ export class WorkflowEventPublisher {
|
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
async agentState(store: RunStore, metadata: WorkflowMetadata, previous: AgentRecord | undefined, agent: AgentRecord): Promise<void> {
|
|
153
|
-
await this.#publish(store, metadata, WORKFLOW_AGENT_STATE_CHANGED_EVENT, { agentId: agent.id, displayLabel: agent.label ?? agent.name,
|
|
154
|
+
await this.#publish(store, metadata, WORKFLOW_AGENT_STATE_CHANGED_EVENT, { agentId: agent.id, displayLabel: agent.label ?? agent.name, structuralPath: [...(agent.structuralPath ?? [])], ...(agent.parentId ? { parentId: agent.parentId } : {}), ...(agent.parentBreadcrumb ? { parentBreadcrumb: agent.parentBreadcrumb } : {}), ...(agent.worktreeOwner ? { worktreeOwner: agent.worktreeOwner } : {}), ...(previous ? { previousState: previous.state } : {}), state: agent.state, attempt: agent.attempts });
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
async agentStates(store: RunStore, metadata: WorkflowMetadata, previous: readonly AgentRecord[], current: readonly AgentRecord[]): Promise<void> {
|
|
@@ -288,6 +289,7 @@ function functionBreadcrumb(name: string, occurrence: number): string { return o
|
|
|
288
289
|
|
|
289
290
|
export function withWorkflowFunctions(bridge: WorkflowBridge, store: RunStore, runContext: Readonly<WorkflowRunContext>, registry: WorkflowRegistryApi, settings: Readonly<WorkflowExtensionSettings> = Object.freeze({})): WorkflowBridge {
|
|
290
291
|
const functionAgentOccurrences = new Map<string, number>();
|
|
292
|
+
const functionHandles = new Set<string>();
|
|
291
293
|
const functionShellOccurrences = new Map<string, number>();
|
|
292
294
|
const functionInvokeOccurrences = new Map<string, number>();
|
|
293
295
|
const functionInvokeBreadcrumbOccurrences = new Map<string, number>();
|
|
@@ -309,6 +311,7 @@ export function withWorkflowFunctions(bridge: WorkflowBridge, store: RunStore, r
|
|
|
309
311
|
// Own rejection immediately; Promise.all below still propagates it after the function completes.
|
|
310
312
|
void sideEffect.catch(() => undefined);
|
|
311
313
|
};
|
|
314
|
+
let closed = false;
|
|
312
315
|
const parentBreadcrumb = breadcrumb ?? functionBreadcrumb(name, identity.occurrence);
|
|
313
316
|
const context: WorkflowFunctionContext = {
|
|
314
317
|
settings: deepFreeze(structuredClone(settings)),
|
|
@@ -328,7 +331,7 @@ export function withWorkflowFunctions(bridge: WorkflowBridge, store: RunStore, r
|
|
|
328
331
|
const nestedIdentity: FunctionIdentity = { path: nestedPath, structuralPath: [...inherited], occurrence, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) };
|
|
329
332
|
return invokeFunction(targetName, targetInput, signal, nestedIdentity, `${parentBreadcrumb} > ${functionBreadcrumb(breadcrumbName, breadcrumbOccurrence)}`);
|
|
330
333
|
},
|
|
331
|
-
agent: async (prompt: string, options?: Readonly<AgentOptions>) => {
|
|
334
|
+
agent: Object.assign(async (prompt: string, options?: Readonly<AgentOptions>) => {
|
|
332
335
|
if (!bridge.agent || typeof prompt !== "string") fail("AGENT_FAILED", "No agent bridge is available");
|
|
333
336
|
const validatedOptions = validateAgentOptions(options === undefined ? {} : options);
|
|
334
337
|
const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
|
|
@@ -337,7 +340,42 @@ export function withWorkflowFunctions(bridge: WorkflowBridge, store: RunStore, r
|
|
|
337
340
|
const occurrence = (functionAgentOccurrences.get(key) ?? 0) + 1;
|
|
338
341
|
functionAgentOccurrences.set(key, occurrence);
|
|
339
342
|
return bridge.agent(prompt, validatedOptions, signal, { structuralPath: [...inherited], callSite: `function:${path}`, occurrence, parentBreadcrumb, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
|
|
340
|
-
},
|
|
343
|
+
}, {
|
|
344
|
+
create: (...created: readonly unknown[]): AgentHandle => {
|
|
345
|
+
if (created.length !== 1 || !object(created[0])) fail("INVALID_METADATA", "agent.create requires one options object");
|
|
346
|
+
const { name, ...rest } = created[0];
|
|
347
|
+
if (typeof name !== "string" || !name.trim()) fail("INVALID_METADATA", "agent.create requires a stable explicit name");
|
|
348
|
+
const config = deepFreeze(structuredClone(validateAgentOptions(rest)));
|
|
349
|
+
const scopedStructuralPath = [...(inheritedHostAgentPath.getStore() ?? [])];
|
|
350
|
+
const scopedWorktreeOwner = inheritedHostWorktreeOwner.getStore() ?? worktreeOwner;
|
|
351
|
+
const scope = JSON.stringify([scopedStructuralPath, scopedWorktreeOwner ?? null]);
|
|
352
|
+
// The registered-function path makes repeated and nested invocations distinct; scope separates branches and worktrees. The digest bounds the journal and turn-copy file names.
|
|
353
|
+
const handle = operationPath(FUNCTION_HANDLE_PREFIX, jsonDigest([path, scopedStructuralPath, scopedWorktreeOwner ?? null]), name);
|
|
354
|
+
if (functionHandles.has(handle)) fail("INVALID_METADATA", `Duplicate agent handle name: ${name}`);
|
|
355
|
+
functionHandles.add(handle);
|
|
356
|
+
let turn = 0;
|
|
357
|
+
let inflight = false;
|
|
358
|
+
// Not a nested promise chain: the caller owns the one returned promise, and inflight clears in finally on every outcome.
|
|
359
|
+
const send = async (...sent: readonly unknown[]): Promise<JsonValue> => {
|
|
360
|
+
if (!bridge.agent) fail("AGENT_FAILED", "No agent bridge is available");
|
|
361
|
+
if (sent.length < 1 || sent.length > 2) fail("INVALID_METADATA", "send requires a prompt and optional options");
|
|
362
|
+
if (closed) fail("INVALID_METADATA", `Agent handle outlived the function invocation that created it: ${name}`);
|
|
363
|
+
if (typeof sent[0] !== "string") fail("INVALID_METADATA", "send prompt must be a string");
|
|
364
|
+
const sendOptions = sent[1] === undefined ? {} : sent[1];
|
|
365
|
+
if (!object(sendOptions) || Object.keys(sendOptions).some((key) => key !== "outputSchema" && key !== "timeoutMs")) fail("INVALID_METADATA", "send options must contain only outputSchema and timeoutMs");
|
|
366
|
+
const sendStructuralPath = [...(inheritedHostAgentPath.getStore() ?? [])];
|
|
367
|
+
if (JSON.stringify([sendStructuralPath, inheritedHostWorktreeOwner.getStore() ?? worktreeOwner ?? null]) !== scope) fail("INVALID_METADATA", `Agent handle sends must run in the scope that created the handle: ${name}`);
|
|
368
|
+
if (inflight) fail("INVALID_METADATA", "Concurrent send(...) calls on one agent handle are unsupported; await each turn");
|
|
369
|
+
const validatedOptions = validateAgentOptions({ ...config, ...sendOptions });
|
|
370
|
+
inflight = true;
|
|
371
|
+
turn += 1;
|
|
372
|
+
try {
|
|
373
|
+
return await bridge.agent(sent[0], validatedOptions, signal, { structuralPath: sendStructuralPath, callSite: `function:${path}:handle:${name}`, occurrence: turn, handle, turn, parentBreadcrumb, ...(scopedWorktreeOwner ? { worktreeOwner: scopedWorktreeOwner } : {}) });
|
|
374
|
+
} finally { inflight = false; }
|
|
375
|
+
};
|
|
376
|
+
return Object.freeze({ name, send }) as AgentHandle;
|
|
377
|
+
},
|
|
378
|
+
}),
|
|
341
379
|
shell: async (...args: readonly unknown[]) => {
|
|
342
380
|
if (!bridge.shell) fail("SHELL_FAILED", "No shell bridge is available");
|
|
343
381
|
if (typeof args[0] !== "string") fail("INVALID_METADATA", "shell command must be a string");
|
|
@@ -360,7 +398,7 @@ export function withWorkflowFunctions(bridge: WorkflowBridge, store: RunStore, r
|
|
|
360
398
|
phase: (name: string) => { ownSideEffect(() => bridge.phase?.(name)); },
|
|
361
399
|
log: (message: string) => { ownSideEffect(() => bridge.log?.(message)); },
|
|
362
400
|
};
|
|
363
|
-
const result = await inheritedHostAgentPath.run([...structuralPath], async () => registry.invokeFunction(name, input, context, path, { get: () => replayed?.value, put: (_path, value) => { stored = value; } }));
|
|
401
|
+
const result = await inheritedHostAgentPath.run([...structuralPath], async () => registry.invokeFunction(name, input, context, path, { get: () => replayed?.value, put: (_path, value) => { stored = value; } })).finally(() => { closed = true; });
|
|
364
402
|
await Promise.all(sideEffects);
|
|
365
403
|
if (!replayed) await store.complete(path, stored ?? result);
|
|
366
404
|
return result;
|
package/src/host-view.ts
CHANGED
|
@@ -639,7 +639,6 @@ export interface AgentDetailPresentation {
|
|
|
639
639
|
readonly lastEventAt?: number;
|
|
640
640
|
readonly structuralPath?: readonly string[];
|
|
641
641
|
readonly model?: AgentRecord["model"];
|
|
642
|
-
readonly role?: string;
|
|
643
642
|
readonly tools?: readonly string[];
|
|
644
643
|
readonly attempts?: number;
|
|
645
644
|
readonly startedAt?: number;
|
|
@@ -664,7 +663,6 @@ export function formatAgentDetail(agent: Readonly<AgentDetailPresentation>, styl
|
|
|
664
663
|
`State: ${state(agent.state)}`,
|
|
665
664
|
...(agent.structuralPath?.length ? [`Structural path: ${agent.structuralPath.join(" > ")}`] : []),
|
|
666
665
|
...(model === undefined ? [] : [`Model: ${model}`]),
|
|
667
|
-
`Role: ${agent.role ?? "(none)"}`,
|
|
668
666
|
`Tools: ${tools}`,
|
|
669
667
|
...(agent.attempts !== undefined && Number.isFinite(agent.attempts) && agent.attempts > 1 ? [`Attempts: ${String(agent.attempts)}`] : []),
|
|
670
668
|
...(duration === undefined ? [] : [`Duration: ${formatWorkflowRuntime(duration)}`]),
|
|
@@ -778,11 +776,10 @@ export function formatNavigatorRun(loaded: { run: PersistedRun; snapshot: Readon
|
|
|
778
776
|
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
779
777
|
lines.push(...renderGroupedAgents(run.agents, ({ agent, index, depth }, grouped) => {
|
|
780
778
|
const model = `${agent.model.provider}/${agent.model.model}${agent.model.thinking ? `:${agent.model.thinking}` : ""}`;
|
|
781
|
-
const role = agent.role ? ` role=${agent.role}` : "";
|
|
782
779
|
const tools = ` tools=${agent.tools.join(",") || "(none)"}`;
|
|
783
780
|
const accounting = agent.accounting ? ` input=${String(agent.accounting.input)} output=${String(agent.accounting.output)} cache-read=${String(agent.accounting.cacheRead)} cache-write=${String(agent.accounting.cacheWrite)} cost=${formatCost(agent.accounting.cost) || "$0.00"}` : "";
|
|
784
781
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
785
|
-
const result = [`${indent}#${String(index + 1)} ${grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId)} state=${agent.state} model=${model}${agent.requestedModel ? ` requested=${agent.requestedModel}` : ""}${
|
|
782
|
+
const result = [`${indent}#${String(index + 1)} ${grouped ? agent.label ?? agent.name : agentBreadcrumb(agent, byId)} state=${agent.state} model=${model}${agent.requestedModel ? ` requested=${agent.requestedModel}` : ""}${tools} attempts=${String(agent.attempts)} retries=${String(Math.max(0, agent.attempts - 1))}${accounting}`];
|
|
786
783
|
for (const attempt of agent.attemptDetails ?? []) result.push(`${indent} attempt ${String(attempt.attempt)}${attempt.error ? ` error=${attempt.error.code}: ${attempt.error.message}` : ""}`);
|
|
787
784
|
for (const call of agent.toolCalls ?? []) result.push(`${indent} tool ${call.name} state=${call.state}`);
|
|
788
785
|
const activity = !SETTLED_AGENT_STATES.has(agent.state) ? formatAgentActivity(agent, "⠦", PLAIN_WORKFLOW_PROGRESS_STYLES, now) : "";
|
|
@@ -814,11 +811,17 @@ export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readon
|
|
|
814
811
|
const visit = (id: string): void => {
|
|
815
812
|
const child = tree.byId.get(id);
|
|
816
813
|
if (!child) return;
|
|
817
|
-
if (child.agent) agents.push(child.agent);
|
|
814
|
+
if (child.agent) agents.push(child.agent);
|
|
815
|
+
for (const childId of child.children) visit(childId);
|
|
818
816
|
};
|
|
819
|
-
|
|
817
|
+
visit(node.id);
|
|
820
818
|
return agents;
|
|
821
819
|
};
|
|
820
|
+
const usageLines = (agents: readonly AgentRecord[]): string[] => {
|
|
821
|
+
const spans = agents.flatMap((agent) => agent.startedAt === undefined ? [] : [[agent.startedAt, agent.startedAt + (elapsedDurationMs(agent, now) ?? 0)] as const]);
|
|
822
|
+
const elapsed = spans.length ? formatWorkflowRuntime(Math.max(...spans.map(([, end]) => end)) - Math.min(...spans.map(([start]) => start))) : "";
|
|
823
|
+
return [...formatAgentAccounting(sumAccounting(agents.map((agent) => agent.accounting ?? zeroAccounting()))), ...(elapsed ? [`Elapsed: ${elapsed}`] : [])];
|
|
824
|
+
};
|
|
822
825
|
const nodeStatus = (node: WorkflowPhaseTreeNode): string => phaseStyle(node.state)(node.state);
|
|
823
826
|
const nodeIcon = (node: WorkflowPhaseTreeNode): string => node.children.length ? expanded.has(node.id) ? "▾" : "▸" : node.kind === "agent" ? "•" : node.kind === "shell" ? "◇" : " ";
|
|
824
827
|
const treeLine = (node: WorkflowPhaseTreeNode): string => {
|
|
@@ -838,12 +841,12 @@ export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readon
|
|
|
838
841
|
const selected = node.phase;
|
|
839
842
|
const counts = selected?.counts ?? phaseAgentCounts(agents);
|
|
840
843
|
const hint = node.children.length ? "enter expand/collapse" : "enter phase details";
|
|
841
|
-
return [styles.bold(`Selected phase: ${node.label}`), `Status: ${nodeStatus(node)}`, `agents completed=${String(counts.completed)} running=${String(counts.running)} failed=${String(counts.failed)} cancelled=${String(counts.cancelled)} pending=${String(counts.pending)}`, `Agents: ${String(agents.length)}`, ...(selection.actions ? [] : [styles.muted(hint)])];
|
|
844
|
+
return [styles.bold(`Selected phase: ${node.label}`), `Status: ${nodeStatus(node)}`, `agents completed=${String(counts.completed)} running=${String(counts.running)} failed=${String(counts.failed)} cancelled=${String(counts.cancelled)} pending=${String(counts.pending)}`, `Agents: ${String(agents.length)}`, ...usageLines(agents), ...(selection.actions ? [] : [styles.muted(hint)])];
|
|
842
845
|
}
|
|
843
846
|
if (node.kind === "operation") {
|
|
844
847
|
const states = phaseAgentCounts(agents);
|
|
845
848
|
const hint = node.children.length ? "enter expand/collapse" : "enter phase details";
|
|
846
|
-
return [styles.bold(`Selected operation: ${node.operationPath.join(" > ")}`), `Phase: ${node.phase?.name ?? node.phaseId}`, `Status: ${nodeStatus(node)}`, `agents completed=${String(states.completed)} running=${String(states.running)} failed=${String(states.failed)} cancelled=${String(states.cancelled)} pending=${String(states.pending)}`, `Agents: ${String(agents.length)}`, ...(selection.actions ? [] : [styles.muted(hint)])];
|
|
849
|
+
return [styles.bold(`Selected operation: ${node.operationPath.join(" > ")}`), `Phase: ${node.phase?.name ?? node.phaseId}`, `Status: ${nodeStatus(node)}`, `agents completed=${String(states.completed)} running=${String(states.running)} failed=${String(states.failed)} cancelled=${String(states.cancelled)} pending=${String(states.pending)}`, `Agents: ${String(agents.length)}`, ...usageLines(agents), ...(selection.actions ? [] : [styles.muted(hint)])];
|
|
847
850
|
}
|
|
848
851
|
if (node.kind === "shell") {
|
|
849
852
|
return [styles.bold("Selected shell"), formatShellActivity(node.shellActivity?.active, node.shellActivity?.startedAt, "⠦", styles, now) ?? "shell [running]"];
|
|
@@ -873,7 +876,8 @@ export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readon
|
|
|
873
876
|
return ["", styles.bold(actions.title), ...actions.options.map((option, index) => `${index === actions.index ? "→ " : " "}${index === actions.index ? styles.accent(option) : option}`)];
|
|
874
877
|
};
|
|
875
878
|
const treeRows = [styles.bold("Tree"), ...(visibleNodes.length ? visibleNodes.map(treeLine) : [styles.muted("(empty)")])];
|
|
876
|
-
|
|
879
|
+
const detailOffset = Math.max(0, (selection.scrollTop ?? 0) - lines.flatMap(wrap).length);
|
|
880
|
+
lines.push(...formatNavigatorColumns(treeRows, [...details(selectedNode), ...actionRows()], safeWidth, { ...selection, detailOffset }));
|
|
877
881
|
if (model.unassignedAgents?.length && !tree.nodes.some((node) => node.phaseId === "unassigned")) lines.push(...wrap(styles.muted(`Unassigned agents: ${String(model.unassignedAgents.length)}`)));
|
|
878
882
|
return lines.flatMap((line) => wrap(line));
|
|
879
883
|
}
|
|
@@ -881,12 +885,15 @@ function wrapNavigatorLine(text: string, width: number): string[] {
|
|
|
881
885
|
return truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, Math.max(1, width), 0).visualLines.map((line) => line.trimEnd());
|
|
882
886
|
}
|
|
883
887
|
/** The navigator layout: a sidebar beside details at 80 columns or wider, otherwise one of them (or both stacked). */
|
|
884
|
-
export function formatNavigatorColumns(sidebar: readonly string[], detail: readonly string[], width: number, layout: Readonly<{ treeOnly?: boolean | undefined; detailsOnly?: boolean | undefined }> = {}): string[] {
|
|
888
|
+
export function formatNavigatorColumns(sidebar: readonly string[], detail: readonly string[], width: number, layout: Readonly<{ treeOnly?: boolean | undefined; detailsOnly?: boolean | undefined; detailOffset?: number | undefined }> = {}): string[] {
|
|
885
889
|
const safeWidth = Math.max(1, width);
|
|
886
890
|
if (safeWidth >= 80) {
|
|
887
891
|
const sidebarWidth = Math.min(42, Math.max(24, Math.floor((safeWidth - 3) * 0.38)));
|
|
888
892
|
const left = sidebar.flatMap((line) => wrapNavigatorLine(line, sidebarWidth));
|
|
889
|
-
const
|
|
893
|
+
const wrapped = detail.flatMap((line) => wrapNavigatorLine(line, safeWidth - sidebarWidth - 3));
|
|
894
|
+
// Keep the details in view when the tree is scrolled, without making the block taller than the tree.
|
|
895
|
+
const shift = Math.min(Math.max(0, layout.detailOffset ?? 0), Math.max(0, left.length - wrapped.length));
|
|
896
|
+
const right = [...Array<string>(shift).fill(""), ...wrapped];
|
|
890
897
|
return Array.from({ length: Math.max(left.length, right.length) }, (_, index) => {
|
|
891
898
|
const cell = left[index] ?? "";
|
|
892
899
|
return `${cell}${" ".repeat(Math.max(0, sidebarWidth - visibleWidth(cell)))} | ${right[index] ?? ""}`;
|