mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-30 16:23:35 +08:00
* feat(desktop): add CodeMirror deps + bundle budget + encoding utils
Phase 2 foundation pieces (tasks 6-7 of editor-lsp-foundation):
- Add 9 @codemirror/* dependencies (state, view, commands, search, language,
autocomplete, lang-{javascript,json,markdown}). Tree-shaking keeps the
current bundle delta at +0.00 KB until WorkspaceEditor.tsx imports them
in task 8.
- Add scripts/check-bundle-budget.ts + bun script "check:bundle-budget".
Asserts dist/assets/*.js gzipped total <= baseline + 100 KB. Baseline
captured at origin/main @ 95931d49 (post-R5, pre-CodeMirror) = 3299.76 KB.
- Add encodingDetect.ts: detectEncoding(bytes) -> 'utf-8' | 'utf-8-bom' |
'unsupported' (uses TextDecoder fatal:true to map invalid UTF-8 to
unsupported); detectLineEnding(text) -> 'LF' | 'CRLF' | 'CR' (dominant
style with LF as fallback for empty/single-line buffers).
Tested:
- bun run test src/components/workspace/encodingDetect.test.ts -- --run (18/18 pass)
- bun run scripts/check-bundle-budget.ts (OK, +0.00 KB delta)
_Requirements: 1.3, 1.6, 1.7, 1.8_
* feat(server,desktop): workspace file save endpoint + buffer/conflict store
Phase 2 backend pieces (tasks 9-12 of editor-lsp-foundation):
Server:
- src/server/events/workspaceFileSaved.ts: shared emitter
emitWorkspaceFileSaved() so both write paths (user now, agent in PR-4)
funnel through one well-typed WS message shape.
- src/server/services/workspaceFileService.ts: WriteWorkspaceFileSchema
(Zod), atomic write (temp file + fsync + rename, Windows EBUSY/EPERM
retry 3x50ms), realpath-based symlink containment, BOM/CRLF/CR
round-trip, sha256 stale-base check (409 on hash mismatch), structured
400/404/409/500 error bodies, post-write workspace.file.saved emit.
- src/server/api/sessions.ts: POST /api/sessions/:id/workspace/file
routes through new handleSessionWorkspacePost; GET path unchanged.
- src/server/services/workspaceFileService.test.ts: 17 tests covering
200 / 400 (bad request, path-escape, parent-missing, absolute-path) /
404 session-missing / 409 stale-base / BOM round-trip / CRLF round-trip /
CR round-trip / temp-file-not-leaked / 10 MiB content cap. Includes
symlink-escape coverage on POSIX (skipped on Windows where symlink
creation needs elevation).
Desktop store:
- desktop/src/stores/workspacePanelStore.ts: bufferStateByTabId record,
WorkspaceBufferState/Conflict types, initBuffer / setBufferState /
applyExternalSave / acknowledgeConflict / clearBuffer actions.
applyExternalSave is the WS-driven entry point — clean buffers rebase
silently when content is supplied; dirty buffers raise a conflict
banner. acknowledgeConflict('reload') resets to base, 'keepMine' /
'openConflict' just dismiss the banner. closePreviewTabs and
clearSession now drop matching buffer state to prevent leaks.
Tested:
- bun test src/server/services/workspaceFileService.test.ts (17/17 pass)
- bun run lint (server tsc clean)
- bun run test src/stores/workspacePanelStore.test.ts (24/24 pass — store
extension is purely additive, R5 tests still green)
Note: pre-existing e2e/business-flow failures in src/server/__tests__/e2e
remain untouched by this change (44 fails on baseline, no new fails
introduced).
_Requirements: 1.5, 2.1-2.9, 3.1-3.3_
* feat(desktop): WorkspaceEditor + ConflictBanner + UnsavedChangesModal
Phase 2 UI pieces (tasks 8, 13, 14, 15 of editor-lsp-foundation):
- desktop/src/components/workspace/WorkspaceEditor.tsx: CodeMirror 6
editor wrapping the workspace panel buffer state. Hooks
EditorView.updateListener -> setBufferState for live dirty tracking,
picks language by extension (ts/tsx/js/jsx/json/md), wires Save to
sessionsApi.saveWorkspaceFile (R2 atomic write endpoint), refreshes
base hash on success, falls back to a read-only message when
detectEncoding returns 'unsupported'. External rebases (e.g. clean
buffer applyExternalSave) are pushed back into the EditorView via
dispatch.
- desktop/src/components/workspace/ConflictBanner.tsx: clean buffer ->
single Reload button; dirty buffer -> three buttons (Reload (discard),
Keep mine, Open conflict view). Renders workspace-relative path,
hash first 8 hex, relative timestamp refreshing every 60 s.
- desktop/src/components/workspace/UnsavedChangesModal.tsx: Discard /
Save / Cancel modal. Cancel is the default focus, Esc triggers it.
While saving, all three buttons disable and the host calls onSave
which only resolves close on success. 30 s in-prompt timeout fires
onTimeout (host owns the toast).
- desktop/src/components/workspace/WorkspaceEditor.test.tsx: 8 RTL
tests covering buffer init / encoding detection / unsupported
fallback / dirty marker / unsaved-changes modal flows / conflict
banner clean-vs-dirty layouts.
- desktop/src/api/sessions.ts: SaveWorkspaceFileInput +
SaveWorkspaceFileResult types and sessionsApi.saveWorkspaceFile
POST helper.
Bundle delta: +0.28 KB gz (CodeMirror 6 + 3 lang packs imported but
WorkspaceEditor is not yet wired into WorkspacePanel — that integration
ships in a follow-up PR alongside Phase 3 LSP work, keeping this PR
focused on the foundation).
Tested:
- bun run lint (tsc --noEmit clean)
- bun run test (workspacePanelStore 24/24, encodingDetect 18/18,
WorkspaceEditor 8/8, WorkspacePanel 27/27 — 77/77 across 4 files)
- bun run check:bundle-budget (delta +0.28 KB gz, 99.72 KB headroom)
- bun run build (clean, 1.20s)
_Requirements: 1.1-1.8, 3.2-3.5, 4.1-4.6_
---------
Co-authored-by: 你的姓名 <you@example.com>