Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dev": "node scripts/dev-server.mjs",
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
"start": "node dist/cli.js serve",
"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-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",
"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",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"keywords": [],
Expand Down
2 changes: 2 additions & 0 deletions skills/dynamic-workflows/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ devspace workflow cancel <runId>
devspace workflow ls
devspace workflow calls <runId>
devspace workflow call <runId> <callIndex>
devspace workflow tui [runId]
```

Named scripts: `.devspace/workflows/<name>.js` or `workflows/<name>.js`.
Expand Down Expand Up @@ -115,6 +116,7 @@ depending on a replayed mutating call.
## When to use CLI vs MCP

- **CLI**: host agent can shell; prefer for long runs + `--follow`.
- **TUI**: `devspace workflow tui` opens a read-only live view for workflows associated with the current working directory.
- **MCP**: ChatGPT plans; call `run_workflow`, then `workflow_status` until terminal. Disconnecting MCP does **not** kill the worker.

## Worked mini-examples
Expand Down
6 changes: 6 additions & 0 deletions src/workflow-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ export async function runWorkflowCommand(
case "call":
await runWorkflowCall(rest, config);
return;
case "tui": {
const { runWorkflowTui } = await import("./workflow-tui.js");
await runWorkflowTui(rest, config);
return;
}
case "__worker":
await runWorkflowWorker(rest, config);
return;
Expand Down Expand Up @@ -96,6 +101,7 @@ export function printWorkflowHelp(): void {
" devspace workflow ls",
" devspace workflow calls <runId>",
" devspace workflow call <runId> <callIndex>",
" devspace workflow tui [runId] # current working directory",
].join("\n"),
);
}
Expand Down
26 changes: 26 additions & 0 deletions src/workflow-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,32 @@ try {
assert.equal(store.getRun(run2.id)?.status, "completed");
assert.equal(store.getRun(run2.id)?.resultJson, JSON.stringify({ ok: 1 }));

const otherProjectRun = store.createRun({
name: "other-project",
source: "inline",
scriptPath: join(root, "other.js"),
scriptHash: "other",
workspaceRoot: join(root, "other-project"),
});
assert.deepEqual(
store
.listRunsForWorkspace(join(root, "project"))
.map((entry) => entry.id)
.sort(),
[run.id, run2.id].sort(),
);
assert.deepEqual(
store
.listRunsForWorkspace(join(root, "project"), { statuses: ["completed"] })
.map((entry) => entry.id),
[run2.id],
);
assert.equal(
store.listRunsForWorkspace(join(root, "other-project"))[0]?.id,
otherProjectRun.id,
);
assert.deepEqual(store.listEvents(run.id, 2).map((event) => event.seq), [2, 3]);

// Reap: stale heartbeat + dead pid (force heartbeat via shared sqlite handle)
const run3 = store.createRun({
name: "stale",
Expand Down
49 changes: 49 additions & 0 deletions src/workflow-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,40 @@ export class WorkflowStore {
return rows.map(rowToRun);
}

listRunsForWorkspace(
workspaceRoot: string,
options: {
statuses?: WorkflowRunStatus[];
limit?: number;
} = {},
): WorkflowRunRecord[] {
const root = resolve(workspaceRoot);
const limit = Math.max(1, Math.min(options.limit ?? 50, 500));
const statuses = options.statuses?.filter((status, index, values) =>
values.indexOf(status) === index,
);

if (!statuses?.length) {
const rows = this.database.sqlite
.prepare(
"select * from workflow_runs where workspace_root = ? order by updated_at desc limit ?",
)
.all(root, limit) as WorkflowRunRow[];
return rows.map(rowToRun);
}

const placeholders = statuses.map(() => "?").join(", ");
const rows = this.database.sqlite
.prepare(
`select * from workflow_runs
where workspace_root = ? and status in (${placeholders})
order by updated_at desc
limit ?`,
)
.all(root, ...statuses, limit) as WorkflowRunRow[];
return rows.map(rowToRun);
}

/**
* Atomically claim a starting run for the worker.
* Returns undefined if the run is missing or not claimable.
Expand Down Expand Up @@ -570,6 +604,21 @@ export class WorkflowStore {
};
}

listEvents(runId: string, limit = 100): WorkflowEventRecord[] {
const capped = Math.max(1, Math.min(limit, WORKFLOW_LIMITS.eventDrainMax));
const rows = this.database.sqlite
.prepare(
`select * from (
select * from workflow_events
where run_id = ?
order by seq desc
limit ?
) order by seq asc`,
)
.all(runId, capped) as WorkflowEventRow[];
return rows.map(rowToEvent);
}

beginAgentCall(input: BeginAgentCallInput): WorkflowAgentCallRecord {
const now = isoNow();
const isolation: AgentIsolationMode = input.isolation === "worktree" ? "worktree" : "shared";
Expand Down
71 changes: 71 additions & 0 deletions src/workflow-tui.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import assert from "node:assert/strict";
import {
renderWorkflowTui,
resolveWorkflowTuiWorkspaceRoot,
} from "./workflow-tui.js";
import type { WorkflowProjectView } from "./workflow-view.js";

const project: WorkflowProjectView = {
workspaceRoot: "/tmp/project",
version: "1",
runs: [
{
id: "wfr_1",
name: "Review auth",
status: "running",
source: "named",
scriptPath: "/tmp/review.js",
scriptHash: "abc",
workspaceRoot: "/tmp/project",
currentPhase: "Implementation",
calls: {
running: 1,
completed: 1,
cached: 0,
failed: 0,
cancelled: 0,
observed: 2,
},
phases: [
{
title: "Implementation",
calls: [
{
callIndex: 1,
status: "running",
provider: "codex",
label: "Patch auth",
isolation: "worktree",
fromCache: false,
updatedAt: "2026-07-26T10:00:02.000Z",
},
],
},
],
unphasedCalls: [],
recentActivity: [
{
seq: 1,
type: "log",
detail: "Running tests",
createdAt: "2026-07-26T10:00:03.000Z",
},
],
latestEventSeq: 1,
version: "v1",
createdAt: "2026-07-26T10:00:00.000Z",
startedAt: "2026-07-26T10:00:00.000Z",
updatedAt: "2026-07-26T10:00:03.000Z",
},
],
};

const rendered = renderWorkflowTui(project, 0, 100, 30, { ansi: false });
assert.match(rendered, /DevSpace workflows · \/tmp\/project/);
assert.match(rendered, /Review auth · Implementation/);
assert.match(rendered, /Patch auth codex · worktree/);
assert.match(rendered, /Running tests/);
assert.match(rendered, /refreshes automatically/);
assert.equal(resolveWorkflowTuiWorkspaceRoot("./test-project").endsWith("test-project"), true);

console.log("workflow-tui.test.ts: ok");
Loading
Loading