Skip to content

Commit dbe26f7

Browse files
committed
feat(workflow): add UI snapshot projections
1 parent 5ee0022 commit dbe26f7

3 files changed

Lines changed: 207 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"dev": "node scripts/dev-server.mjs",
2929
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
3030
"start": "node dist/cli.js serve",
31-
"test": "tsx src/config.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/review-checkpoints.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts && tsx src/workflow-contracts.test.ts && tsx src/workflow-errors.test.ts && tsx src/workflow-types.test.ts && tsx src/workflow-store.test.ts && tsx src/workflow-view.test.ts && tsx src/workflow-tui.test.ts && tsx src/workflow-script.test.ts && tsx src/workflow-sandbox.test.ts && tsx src/workflow-engine.test.ts && tsx src/workflow-files.test.ts && tsx src/workflow-replay.test.ts && tsx src/workflow-schema.test.ts",
31+
"test": "tsx src/config.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/review-checkpoints.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts && tsx src/workflow-contracts.test.ts && tsx src/workflow-errors.test.ts && tsx src/workflow-types.test.ts && tsx src/workflow-store.test.ts && tsx src/workflow-view.test.ts && tsx src/workflow-ui.test.ts && tsx src/workflow-tui.test.ts && tsx src/workflow-script.test.ts && tsx src/workflow-sandbox.test.ts && tsx src/workflow-engine.test.ts && tsx src/workflow-files.test.ts && tsx src/workflow-replay.test.ts && tsx src/workflow-schema.test.ts",
3232
"typecheck": "tsc -p tsconfig.json --noEmit"
3333
},
3434
"keywords": [],

src/workflow-ui.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import assert from "node:assert/strict";
2+
import { mkdtempSync, rmSync } from "node:fs";
3+
import { tmpdir } from "node:os";
4+
import { join } from "node:path";
5+
import { WorkflowStore } from "./workflow-store.js";
6+
import {
7+
loadActiveWorkflowSummaries,
8+
loadWorkflowUiCallDetail,
9+
loadWorkflowUiProject,
10+
loadWorkflowUiRun,
11+
} from "./workflow-ui.js";
12+
13+
const root = mkdtempSync(join(tmpdir(), "devspace-workflow-ui-test-"));
14+
const store = new WorkflowStore(root);
15+
16+
try {
17+
const workspaceRoot = join(root, "project");
18+
const run = store.createRun({
19+
name: "UI run",
20+
source: "named",
21+
scriptPath: join(root, "run.js"),
22+
scriptHash: "abc",
23+
workspaceRoot,
24+
});
25+
store.claimRun(run.id, process.pid);
26+
store.appendEvent({
27+
runId: run.id,
28+
type: "phase_started",
29+
phase: "Review",
30+
data: { title: "Review" },
31+
});
32+
store.beginAgentCall({
33+
runId: run.id,
34+
callIndex: 0,
35+
cacheKey: "key",
36+
prompt: "Review auth",
37+
schemaJson: JSON.stringify({ type: "object" }),
38+
provider: "codex",
39+
label: "Auth review",
40+
phase: "Review",
41+
isolation: "worktree",
42+
worktreePath: join(root, "wt"),
43+
});
44+
45+
const summaries = loadActiveWorkflowSummaries(store, workspaceRoot);
46+
assert.equal(summaries[0]?.id, run.id);
47+
assert.equal(summaries[0]?.currentPhase, "Review");
48+
assert.equal(summaries[0]?.calls.running, 1);
49+
50+
const project = loadWorkflowUiProject(store, workspaceRoot);
51+
assert.equal(project.runs[0]?.phases[0]?.title, "Review");
52+
assert.equal(loadWorkflowUiRun(store, run.id)?.name, "UI run");
53+
54+
const detail = loadWorkflowUiCallDetail(store, run.id, 0);
55+
assert.equal(detail?.prompt, "Review auth");
56+
assert.deepEqual(detail?.schema, { type: "object" });
57+
assert.equal(detail?.worktreePath, join(root, "wt"));
58+
assert.equal(loadWorkflowUiCallDetail(store, run.id, 99), undefined);
59+
} finally {
60+
store.close();
61+
rmSync(root, { recursive: true, force: true });
62+
}
63+
64+
console.log("workflow-ui.test.ts: ok");

