-
-
Notifications
You must be signed in to change notification settings - Fork 389
feat(tui): add workflow Navigator and call inspector #152
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8542b0e
b9c81c9
0ece2a2
3aadb3c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,8 @@ | ||||||||||||||||||||||
| import assert from "node:assert/strict"; | ||||||||||||||||||||||
| import { | ||||||||||||||||||||||
| createWorkflowTuiState, | ||||||||||||||||||||||
| reduceWorkflowTuiState, | ||||||||||||||||||||||
| reconcileWorkflowTuiState, | ||||||||||||||||||||||
| renderWorkflowTui, | ||||||||||||||||||||||
| resolveWorkflowTuiWorkspaceRoot, | ||||||||||||||||||||||
| } from "./workflow-tui.js"; | ||||||||||||||||||||||
|
|
@@ -26,23 +29,50 @@ const project: WorkflowProjectView = { | |||||||||||||||||||||
| cancelled: 0, | ||||||||||||||||||||||
| observed: 2, | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| totalTokens: 2_400, | ||||||||||||||||||||||
| phases: [ | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| title: "Planning", | ||||||||||||||||||||||
| status: "completed", | ||||||||||||||||||||||
| calls: [], | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| title: "Implementation", | ||||||||||||||||||||||
| status: "running", | ||||||||||||||||||||||
| calls: [ | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| callIndex: 1, | ||||||||||||||||||||||
| status: "running", | ||||||||||||||||||||||
| provider: "codex", | ||||||||||||||||||||||
| label: "Patch auth", | ||||||||||||||||||||||
| phase: "Implementation", | ||||||||||||||||||||||
| isolation: "worktree", | ||||||||||||||||||||||
| fromCache: false, | ||||||||||||||||||||||
| prompt: "Patch the auth flow", | ||||||||||||||||||||||
| providerSessionId: "session_1", | ||||||||||||||||||||||
| usage: { | ||||||||||||||||||||||
| inputTokens: 1_600, | ||||||||||||||||||||||
| outputTokens: 800, | ||||||||||||||||||||||
| totalTokens: 2_400, | ||||||||||||||||||||||
| state: "partial", | ||||||||||||||||||||||
| updatedAt: "2026-07-26T10:00:02.000Z", | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| updatedAt: "2026-07-26T10:00:02.000Z", | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| ], | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| ], | ||||||||||||||||||||||
| unphasedCalls: [], | ||||||||||||||||||||||
| unphasedCalls: [{ | ||||||||||||||||||||||
| callIndex: 2, | ||||||||||||||||||||||
| status: "completed", | ||||||||||||||||||||||
| provider: "claude", | ||||||||||||||||||||||
| label: "Summarize rollout", | ||||||||||||||||||||||
| isolation: "shared", | ||||||||||||||||||||||
| fromCache: false, | ||||||||||||||||||||||
| prompt: "Summarize the rollout", | ||||||||||||||||||||||
| responseText: "Ready", | ||||||||||||||||||||||
| updatedAt: "2026-07-26T10:00:03.000Z", | ||||||||||||||||||||||
| }], | ||||||||||||||||||||||
| recentActivity: [ | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| seq: 1, | ||||||||||||||||||||||
|
|
@@ -60,12 +90,75 @@ const project: WorkflowProjectView = { | |||||||||||||||||||||
| ], | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| 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); | ||||||||||||||||||||||
| let state = createWorkflowTuiState(project); | ||||||||||||||||||||||
| let rendered = renderWorkflowTui(project, state, 100, 30, { ansi: false }); | ||||||||||||||||||||||
| assert.match(rendered, /Workflows · \/tmp\/project/); | ||||||||||||||||||||||
| assert.match(rendered, /Review auth Implementation/); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| state = reduceWorkflowTuiState(project, state, "return"); | ||||||||||||||||||||||
| assert.equal(state.screen, "workflow"); | ||||||||||||||||||||||
| rendered = renderWorkflowTui(project, state, 100, 30, { ansi: false }); | ||||||||||||||||||||||
| assert.match(rendered, /Workflow › Review auth/); | ||||||||||||||||||||||
| assert.match(rendered, /PHASES\s+│ AGENTS · Implementation/); | ||||||||||||||||||||||
| assert.match(rendered, /Patch auth codex 2\.4k/); | ||||||||||||||||||||||
| assert.match(rendered, /Other 1\/1/); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| state = reduceWorkflowTuiState(project, state, "tab"); | ||||||||||||||||||||||
| state = reduceWorkflowTuiState(project, state, "return"); | ||||||||||||||||||||||
| assert.equal(state.screen, "call"); | ||||||||||||||||||||||
| rendered = renderWorkflowTui(project, state, 72, 30, { | ||||||||||||||||||||||
| ansi: false, | ||||||||||||||||||||||
| activity: [{ | ||||||||||||||||||||||
| runId: "wfr_1", | ||||||||||||||||||||||
| callIndex: 1, | ||||||||||||||||||||||
| seq: 1, | ||||||||||||||||||||||
| kind: "tool", | ||||||||||||||||||||||
| status: "completed", | ||||||||||||||||||||||
| label: "bash", | ||||||||||||||||||||||
| detail: "npm test", | ||||||||||||||||||||||
| createdAt: "2026-07-26T10:00:03.000Z", | ||||||||||||||||||||||
| }], | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| assert.match(rendered, /Workflow › Implementation › Patch auth/); | ||||||||||||||||||||||
| assert.match(rendered, /tool\s+bash · npm test/); | ||||||||||||||||||||||
|
Comment on lines
+122
to
+123
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Assert the activity timestamp, and pin the locale in Line 123 asserts only the kind, label, and detail of the activity row. It does not assert the leading timestamp. Pin the format in 🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| let unphasedState = createWorkflowTuiState(project, "wfr_1"); | ||||||||||||||||||||||
| unphasedState = reduceWorkflowTuiState(project, unphasedState, "down"); | ||||||||||||||||||||||
| assert.equal(unphasedState.screen === "workflow" && unphasedState.phaseIndex, 2); | ||||||||||||||||||||||
| unphasedState = reduceWorkflowTuiState(project, unphasedState, "tab"); | ||||||||||||||||||||||
| unphasedState = reduceWorkflowTuiState(project, unphasedState, "return"); | ||||||||||||||||||||||
| assert.equal(unphasedState.screen, "call"); | ||||||||||||||||||||||
| assert.match(renderWorkflowTui(project, unphasedState, 80, 20, { ansi: false }), /Other › Summarize rollout/); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const reorderedProject = { ...project, runs: [{ ...project.runs[0]!, id: "wfr_new" }, project.runs[0]!] }; | ||||||||||||||||||||||
| const reconciled = reconcileWorkflowTuiState(project, reorderedProject, { | ||||||||||||||||||||||
| screen: "workflow", | ||||||||||||||||||||||
| runIndex: 0, | ||||||||||||||||||||||
| phaseIndex: 1, | ||||||||||||||||||||||
| callIndex: 0, | ||||||||||||||||||||||
| focus: "calls", | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| assert.equal(reconciled.runIndex, 1); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const unsafeProject = { | ||||||||||||||||||||||
| ...project, | ||||||||||||||||||||||
| workspaceRoot: "/tmp/project\u001b]52;c;clipboard\u0007", | ||||||||||||||||||||||
| runs: [{ ...project.runs[0]!, name: "Review\u001b[2Jauth" }], | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
| const safeRender = renderWorkflowTui(unsafeProject, createWorkflowTuiState(unsafeProject), 100, 20, { ansi: false }); | ||||||||||||||||||||||
| assert.doesNotMatch(safeRender, /\u001b|\u0007/); | ||||||||||||||||||||||
| assert.match(safeRender, /\\x1b/); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const narrow = renderWorkflowTui(project, { | ||||||||||||||||||||||
| screen: "workflow", | ||||||||||||||||||||||
| runIndex: 0, | ||||||||||||||||||||||
| phaseIndex: 1, | ||||||||||||||||||||||
| callIndex: 0, | ||||||||||||||||||||||
| focus: "phases", | ||||||||||||||||||||||
| }, 60, 20, { ansi: false }); | ||||||||||||||||||||||
| assert.match(narrow, /PHASES/); | ||||||||||||||||||||||
| assert.doesNotMatch(narrow, /AGENTS · Implementation/); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| assert.equal(resolveWorkflowTuiWorkspaceRoot(process.cwd()), process.cwd()); | ||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win This assertion depends on the ambient environment and can fail in CI.
Two environments break the assertion:
Isolate the environment, or assert the delegation instead of the value. 🛠️ Proposed fix: isolate the environment for the assertion-assert.equal(resolveWorkflowTuiWorkspaceRoot(process.cwd()), process.cwd());
+const previousRoot = process.env.DEVSPACE_WORKSPACE_ROOT;
+const isolated = mkdtempSync(join(tmpdir(), "devspace-tui-"));
+process.env.DEVSPACE_WORKSPACE_ROOT = isolated;
+try {
+ assert.equal(resolveWorkflowTuiWorkspaceRoot(isolated), resolve(isolated));
+} finally {
+ if (previousRoot === undefined) delete process.env.DEVSPACE_WORKSPACE_ROOT;
+ else process.env.DEVSPACE_WORKSPACE_ROOT = previousRoot;
+}Add the supporting imports: import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| console.log("workflow-tui.test.ts: ok"); | ||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Three Navigator behaviors are missing or contradicted here.
devspace workflow tui [run-id]. When the user suppliesrun-id,createWorkflowTuiStatereturns theworkflowscreen directly and skips the list. State the two entry points.renderNavigatoruses that two-pane layout only when the terminal is at least 80 columns wide. Below 80 columns it shows one pane at a time andTabswitches between them. Document the narrow layout.navigatorPhasesappends a synthetic phase namedOtherthat holds every call without a declared phase. The PR objectives describe this grouping, but this section does not. A user who seesOtherbeside the declared phases has no explanation for it.📝 Proposed wording
📝 Committable suggestion
🤖 Prompt for AI Agents