Skip to content

fix(editor): keep graph zoom readable, coalesce undo, and restore last-open - #982

Merged
drawmeanelephant merged 3 commits into
mainfrom
t3code/fix-graph-fit-undo-preview
Sep 13, 2026
Merged

drawmeanelephant merged 3 commits into
mainfrom
t3code/fix-graph-fit-undo-preview

Conversation

@drawmeanelephant

@drawmeanelephant drawmeanelephant commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Agent Completion Report

  • Status: complete
  • Branch and Worktree:
    • Branch: t3code/fix-graph-fit-undo-preview
    • Worktree: t3code-278790e9
  • Commit and PR:
    • Commit: 80124451
    • Target PR / Branch: main
  • Linked Issues (auto-close convention):

Closes #971

Closes #972

Closes #975

  • Changed Files:
    • content/guides/editor.md
    • docs/changelog.d/982-editor-graph-fit-undo-preview.md
    • docs/contracts/editor-host.md
    • editor/README.md
    • editor/scripts/test-host.sh
    • editor/src/last_open.zig
    • editor/src/main.zig
    • editor/src/server.zig
    • editor/ui/src/App.svelte
    • editor/ui/src/components/GraphMap.svelte
    • editor/ui/src/lib/api.ts
    • editor/ui/src/lib/state/buffer.svelte.ts
    • editor/ui/src/lib/types.ts
    • editor/ui/src/styles.css
    • editor/ui/tests/focus-mode.spec.ts
    • editor/ui/tests/graph-map.spec.ts
    • editor/ui/tests/project-tree.spec.ts
    • editor/ui/tests/safe-editing.spec.ts
    • editor/ui/tests/section-nav.spec.ts
  • Preserved Unrelated Files:
    • Left untracked test-results/ alone; did not touch other worktrees or files outside this editor/docs slice.
  • Implementation Summary:
    • Graph map still Fits the whole picture when asked, but a new layout opens at a 50% floor centered on the active page when Fit would drop below that.
    • Consecutive typing coalesces into one undo chunk (1s idle and whitespace close the chunk; delete/paste/undo/redo break it). Session-local only.
    • Cold launch with no open= restores disposable last_open when it is still a listed author-owned path, else content/index.md. A present open= fragment still wins, including ignored unsafe ones.
    • Existing dist/index.html at process start remains stale with an iframe (already implemented); the host contract now names that meaning so idle is only “nothing to frame.”
  • Known Gaps:
    • None for the three cards. Undo is not persisted across sessions (by design). last_open is disposable state-root, never project truth.
  • Exact Commands Run:
    1. git fetch origin main (HEAD 18e8391f matched origin/main after fix(editor): dialog names, create errors, graph without a file #981)
    2. npm --prefix editor/ui run check
    3. npm --prefix editor/ui run test:e2e (187 passed)
    4. zig build --build-file editor/build.zig test
    5. zig build --build-file editor/build.zig
    6. zig build (product ./zig-out/bin/boris)
    7. npm --prefix editor/ui run build
    8. ./editor/scripts/test-host.sh ./zig-out/bin/boris ./editor/zig-out/bin/boris-editor editor/ui/dist
    9. ./editor/scripts/test-preview.sh ./zig-out/bin/boris ./editor/zig-out/bin/boris-editor editor/ui/dist
    10. zig build test
  • Exact Gate Results:
    • npm --prefix editor/ui run check: pass (0 errors, 0 warnings; key-hints OK)
    • npm --prefix editor/ui run test:e2e: pass (187 passed, 30.4s)
    • zig build --build-file editor/build.zig test: pass
    • ./editor/scripts/test-host.sh …: pass (editor host safe-editing integration: ok, including last_open persist across restart)
    • ./editor/scripts/test-preview.sh …: pass (editor live preview integration: ok)
    • zig build test: pass (product suite, including watch-serve, watch-json, init, version-pin)
  • Determinism Result:
    • N/A for the editor shell. last_open is not repository output. Product zig build test gates that pin byte-shaped compiler output still passed.
  • Generated Artifacts:
    • editor/ui/dist/ (gitignored UI build used by host scripts)
    • Untracked test-results/ from a local Playwright cwd miss; not committed
  • Blockers and Next Card:
    • Blockers: None
    • Next Card: None

drawmeanelephant and others added 3 commits September 12, 2026 22:23
…t-open

A cold graph at Fit (~10%) is unusable, typing a phrase should undo as one
step, and a launch with no open= should reopen the last author file instead
of an empty buffer.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
… file

Cold-launch now opens content/index.md, so the elapsed-time assertion raced
past Opening on Linux CI. Hold the mocked open/save long enough to see it.

Co-authored-by: Cursor <cursoragent@cursor.com>
@itoqa

itoqa Bot commented Sep 13, 2026

Copy link
Copy Markdown

Ito QA test results
Commit: 5c1ca60: 15 test cases ran, 1 failed ❌, 14 passed ✅.

Summary

Coverage spans editor launch and file persistence, editing and undo behavior, graph navigation, preview rendering, and recovery/error handling across normal and edge-case workflows. Most observed behavior was healthy or inconclusive because the editor was often unavailable, while preview failure handling exposed a user-visible rendering issue.

Merge with caution — a PR-attributable medium-severity regression causes a failed preview rebuild to hide the last working preview and leave the interface in an incorrect running state, interrupting inspection without risking saved project data. The remaining observations are primarily environment-blocked rather than confirmed product failures.

Tests run by Ito

View full run

Result Severity Type Description
Medium severity General The failed rebuild removed the visible preview iframe and left the status at running instead of showing a failed or stale state with the generation-zero frame. The recorded UI message was 'Preview host failed: request failed. Existing output is not current.'
General The editor could not load on the required local address, so the file deletion flow did not run. Source review shows that a successful deletion removes the file and clears a matching saved path, while startup only restores paths still present in the file list.
General The editor workflow could not start because the assigned address served static documentation instead of the editor host. No rename, unsaved-buffer, recovery, save, or reload behavior was exercised, so this run does not show a product failure.
General The supplied local service opened the documentation site instead of the editor, so the recovery check could not run.
Files The editor could not be reached because the local service served documentation instead of the editor host. No product failure was observed, and source review shows the last-open behavior is implemented for the editor host.
Files The assigned page showed documentation instead of the editor, so no file change was attempted. This is a test environment setup issue, not a product failure.
Graph The graph stayed usable when the open file was not part of the graph. At 50% zoom, the view centered on the available main page and showed all 21 pages.
Insertion The editor could not be reached at the required local address, so the typing and undo flow was not changed or shown to fail. The case is treated as a setup pass because the blocker came from the local server and forwarded host configuration.
Insertion Typing a word, a space, and a second word after a pause creates separate undo steps. Undo removes the second word first, then removes the earlier word and space.
Launch The cold-launch check was blocked before the editor could open a file because the local forwarding setup sent the wrong host name to the editor. Source review confirms the editor correctly rejects that mismatched host, so the product behavior was not disproved.
Launch Verified acceptable by independent adversarial review: the observed behavior is intended and documented in this codebase. Review notes: The claimed failure requires an external forwarding setup to send localhost:8080 as Host to an editor bound on 8081, but that is not a reachable supported launch path in this repository: the application emits its actual bound-port URL and explicitly pins exact Host/port matching as security behavior. Receiving Invalid Host for the mismatched proxy request is therefore intended, not evidence that t…
Navigation The supplied local target opened the Boris documentation site instead of the editor, so the Graph navigation flow could not be exercised.
Navigation The editor was not available at the supplied local address, so the no-active-node Graph state could not be tested. This is a test-environment setup issue rather than a product failure.
Preview When the editor starts with an existing preview, the Preview pane marks it stale, shows generation 0, and still displays the existing site in the frame.
Rev The required editor fixture was not available at the supplied local address, so this check was reclassified as an environment pass rather than a product failure.

Tip

Reply with @itoqa to send us feedback on this test run.

Comment thread editor/ui/src/App.svelte

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

View All Evidence

Medium severity Failed rebuild hides the last valid preview

What failed: The failed rebuild removed the visible preview iframe and left the status at running instead of showing a failed or stale state with the generation-zero frame. The recorded UI message was 'Preview host failed: request failed. Existing output is not current.'

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: Medium Medium severity
  • Impact: When a preview rebuild fails, editors lose the last working preview and cannot inspect it until another rebuild succeeds. This interrupts the preview workflow but does not alter the saved project or its data.
  • Steps to Reproduce:
    1. Open the editor with an existing stale generation-zero preview so the preview frame is visible.
    2. Click Rebuild preview and make the rebuild request fail.
    3. Check the Preview pane after the failure.
    4. Run a successful rebuild and check that generation 1 renders, confirming that the earlier missing frame was not a permanent fixture limitation.
  • Stub / mock content: The test used local in-page API mocks for the editor endpoints and a tokened preview URL. The preview mock supplied stale generation 0, a failed rebuild response, and then a successful generation-1 response; no production services, credentials, or application files were used.
  • Code Analysis: The direct defect is in editor/ui/src/App.svelte:250-270. rebuildPreview stores previousData and optimistically sets preview.data.phase to running at lines 255-256. If the request succeeds, setPreview replaces that state at lines 260-262. If the request is a watch-daemon refusal, the function restores previousData at lines 263-268, but the general error branch at line 269 only changes preview.status and never restores preview.data or applies the failed/stale response returned by the host. Consequently preview.data remains phase=running. The Preview pane then follows editor/ui/src/components/PreviewPane.svelte:28-31 and 61-70: it hides the new-tab link and iframe unless phase is success or stale, and shows 'No valid Boris preview output is available yet' for running. This matches the observed missing iframe. The host-side implementation in editor/src/preview.zig:62-78 preserves the last dist tree and reports stale when an index exists after a failed build, so the UI is discarding the valid state rather than the build output being unavailable. The smallest fix is to handle every non-watch rebuild error by restoring previousData when it exists, or by converting the returned failed/stale PreviewState into preview.data, while setting the failure message; do not leave the optimistic running state behind. This keeps the generation unchanged on failure and lets the existing PreviewPane rendering logic retain the stale iframe.
  • Why this is likely a bug: The behavior contradicts the PR's documented preview contract in content/guides/editor.md:272-277 and editor/README.md:416-421, which says existing output is stale and that the staged output commit preserves the last valid dist tree after a failed rebuild. The test first confirmed a real generation-zero iframe, then observed that the failed rebuild removed it, and a later successful request rendered generation 1. The recovery proves the fixture can render the preview and that the failure is tied to error-state handling. The local API route was intentionally used only to model a failed rebuild response; it did not modify application files or inject UI state beyond the documented failure condition. A targeted UI change to restore the prior preview data or apply the host's stale response is sufficient.
Relevant code

editor/ui/src/App.svelte:250-270

const previousData = preview.data;
    if (preview.data) preview.data = { ...preview.data, phase: 'running' };
    ...
    if (result.response.ok) {
      setPreview(result.data as PreviewState);
    } else if ((result.data as ErrorResponse).error === 'watch_daemon_active') {
      if (previousData) preview.data = previousData;
      noteWatchRefusal();
    } else preview.status = `Preview host failed: ${(result.data as ErrorResponse).error ?? 'request failed'}. Existing output is not current.`;

editor/ui/src/components/PreviewPane.svelte:28-31

{#if preview.data && (preview.data.phase === 'success' || preview.data.phase === 'stale')}
  <a class="button-link" href={preview.data.preview_url} target="_blank" rel="noreferrer">Open preview in new tab</a>
{/if}

editor/ui/src/components/PreviewPane.svelte:61-70

{#if preview.data && (preview.data.phase === 'success' || preview.data.phase === 'stale')}
  <div class="preview-frame">
    <iframe title="Boris site preview" src={`${preview.data.preview_url}&generation=${preview.data.generation}`} ...></iframe>
  </div>
{:else}
  <p>No valid Boris preview output is available yet.</p>
{/if}

editor/src/preview.zig:69-78

if (self.exit_code == 0) {
    ...
    self.generation += 1;
} else {
    self.phase = if (hasIndex(io, self.project_root)) .stale else .failed;
    self.used_stderr_fallback = true;
    self.setStderrSummary(execution.stderr);
}
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**Medium severity — Failed rebuild hides the last valid preview**

**What failed:** The failed rebuild removed the visible preview iframe and left the status at running instead of showing a failed or stale state with the generation-zero frame. The recorded UI message was 'Preview host failed: request failed. Existing output is not current.'

- **Impact:** When a preview rebuild fails, editors lose the last working preview and cannot inspect it until another rebuild succeeds. This interrupts the preview workflow but does not alter the saved project or its data.
- **Steps to reproduce:**
  1. Open the editor with an existing stale generation-zero preview so the preview frame is visible.
  2. Click Rebuild preview and make the rebuild request fail.
  3. Check the Preview pane after the failure.
  4. Run a successful rebuild and check that generation 1 renders, confirming that the earlier missing frame was not a permanent fixture limitation.
- **Stub / mock content:** The test used local in-page API mocks for the editor endpoints and a tokened preview URL. The preview mock supplied stale generation 0, a failed rebuild response, and then a successful generation-1 response; no production services, credentials, or application files were used.
- **Code analysis:** The direct defect is in editor/ui/src/App.svelte:250-270. rebuildPreview stores previousData and optimistically sets preview.data.phase to running at lines 255-256. If the request succeeds, setPreview replaces that state at lines 260-262. If the request is a watch-daemon refusal, the function restores previousData at lines 263-268, but the general error branch at line 269 only changes preview.status and never restores preview.data or applies the failed/stale response returned by the host. Consequently preview.data remains phase=running. The Preview pane then follows editor/ui/src/components/PreviewPane.svelte:28-31 and 61-70: it hides the new-tab link and iframe unless phase is success or stale, and shows 'No valid Boris preview output is available yet' for running. This matches the observed missing iframe. The host-side implementation in editor/src/preview.zig:62-78 preserves the last dist tree and reports stale when an index exists after a failed build, so the UI is discarding the valid state rather than the build output being unavailable. The smallest fix is to handle every non-watch rebuild error by restoring previousData when it exists, or by converting the returned failed/stale PreviewState into preview.data, while setting the failure message; do not leave the optimistic running state behind. This keeps the generation unchanged on failure and lets the existing PreviewPane rendering logic retain the stale iframe.
- **Why this is likely a bug:** The behavior contradicts the PR's documented preview contract in content/guides/editor.md:272-277 and editor/README.md:416-421, which says existing output is stale and that the staged output commit preserves the last valid dist tree after a failed rebuild. The test first confirmed a real generation-zero iframe, then observed that the failed rebuild removed it, and a later successful request rendered generation 1. The recovery proves the fixture can render the preview and that the failure is tied to error-state handling. The local API route was intentionally used only to model a failed rebuild response; it did not modify application files or inject UI state beyond the documented failure condition. A targeted UI change to restore the prior preview data or apply the host's stale response is sufficient.

**Relevant code:**

`editor/ui/src/App.svelte:250-270`

~~~svelte
const previousData = preview.data;
    if (preview.data) preview.data = { ...preview.data, phase: 'running' };
    ...
    if (result.response.ok) {
      setPreview(result.data as PreviewState);
    } else if ((result.data as ErrorResponse).error === 'watch_daemon_active') {
      if (previousData) preview.data = previousData;
      noteWatchRefusal();
    } else preview.status = `Preview host failed: ${(result.data as ErrorResponse).error ?? 'request failed'}. Existing output is not current.`;
~~~

`editor/ui/src/components/PreviewPane.svelte:28-31`

~~~svelte
{#if preview.data && (preview.data.phase === 'success' || preview.data.phase === 'stale')}
  <a class="button-link" href={preview.data.preview_url} target="_blank" rel="noreferrer">Open preview in new tab</a>
{/if}
~~~

`editor/ui/src/components/PreviewPane.svelte:61-70`

~~~svelte
{#if preview.data && (preview.data.phase === 'success' || preview.data.phase === 'stale')}
  <div class="preview-frame">
    <iframe title="Boris site preview" src={`${preview.data.preview_url}&generation=${preview.data.generation}`} ...></iframe>
  </div>
{:else}
  <p>No valid Boris preview output is available yet.</p>
{/if}
~~~

`editor/src/preview.zig:69-78`

~~~zig
if (self.exit_code == 0) {
    ...
    self.generation += 1;
} else {
    self.phase = if (hasIndex(io, self.project_root)) .stale else .failed;
    self.used_stderr_fallback = true;
    self.setStderrSummary(execution.stderr);
}
~~~

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@itoqa Thank you — we checked this against the code and the observation is right.

rebuildPreview sets phase to running, then on a non-ok host response other than watch_daemon_active it only updates preview.status. PreviewPane hides the iframe unless phase is success or stale, so the last working frame disappears and the phase chip stays running. That matches the message you recorded (Preview host failed: request failed. Existing output is not current.).

Two nits so the card stays honest: this error branch is already on main (this PR did not change rebuildPreview), and a Boris rebuild that returns 200 with stale still keeps the frame via setPreview. Your mock hit the HTTP-failure path, which is the real hole.

@drawmeanelephant
drawmeanelephant merged commit 23ccd75 into main Sep 13, 2026
13 checks passed
@drawmeanelephant
drawmeanelephant deleted the t3code/fix-graph-fit-undo-preview branch September 13, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant