Skip to content

feat(app): cell execution state indicators + flush partial stdout#103

Open
hamelsmu wants to merge 1 commit intomainfrom
feat/cell-exec-state-and-stdout-flush
Open

feat(app): cell execution state indicators + flush partial stdout#103
hamelsmu wants to merge 1 commit intomainfrom
feat/cell-exec-state-and-stdout-flush

Conversation

@hamelsmu
Copy link
Collaborator

Summary

  • Execution state machine (idle → pending → running → success → error) with colored left-border indicators on cell cards and bracket notation ([ ], [*], [1], [!]) in the gutter — addresses Cell Execution Status isn't clear #38
  • 150ms timeout-based flush for partial stdout lines (e.g. Password:, [y/n]) in both the React CellConsole component and the bindStreamsToCell data layer, with IOPub classification deferral preserved — addresses Not clear when bash cell output waiting for input #99
  • 13 unit tests covering exec state transitions and stdout flush behavior
  • Browser integration test fixture (stdin-prompt-test.runme.md) and Phase 8 in test-notebook-ui.sh

Changes

File What changed
Actions.tsx CellExecState type, rewrote RunActionButton, added pending state + handlePid + execState derivation, data-exec-state attribute, colored bracket
CellConsole.tsx flushTimerRef with 150ms timeout for partial lines, cleanup on unmount
notebookData.ts flushTimer in bindStreamsToCell, appendStdoutChunk returns boolean to preserve IOPub deferral, timer cleanup in exitCode/errors/dispose
index.css Left-border indicators per exec state, pulse-border keyframe animation on border-left-color
Actions.test.tsx 6 new tests: idle bracket, pending on click, running on PID, success/error on exitCode, re-run reset
CellConsole.test.tsx 3 new tests: partial line flush, no double-write, complete lines immediate
test-notebook-ui.sh Phase 8: verify data-exec-state and cell-bracket in page
stdin-prompt-test.runme.md New fixture with echo+prompt, password prompt, y/n confirm cells

Test plan

  • pnpm run build passes
  • All 13 unit tests pass (vitest run src/components/Actions/)
  • Browser smoke test (test-smoke.sh) with frontend running
  • Manual verification: run a cell, observe pending → running → success/error transitions
  • Manual verification: run a cell with read -p "prompt: ", confirm prompt appears within ~150ms

🤖 Generated with Claude Code

Add visual feedback during cell execution (issue #38) and fix prompts
without trailing newlines being buffered indefinitely (issue #99).

Execution state machine: idle → pending → running → success/error with
colored left-border indicators and bracket notation in the gutter.
Timeout-based flush (150ms) for partial stdout lines in both the React
component and data layer, with IOPub classification deferral preserved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 16, 2026 22:03
@github-actions
Copy link

[codex-cuj-presubmit][sha:da90538c443d3f40873b53af191700c2c9683e24]
@codex Please run the Critical User Journeys from docs-dev/cujs for commit da90538.

Required steps:

  • Execute app/test/browser/run-cuj-scenarios.ts.
  • Report assertion-level pass/fail for each CUJ.
  • Capture screenshots and scenario logs.
  • Record a short CUJ walkthrough video and attach/upload it to this PR thread.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: da90538c44

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +422 to 425
setPending(true);
setPid(null);
setExitCode(null);
cellData.run();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reset pending when run startup fails

runCode always sets pending before invoking cellData.run(), but there is no failure path that clears it when execution never reaches PID/exit callbacks. This happens when NotebookData.runCodeCell cannot start (e.g. no runner endpoint) or when the stream errors before emitting exitCode, so the cell stays stuck in pending with a spinner and [*] indefinitely; add a way to clear pending on startup failure/error.

Useful? React with 👍 / 👎.

Comment on lines +281 to +285
if (stdoutBufferRef.current) {
flushTimerRef.current = setTimeout(() => {
flushTimerRef.current = null;
flushStdoutBuffer();
}, 150);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Defer timed flush for incomplete IOPub fragments

The new 150ms timer flushes any partial stdout buffer via flushStdoutBuffer(), which falls back to plain-text output when parsing fails; if an IPykernel JSON line is split across frames and the next fragment arrives after 150ms, the first fragment is flushed prematurely and the full message can no longer be reconstructed, producing raw JSON/noisy output instead of parsed notebook output. Apply the same deferral guard used in bindStreamsToCell before scheduling this timeout.

Useful? React with 👍 / 👎.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a visual execution state machine for notebook cells and implements timeout-based flushing of partial stdout to improve user experience when cells wait for input. The changes address issue #38 (unclear cell execution status) and issue #99 (prompts not visible when waiting for input).

Changes:

  • Adds a 5-state execution lifecycle (idle → pending → running → success/error) with colored left-border indicators and bracket notation in the cell toolbar
  • Implements 150ms timeout-based flush for partial stdout lines to display prompts like "Password:" or "[y/n]" that don't end with newlines
  • Adds comprehensive test coverage with 13 unit tests and browser integration tests

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
app/src/components/Actions/Actions.tsx Introduces CellExecState type, refactors RunActionButton to show execution states, adds state transition logic with pending/running/success/error indicators, updates bracket label to show state symbols
app/src/lib/notebookData.ts Implements flush timer in bindStreamsToCell with 150ms timeout, adds deferral logic to preserve IOPub classification, clears timer in exitCode/errors/dispose paths
app/src/components/Actions/CellConsole.tsx Adds flushTimerRef for 150ms timeout-based partial line flushing, includes cleanup in useEffect return
app/src/index.css Adds colored left-border indicators for execution states (amber for pending/running, green for success, red for error) with pulse animation
app/src/components/Actions/Actions.test.tsx Adds 6 tests covering state transitions from idle through pending/running to success/error, including re-run behavior
app/src/components/Actions/CellConsole.test.tsx Adds 3 tests for flush behavior including partial line timeout, double-write prevention, and immediate complete line handling
app/test/browser/test-notebook-ui.sh Adds Phase 8 to verify data-exec-state attribute and cell-bracket testid in rendered page
app/test/fixtures/notebooks/stdin-prompt-test.runme.md New test fixture with three cells demonstrating stdin prompts: echo+read, password prompt, and y/n confirmation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 352 to 354
if (code !== null) {
setPid(null);
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The state transition logic has a potential edge case: if exitCode arrives as null (line 349-350), it will be stored but won't clear the pending state unless code is non-null at line 352-354. This means if exitCode is explicitly set to null, the cell could remain in a non-idle state. Consider whether setExitCode(null) should also clear pending state, or if null exitCode should be ignored entirely.

Suggested change
if (code !== null) {
setPid(null);
}
setPid(null);

Copilot uses AI. Check for mistakes.
Comment on lines 442 to +448
const sequenceLabel = useMemo(() => {
if (!cell) {
return " ";
}
if (execState === "error") return "!";
if (execState === "pending" || execState === "running") return "*";
if (!cell) return " ";
const seq = Number(cell.metadata[RunmeMetadataKey.Sequence]);
if (!seq) {
return " ";
}
return seq.toString();
}, [cell, pid, exitCode]);
return seq ? seq.toString() : " ";
}, [cell, execState]);
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bracket label will always display "[ ]", "[]", or "[!]" even when there's a sequence number in error/pending/running states. Once a cell has been successfully executed and has a sequence number, if it's re-run and enters pending/running state, users lose visibility of which execution number it was. Consider displaying the sequence number alongside the state indicator (e.g., "[1]" for cell 1 running) to maintain execution history context.

Copilot uses AI. Check for mistakes.
expect(text.match(/Password:/g)?.length).toBe(1);
document.body.removeChild(div);
});

Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test case "does not double-write when newline arrives after flush" validates that the buffer is properly cleared after a flush. However, it doesn't verify what happens if more data arrives while the timer is running but before it fires. Consider adding a test case where: 1) partial data arrives, 2) timer is scheduled, 3) more data arrives completing the line before timer fires, 4) verify the line appears once and timer is cleared.

