Home

mweinbach / agent-coworker

publicmweinbach/agent-coworker
Overview Code History Branches Pull requestsIssuesInsights
main
HomeOverview Code PRsIssues

Add canvas-backed spreadsheet editing and artifact runtime support (#141)

4 months ago

3bf5455
Authored
Max Weinbach6/1/2026, 1:26:25 AM
* fix(canvas): prevent hooks-order crash and markdown round-trip data loss

Canvas is mounted unkeyed, so switching a markdown/text file to a csv/xlsx/pptx re-rendered the same instance with early returns sitting above 7 useEffect/useCallback hooks, crashing React ("rendered fewer hooks than expected"). Move the spreadsheet/pptx returns below all hooks and guard the editor-only effects so preview behavior is unchanged.

Also fix lossy markdown editing: nodeToMarkdown emitted "-" for ordered <li> (numbered lists silently became bullets) and markdownToHtml treated `*emphasis*`/`-text` lines as list items; require whitespace after list markers and serialize <ol> items as "N.". Harden link hrefs against javascript:/data: schemes.

Add canvasMarkdown unit tests and a hooks-stability regression test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(canvas): editable Excel-like spreadsheet with lossless cell writes

Add direct cell editing to the canvas spreadsheet view via a new cowork/workspace/spreadsheet/edit JSON-RPC method. CSV edits use a quote-aware round trip that preserves BOM, line terminator, and trailing-newline state. XLSX edits are a surgical zip patch (jszip + fast-xml-parser): only the target worksheet XML is rewritten and the cell keeps its style index, so fonts, number formats, charts, and other sheets stay byte-identical — SheetJS's community build cannot write styles, so the workbook is never round-tripped through it.

Rework SpreadsheetPreview into an Excel-like grid: formula/value bar, click-to-select, inline editing (double-click/Enter/F2/type), Esc/Enter/Tab semantics, and arrow/Tab/Enter navigation with viewport auto-paging; commits optimistically then refetches. Add an in-app Maximize/Restore full-bleed layout alongside the existing pop-out window.

Wire the client store action + socket helper, document the method in websocket-protocol.md, regenerate the JSON-RPC artifacts, and add backend (lossless CSV/XLSX) and UI (edit/formula-bar/navigation/maximize) tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix artifacts

* update

* fix(canvas): polish spreadsheet workbook preview

* fix(canvas): simplify spreadsheet preview chrome

* fix(canvas): harden spreadsheet objects and styling

* fix(canvas): keep workbook links inside electron

* feat(canvas): improve spreadsheet editing

* more

* feat: replace spreadsheet canvas with Univer

* fix: suppress noisy Univer text warnings

* Add spreadsheet autosave and external refresh

* fix(spreadsheet): apply batch patches as one atomic read-modify-write

patchSpreadsheetBatch previously delegated each operation to
editSpreadsheetCell/formatSpreadsheetRange, so every op independently
re-read and re-wrote the whole file (zip load + generate). A bulk paste
from the Univer canvas (one op per changed cell) meant N full-file
rewrites, and a mid-batch failure left ops 1..N-1 persisted with no
rollback. Concurrent batches to the same file could also lose edits via
read-modify-write races.

Introduce a shared in-memory editing core: the file is read/unzipped
once, all cell and format ops apply to cached worksheet XML + a single
parsed stylesheet, then the result is validated and written exactly
once. Single-cell edit, range format, and batch patch all route through
the same core, so behavior stays identical while batches become atomic.
A per-file write lock serializes concurrent read-modify-write cycles
against the same resolved path so neither edit is lost.

Also use the semantic `text-primary` token for the dirty save-state
icon instead of the raw `text-[var(--accent)]` arbitrary value.

Adds regression tests for mid-batch atomicity (file stays byte-identical
on failure) and concurrent-edit serialization.

https://claude.ai/code/session_01VAPGeo1v6uzu9MG7u4DnGN

* fix(desktop): clear biome lint errors in canvas spreadsheet UI

The Lint CI lane was red on pre-existing biome findings in the canvas
spreadsheet code:

- univerSpreadsheet.ts: drop an unused type import and replace an
  assignment-in-expression (`??=` inside a binding) with an explicit
  initialise-then-store.
- styles.css: annotate the deliberate Univer formula-bar `!important`
  override with a biome-ignore, and move the universal `box-sizing`
  reset above the `[data-cowork-univer-canvas] *` rule to fix descending
  specificity.
- SpreadsheetPreview.tsx: give the formatting toolbar `role="toolbar"`
  and the color groups `role="group"` so their `aria-label`s are valid
  (a sub-group inside a toolbar is the correct ARIA pattern, so the
  redundant useSemanticElements suggestion is suppressed with a reason).

https://claude.ai/code/session_01VAPGeo1v6uzu9MG7u4DnGN

* fix(spreadsheet): no-op empty batches and attribute batch errors correctly

Addresses two review findings on the atomic-batch refactor:

- An empty `operations` array now short-circuits to success before
  touching disk. Previously it flowed into the executor, which re-zipped
  the XLSX (changing the fingerprint) or re-quoted the CSV (possibly
  changing line endings/quotes) despite no edits.
- A throw inside an operation (e.g. an invalid color reaching
  normalizeColor) is now attributed to that operation's index instead of
  a hard-coded 0, so the batch no longer reports "Operation 1 failed"
  when a later op is the culprit. Failures not tied to a specific op
  (file read/write, post-batch validation, unsupported type) carry a
  null index and report without an operation prefix. As a bonus the XLSX
  path skips the rewrite when no operation actually mutated the workbook.

Adds regression tests for the empty-batch no-op (file stays byte- and
mtime-identical) and for correct operation-index attribution on a
mid-batch throw.

https://claude.ai/code/session_01VAPGeo1v6uzu9MG7u4DnGN

* style: satisfy biome check (format + import ordering) for the Lint CI lane

The CI "Lint" job runs `bun run check` (biome lint + format + organize
imports), not just `biome lint`, so pre-existing format and import-order
drift in the canvas spreadsheet files (plus the notarization helper
carried over from main) kept the lane red. Applied biome's safe
formatting and import-ordering fixes; no behavior changes.

https://claude.ai/code/session_01VAPGeo1v6uzu9MG7u4DnGN

* fix: guard referenced context and large file reads

Keep queued steer reference context attached to the steer that supplied it, cap forced skill injection by per-skill and total byte limits, and prevent desktop readFile from loading oversized files.

Show a Canvas truncation banner when preview reads are partial so large files remain read-only with visible user guidance.

Tests: bun test test/session/agentSession.messaging.test.ts test/session.syntheticSkillInjection.test.ts; bun test apps/desktop/test/ipc-files.test.ts apps/desktop/test/canvas-hooks-stability.test.tsx; bun run typecheck; bun run docs:check; bun lint; bun test --max-concurrency 1

* refactor(spreadsheet): remove legacy preview surface

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

* feat(desktop): render spreadsheet canvas requests as a chat bubble

Parse the <spreadsheet_canvas_request> envelope the Univer canvas sends
and show a compact file/sheet/region header above the user's request
instead of the raw XML blob. Falls through cleanly for plain and legacy
canvas-edit messages.

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

* feat(desktop): unify all canvas requests into one chat bubble

Emit the structured XML envelope from the document/text/slide canvas too
(not just spreadsheets) and parse every canvas request through a single
parseCanvasRequest + CanvasRequestBody, with a legacy markdown fallback so
historical transcripts render the same compact file/region header.

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

* refactor(desktop): share canvas request envelope helpers

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

* fix(spreadsheet): preserve canvas prompts and number formats

* fix(spreadsheet): persist canvas merge patches

* fix(spreadsheet): protect failed canvas saves

* fix(spreadsheet): refresh canvas prompt context

* fix(spreadsheet): serialize canvas saves

* fix(spreadsheet): keep canvas context live

* fix(desktop): wire canvas maximize control

* fix(desktop): harden spreadsheet canvas saves

* fix(server): harden canvas previews

* fix(spreadsheet): persist column resets

* fix(spreadsheet): preserve workbook date and style metadata

* fix(desktop): preserve canvas editor lifecycle

* fix(spreadsheet): harden canvas save bounds

* fix(desktop): avoid spreadsheet canvas ownership conflicts

* fix(desktop): report spreadsheet unload save failures

* fix(desktop): preserve Windows markdown links

* fix(desktop): keep maximized canvas unclipped

* test(desktop): isolate canvas lifecycle mocks

* fix: refresh stale workspace tools skills

* feat: detect marketplace extension updates

* fix: guard spreadsheet saves and plugin update state

* chore: format marketplace update changes

* fix: rebase spreadsheet saves after disk conflicts

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

Parent57c99a5

138 files changed
  • CHANGELOG.md+2−2
  • apps/desktop/electron/ipc/files.ts+11−0
  • apps/desktop/electron/main.ts+24−1
  • apps/desktop/electron/services/serverManager.ts+34−0
  • apps/desktop/package.json+12−0
  • apps/desktop/scripts/notarize.cjs+7−5
  • apps/desktop/src/App.tsx+39−13
  • apps/desktop/src/app/store.actions/explorer.ts+10
−2
  • apps/desktop/src/app/store.actions/plugins.ts+78−0
  • apps/desktop/src/app/store.actions/preview.ts+51−9
  • apps/desktop/src/app/store.helpers.ts+17−5
  • apps/desktop/src/app/store.helpers/controlSocket.ts+35−0
  • apps/desktop/src/app/store.helpers/jsonRpcSocket.ts+39−8
  • apps/desktop/src/app/store.ts+1−0
  • apps/desktop/src/components/ai-elements/message.tsx+34−2
  • apps/desktop/src/lib/canvasMarkdown.ts+35−5
  • apps/desktop/src/lib/canvasRequest.ts+69−0
  • apps/desktop/src/lib/desktopCommands.ts+6−4
  • apps/desktop/src/lib/spreadsheetSaveNotifications.ts+29−0
  • apps/desktop/src/lib/univerCanvasConfig.ts+15−0
  • apps/desktop/src/lib/univerSaveState.ts+19−0
  • apps/desktop/src/lib/univerSpreadsheet.ts+874−0
  • apps/desktop/src/styles.css+43−4
  • apps/desktop/src/styles/theme-bridge.css+16−0
  • apps/desktop/src/ui/Canvas.tsx+187−124
  • apps/desktop/src/ui/FilePreviewModal.tsx+2−2
  • apps/desktop/src/ui/LazyUniverSpreadsheetCanvas.tsx+28−0
  • apps/desktop/src/ui/SpreadsheetPreview.tsx+0−526
  • apps/desktop/src/ui/UniverSpreadsheetCanvas.tsx+740−0
  • apps/desktop/src/ui/canvas/CanvasFilePreviewLayout.tsx+35−16
  • apps/desktop/src/ui/chat/FeedRow.tsx+54−13
  • apps/desktop/src/ui/chat/feedMessageParsing.ts+98−20
  • apps/desktop/src/ui/layout/AppTopBar.tsx+22−0
  • apps/desktop/src/ui/plugins/PluginDetailDialog.tsx+56−8
  • apps/desktop/test/app-topbar.test.tsx+74−0
  • apps/desktop/test/canvas-hooks-stability.test.tsx+281−0
  • apps/desktop/test/canvas-window-lifecycle.test.tsx+286−0
  • apps/desktop/test/canvasMarkdown.test.ts+126−0
  • apps/desktop/test/desktop-feature-flags.test.ts+10−0
  • apps/desktop/test/feed-message-parsing.test.ts+155−0
  • apps/desktop/test/feed-row-canvas-request.test.tsx+68−0
  • apps/desktop/test/file-preview-modal.test.tsx+8−101
  • apps/desktop/test/ipc-files.test.ts+110−2
  • apps/desktop/test/message-links.test.ts+39−0
  • apps/desktop/test/plugin-actions.test.ts+46−0
  • apps/desktop/test/plugin-detail-dialog.test.tsx+94−5
  • apps/desktop/test/spreadsheet-preview-server-start.test.ts+101−0
  • apps/desktop/test/spreadsheet-preview.test.tsx+0−311
  • apps/desktop/test/store-explorer.test.ts+40−1
  • apps/desktop/test/univer-canvas-config.test.ts+13−0
  • apps/desktop/test/univer-save-state.test.ts+35−0
  • apps/desktop/test/univer-spreadsheet.test.ts+471−0
  • bun.lock+284−2
  • docs/generated/websocket-jsonrpc.d.ts+30−22
  • docs/generated/websocket-jsonrpc.schema.json+1−1
  • docs/websocket-protocol.md+68−6
  • package.json+2−0
  • scripts/build_cowork_server_binary.ts+33−0
  • scripts/build_desktop_resources.ts+52−1
  • scripts/postinstall.ts+6−0
  • scripts/setup_artifact_runtime.ts+34−0
  • skills/documents/SKILL.md+1−1
  • skills/presentations/scripts/artifact_tool_utils.mjs+5−4
  • skills/spreadsheets/SKILL.md+3−3
  • src/agent.ts+17−7
  • src/artifactRuntime.ts+8−0
  • src/artifactRuntime/archive.ts+105−0
  • src/artifactRuntime/constants.ts+24−0
  • src/artifactRuntime/ensureReady.ts+161−0
  • src/artifactRuntime/index.ts+7−0
  • src/artifactRuntime/instructions.ts+97−0
  • src/artifactRuntime/internal.ts+18−0
  • src/artifactRuntime/migrate.ts+77−0
  • src/artifactRuntime/runtimeDiscovery.ts+324−0
  • src/artifactRuntime/state.ts+36−0
  • src/artifactRuntime/types.ts+51−0
  • src/codexPrimaryRuntime.ts+0−1
  • src/codexPrimaryRuntime/ensureReady.ts+1−36
  • src/codexPrimaryRuntime/index.ts+0−1
  • src/codexPrimaryRuntime/instructions.ts+0−51
  • src/codexPrimaryRuntime/internal.ts+1−2
  • src/codexPrimaryRuntime/runtimeDiscovery.ts+0−249
  • src/codexPrimaryRuntime/skills.ts+28−16
  • src/codexPrimaryRuntime/state.ts+0−2
  • src/codexPrimaryRuntime/types.ts+0−15
  • src/extensions/sourceFingerprint.ts+78−0
  • src/managedSofficeRuntime/ensureReady.ts+1−1
  • src/plugins/catalog.ts+24−1
  • src/plugins/index.ts+2−0
  • src/plugins/manifest.ts+5−0
  • src/plugins/marketplace.ts+5−0
  • src/plugins/operations.ts+105−2
  • src/plugins/remoteMarketplace.ts+5−0
  • src/providers/codexAppServerClient.ts+6−5
  • src/runtime/codexAppServer/config.ts+7−4
  • src/server/jsonrpc/routes/plugins.ts+41−0
  • src/server/jsonrpc/routes/workspace.ts+68−5
  • src/server/jsonrpc/schema.plugins.ts+4−0
  • src/server/jsonrpc/schema.workspace.ts+180−49
  • src/server/presentationPreview.ts+23−20
  • src/server/protocol.ts+6−0
  • src/server/runtime/ServerRuntime.ts+11−3
  • src/server/session/AgentSession.ts+8−0
  • src/server/session/PluginCatalogService.ts+3−1
  • src/server/session/SessionRuntime.ts+8−0
  • src/server/session/SkillManager.ts+73−0
  • src/server/session/turnExecution/referenceInjection.ts+37−0
  • src/server/session/turnExecution/steerCoordinator.ts+5−10
  • src/server/sessionBackup/command.ts+2−2
  • src/server/spreadsheetA1.ts+35−0
  • src/server/spreadsheetEdit.ts+140−0
  • src/server/spreadsheetEditCsv.ts+117−0
  • src/server/spreadsheetEditTypes.ts+11−0
  • src/server/spreadsheetEditXlsx.ts+955−0
  • src/server/spreadsheetOoxml.ts+575−0
  • src/server/spreadsheetPreview.ts+320−141
  • src/server/transport/h3/server.ts+2−1
  • src/shared/jsonrpcControlSchemas.ts+37−0
  • src/shared/spreadsheetPreview.ts+179−29
  • src/skills/operations.ts+106−21
  • src/tools/bash.ts+2−2
  • src/types.ts+22−0
  • test/agent.test.ts+20−15
  • test/artifactRuntime.test.ts+325−0
  • test/codexPrimaryRuntime.test.ts+5−64
  • test/extensions.sourceFingerprint.test.ts+51−0
  • test/jsonrpc.workspace-route.test.ts+96−0
  • test/jsonrpc/flow.threads.test.ts+8−11
  • test/package-manifest.test.ts+1−0
  • test/plugins.catalog.test.ts+102−2
  • test/presentationPreview.test.ts+64−0
  • test/runtime.codex-app-server.test.ts+13−13
  • test/session.syntheticSkillInjection.test.ts+75−0
  • test/session/agentSession.messaging.test.ts+132−0
  • test/skills.marketplace.test.ts+63−0
  • test/skills.operations.test.ts+42−0
  • test/spreadsheetEdit.test.ts+887−0
  • test/spreadsheetPreview.test.ts+243−30