src/workflow-ui.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import type { JsonValue } from "./json-types.js";
2+
import type { WorkflowStore } from "./workflow-store.js";
3+
import {
4+
ACTIVE_WORKFLOW_STATUSES,
5+
buildWorkflowRunView,
6+
loadWorkflowProjectView,
7+
type WorkflowCallCounts,
8+
type WorkflowProjectView,
9+
type WorkflowRunView,
10+
} from "./workflow-view.js";
11+
12+
export interface WorkflowRunSummaryView {
13+
id: string;
14+
name: string;
15+
status: WorkflowRunView["status"];
16+
currentPhase?: string;
17+
calls: WorkflowCallCounts;
18+
updatedAt: string;
19+
}
20+
21+
export interface WorkflowCallDetailView {
22+
runId: string;
23+
callIndex: number;
24+
status: string;
25+
provider: string;
26+
model?: string;
27+
effort?: string;
28+
label?: string;
29+
phase?: string;
30+
prompt: string;
31+
schema?: JsonValue | string;
32+
responseText?: string;
33+
structured?: JsonValue | string;
34+
error?: string;
35+
errorKind?: string;
36+
providerSessionId?: string;
37+
isolation: "shared" | "worktree";
38+
worktreePath?: string;
39+
dirty?: boolean;
40+
fromCache: boolean;
41+
replayMatch?: "same_index" | "compatible_key";
42+
replayedFromRunId?: string;
43+
replayedFromCallIndex?: number;
44+
replayReason?: string;
45+
createdAt: string;
46+
startedAt?: string;
47+
completedAt?: string;
48+
updatedAt: string;
49+
}
50+
51+
export function loadActiveWorkflowSummaries(
52+
store: WorkflowStore,
53+
workspaceRoot: string,
54+
): WorkflowRunSummaryView[] {
55+
return loadWorkflowProjectView(store, workspaceRoot, {
56+
statuses: [...ACTIVE_WORKFLOW_STATUSES],
57+
limit: 50,
58+
eventLimit: 50,
59+
}).runs.map(summarizeWorkflowRun);
60+
}
61+
62+
export function loadWorkflowUiProject(
63+
store: WorkflowStore,
64+
workspaceRoot: string,
65+
): WorkflowProjectView {
66+
return loadWorkflowProjectView(store, workspaceRoot, {
67+
statuses: [...ACTIVE_WORKFLOW_STATUSES],
68+
limit: 50,
69+
eventLimit: 100,
70+
});
71+
}
72+
73+
export function loadWorkflowUiRun(
74+
store: WorkflowStore,
75+
runId: string,
76+
): WorkflowRunView | undefined {
77+
const run = store.getRun(runId);
78+
if (!run) return undefined;
79+
return buildWorkflowRunView(
80+
run,
81+
store.listAgentCalls(run.id),
82+
store.listEvents(run.id, 100),
83+
);
84+
}
85+
86+
export function loadWorkflowUiCallDetail(
87+
store: WorkflowStore,
88+
runId: string,
89+
callIndex: number,
90+
): WorkflowCallDetailView | undefined {
91+
const call = store.getAgentCall(runId, callIndex);
92+
if (!call) return undefined;
93+
return {
94+
runId,
95+
callIndex,
96+
status: call.status,
97+
provider: call.provider,
98+
model: call.model,
99+
effort: call.effort,
100+
label: call.label,
101+
phase: call.phase,
102+
prompt: call.prompt,
103+
schema: parseStoredJson(call.schemaJson),
104+
responseText: call.responseText,
105+
structured: parseStoredJson(call.structuredJson),
106+
error: call.error,
107+
errorKind: call.errorKind,
108+
providerSessionId: call.providerSessionId,
109+
isolation: call.isolation,
110+
worktreePath: call.worktreePath,
111+
dirty: call.dirty,
112+
fromCache: call.fromCache,
113+
replayMatch: call.replayMatch,
114+
replayedFromRunId: call.replayedFromRunId,
115+
replayedFromCallIndex: call.replayedFromCallIndex,
116+
replayReason: call.replayReason,
117+
createdAt: call.createdAt,
118+
startedAt: call.startedAt,
119+
completedAt: call.completedAt,
120+
updatedAt: call.updatedAt,
121+
};
122+
}
123+
124+
export function summarizeWorkflowRun(run: WorkflowRunView): WorkflowRunSummaryView {
125+
return {
126+
id: run.id,
127+
name: run.name,
128+
status: run.status,
129+
currentPhase: run.currentPhase,
130+
calls: run.calls,
131+
updatedAt: run.updatedAt,
132+
};
133+
}
134+
135+
function parseStoredJson(value: string | undefined): JsonValue | string | undefined {
136+
if (value === undefined) return undefined;
137+
try {
138+
return JSON.parse(value) as JsonValue;
139+
} catch {
140+
return value;
141+
}
142+
}

0 commit comments

Comments
 (0)