Suggested change
it("does not double-write when line completes before flush timer fires", async () => {
vi.useFakeTimers();
const { streams, div } = await renderWithStreams();
// Emit partial line (no newline) to schedule the flush timer
await act(async () => {
streams.emitStdout(new TextEncoder().encode("Password:"));
});
// Advance time, but not enough to trigger the flush
await act(async () => {
vi.advanceTimersByTime(100);
});
// Partial line should still be buffered
expect(getTerminalText(div)).not.toContain("Password:");
// Now complete the line before the timer fires
await act(async () => {
streams.emitStdout(new TextEncoder().encode("\n"));
});
// "Password:" should appear exactly once after completing the line
let text = getTerminalText(div);
expect(text.match(/Password:/g)?.length).toBe(1);
// Advance timers past the flush threshold; line should not be written again
await act(async () => {
vi.advanceTimersByTime(200);
});
text = getTerminalText(div);
expect(text.match(/Password:/g)?.length).toBe(1);
document.body.removeChild(div);
});

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +102
// Fake streams for testing PID/exitCode callbacks through CellConsole subscriptions.
class FakeStreams {
private pidCbs = new Set<(pid: number) => void>();
private exitCbs = new Set<(code: number) => void>();
private stdoutCbs = new Set<(data: Uint8Array) => void>();
private stderrCbs = new Set<(data: Uint8Array) => void>();

stdout = {
subscribe: (cb: (data: Uint8Array) => void) => {
this.stdoutCbs.add(cb);
return { unsubscribe: () => this.stdoutCbs.delete(cb) };
},
};
stderr = {
subscribe: (cb: (data: Uint8Array) => void) => {
this.stderrCbs.add(cb);
return { unsubscribe: () => this.stderrCbs.delete(cb) };
},
};
pid = {
subscribe: (cb: (pid: number) => void) => {
this.pidCbs.add(cb);
return { unsubscribe: () => this.pidCbs.delete(cb) };
},
};
exitCode = {
subscribe: (cb: (code: number) => void) => {
this.exitCbs.add(cb);
return { unsubscribe: () => this.exitCbs.delete(cb) };
},
};

emitPid(pid: number) { this.pidCbs.forEach((cb) => cb(pid)); }
emitExitCode(code: number) { this.exitCbs.forEach((cb) => cb(code)); }
emitStdout(data: Uint8Array) { this.stdoutCbs.forEach((cb) => cb(data)); }

setCallback() {}
sendExecuteRequest() {}
close() {}
}
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FakeStreams class is duplicated between CellConsole.test.tsx and Actions.test.tsx with identical implementation. Consider extracting this to a shared test utility file to reduce code duplication and ensure consistency across test files. This would also make it easier to maintain and extend the FakeStreams API in the future.

Copilot uses AI. Check for mistakes.
Comment on lines +234 to +235
@keyframes pulse-border {
0%, 100% { border-left-color: #fbbf24; }
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded color value #fbbf24 in the keyframe animation should use the Tailwind amber-400 color for consistency. The @apply directive at line 156 uses border-l-amber-400, but the animation hardcodes a hex value. If Tailwind's amber-400 color is ever changed in the theme, these values will diverge. Consider using CSS custom properties or ensuring the hex value matches Tailwind's amber-400 exactly.

Suggested change
@keyframes pulse-border {
0%, 100% { border-left-color: #fbbf24; }
:root {
/* Tailwind amber-400 */
--color-amber-400: #fbbf24;
}
@keyframes pulse-border {
0%, 100% { border-left-color: var(--color-amber-400); }

Copilot uses AI. Check for mistakes.
@github-actions
Copy link

[codex-cuj-presubmit][sha:e0a0ea09615255def5b067513e014f0b83155fe2]
@codex Please run the Critical User Journeys from docs-dev/cujs for commit e0a0ea0.

Required steps:

  • Execute app/test/browser/run-cuj-scenarios.ts.
  • Report assertion-level pass/fail for each CUJ.
  • Capture screenshots and scenario logs.
  • Record a short CUJ walkthrough video and attach/upload it to this PR thread.

@github-actions
Copy link

[codex-cuj-presubmit][sha:4e2daeeed3589362666d7ffc17d25d0df4d97046]
@codex Please run the Critical User Journeys from docs-dev/cujs for commit 4e2daee.

Required steps:

  • Execute app/test/browser/run-cuj-scenarios.ts.
  • Report assertion-level pass/fail for each CUJ.
  • Capture screenshots and scenario logs.
  • Record a short CUJ walkthrough video and attach/upload it to this PR thread.

hamelsmu added a commit that referenced this pull request Feb 16, 2026
hamelsmu added a commit that referenced this pull request Feb 16, 2026
hamelsmu added a commit that referenced this pull request Feb 16, 2026
hamelsmu added a commit that referenced this pull request Feb 16, 2026
hamelsmu added a commit that referenced this pull request Feb 16, 2026
hamelsmu added a commit that referenced this pull request Feb 16, 2026
hamelsmu added a commit that referenced this pull request Feb 16, 2026
@hamelsmu hamelsmu force-pushed the feat/cell-exec-state-and-stdout-flush branch from 4e2daee to da90538 Compare February 16, 2026 22:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants