You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Problem — The current raw editor is a bare <textarea> with no line numbers, no syntax highlighting, and no visual affordance that it is a code editor. It looks unprofessional and is painful for editing TeX source, config files, or any structured text.
Approach — Replace the <textarea>RawEditor with a CodeMirror 6 editor component. CodeMirror 6 provides line numbers, syntax highlighting, a gutter, theme integration, and a proper editing experience (~150KB gzipped). Add language modes for LaTeX (.tex, .bib, .tikz, .sty, .cls), Markdown (.md), and plain text (.txt, .log). Wire auto-save (1s debounce + Cmd+S) with a design-token save indicator. Support a read-only mode for files that should not be edited (.log).
Scope — in: CodeMirror 6 integration, line numbers, syntax highlighting, language modes (LaTeX, Markdown, plain text), auto-save with status indicator, read-only mode, theme integration. out: vim/emacs keybindings (can be added later via CM6 extensions), minimap, multiple cursors, diff decorations (those belong in Files Changed if ever added).
Acceptance Criteria
codemirror, @codemirror/lang-markdown, @codemirror/lang-javascript, and @codemirror/legacy-modes (for LaTeX) are added as dependencies
A CodeMirrorEditor component accepts: file content (string), language hint (extension string), read-only flag, onSave callback, onChange callback
Line numbers are always visible in the gutter
Syntax highlighting works for: LaTeX (.tex, .bib, .tikz, .sty, .cls), Markdown (.md), plain text (.txt, .log), and any other language CM6 supports out of the box
Theme integrates with the V2 design token system — editor background uses --v2-background-bg-base, text uses --v2-text-text-base, gutter uses --v2-background-bg-layer-01, line numbers use --v2-text-text-muted. These are CSS custom properties from the harmoniqs theme JSON (packages/ui/src/theme/themes/harmoniqs.json), not --amc-* tokens (those are for the dashboard widget SDK only)
Dark/light theme switches correctly when the app theme changes
Auto-save fires 1 second after the last edit via the SDK's typed sdk().client.file.write({ path, content }) method (not raw fetch — the current SessionPreviewTab uses raw fetch to /file/write, but the SDK provides a typed wrapper at sdk.gen.ts:1957); Cmd+S triggers an immediate save
Save indicator: "Saving…" in --status-running (yellow), "Saved" in --status-done (green), clears after 2 seconds
Read-only mode: line numbers and syntax highlighting are present, but editing is disabled (for .log files)
The editor fills its container and is responsive to panel resizing
Tab key inserts the appropriate whitespace (2 spaces for most files, configurable)
Standard keybindings work: Cmd+Z (undo), Cmd+Shift+Z (redo), Cmd+A (select all), Cmd+C/V/X (copy/paste/cut) — the editor intercepts these before the app's global command system
Testing Decisions
Unit test the language-mode dispatch (extension string → CM6 language extension)
Unit test the auto-save debounce timing and Cmd+S immediate-save behavior (mock the file write API)
Unit test read-only mode — verify edits are rejected
Integration test: mount the editor with fixture content, verify line numbers render and syntax tokens are applied
Theme test: verify CSS custom properties are consumed (snapshot or computed-style check)
Key Decisions
CodeMirror 6, not Monaco. Monaco is VS Code's editor but is ~3MB and designed for standalone use — it bundles its own worker, layout engine, and theme system. CodeMirror 6 is modular (~150KB for our use case), integrates with any DOM layout, and its theme system maps cleanly to CSS custom properties. It is the right fit for an embedded editor in a panel.
LaTeX via @codemirror/legacy-modes. CM6 does not have a first-party LaTeX language package. The stex (Structured TeX) mode from CodeMirror 5 is available via @codemirror/legacy-modes/mode/stex and provides adequate syntax highlighting for LaTeX, BibTeX, and TikZ. If a better CM6-native LaTeX package emerges, it is a drop-in replacement.
Auto-save, not explicit save. Matches the existing SessionPreviewTab pattern. The 1-second debounce prevents hammering the server on every keystroke. Cmd+S is the escape hatch for immediate save. Use the SDK's typed file.write method rather than the raw fetch('/file/write', ...) pattern the existing code uses.
Bundle size. CodeMirror 6 with line numbers, syntax highlighting, and the stex legacy mode adds ~150KB gzipped. Combined with pdfjs-dist in slice 3 (~800KB), the total addition across both slices is ~1MB. This is within acceptable bounds for a panel that replaces a bare textarea with a professional editor + PDF viewer.
Design-token theme, not a canned CM6 theme. Build a custom CM6 theme that reads from --v2-* CSS custom properties (the V2 design token system defined in packages/ui/src/theme/themes/harmoniqs.json). Because the theme references var(--v2-*), it auto-switches between light and dark mode when ThemeProvider updates — no CM6 Compartment reconfiguration needed. Note: --amc-* tokens are for the dashboard widget SDK only, not the app.
Constraints & Invariants
The editor never writes to the filesystem without the user having typed (no phantom saves on mount)
Read-only mode is enforced by CM6's EditorState.readOnly — not a CSS pointer-events hack
The editor never captures Cmd+S when it is not focused
Theme changes apply without remounting the editor (CM6's Compartment system)
Prior Art
RawEditor in session-preview-tab.tsx — the bare <textarea> being replaced
Auto-save + POST /file/write — existing pattern in SessionPreviewTab
@codemirror/merge — considered for diff editing in Files Changed; not needed for this slice but the CM6 dependency is shared
Source
harmoniqs/opencode — new CodeMirrorEditor component in packages/app/src/components/, consumed by the renderer dispatch (slice 2)
Implementation Plan
Files to create
File
Repo
Purpose
packages/app/src/components/codemirror-editor.tsx
opencode
CodeMirrorEditor component: CM6 with line numbers, syntax highlighting, auto-save
packages/app/src/components/codemirror-theme.ts
opencode
Custom CM6 theme consuming --v2-* design tokens
packages/app/src/utils/codemirror-languages.ts
opencode
Language mode dispatch (extension → CM6 language extension)
Theme switching — the ThemeProvider at packages/ui/src/theme/context applies --v2-* variables to the root element. Because the CM6 theme references var(--v2-*), it auto-switches with no Compartment reconfiguration needed. The light/dark switch happens via AmicodeThemeBridge (app.tsx:412) calling theme.setColorScheme().
3. Language mode dispatch:
// codemirror-languages.tsimport{markdown}from"@codemirror/lang-markdown";import{StreamLanguage}from"@codemirror/language";import{stex}from"@codemirror/legacy-modes/mode/stex";importtype{Extension}from"@codemirror/state";exportfunctionlanguageForExtension(ext: string): Extension{switch(ext.toLowerCase()){case".md":
returnmarkdown();case".tex": case".bib": case".tikz": case".sty": case".cls":
returnStreamLanguage.define(stex);case".txt": case".log": default:
return[];// plain text — no language mode}}
4. Auto-save — replicate the debounce pattern from session-preview-tab.tsx:153-205 but use the SDK typed method:
import{useSDK}from"@/context/sdk";// Inside the component or a wrapper:constsdk=useSDK();letsaveTimer: ReturnType<typeofsetTimeout>|undefined;const[saveStatus,setSaveStatus]=createSignal<"idle"|"saving"|"saved">("idle");functiondebouncedSave(path: string,content: string){if(saveTimer)clearTimeout(saveTimer);saveTimer=setTimeout(()=>saveFile(path,content),1000);}asyncfunctionsaveFile(path: string,content: string){setSaveStatus("saving");try{awaitsdk().client.file.write({ path, content });setSaveStatus("saved");setTimeout(()=>setSaveStatus("idle"),2000);}catch{setSaveStatus("idle");}}
5. Save indicator — use --status-running and --status-done tokens from design-polish.css:105-117:
6. Cmd+S interception — CodeMirror's keymap handles Mod-s natively (see the mount code above). The existing RawEditor at session-preview-tab.tsx:411-416 uses e.stopPropagation() to prevent the app's global command system from stealing the shortcut. CM6's keymap does this automatically when run() returns true.
Bundle size
~150KB gzipped for codemirror + language modes + view/state/commands. Verify with npx vite build.
Verification
# Unit tests (opencode app uses bun test, not vitest)cd~/harmoniqs/opencode/packages/app && bun test --conditions=solid --preload ./happydom.ts src/utils/codemirror-languages.test.ts
cd~/harmoniqs/opencode/packages/app && bun test --conditions=solid --preload ./happydom.ts src/components/codemirror-editor.test.ts
# Test expectations:# - languageForExtension(".tex") returns StreamLanguage stex# - languageForExtension(".md") returns markdown()# - languageForExtension(".txt") returns empty array# - Auto-save debounce: edit → wait 1s → save fires (mock SDK)# - Cmd+S: immediate save fires (mock SDK)# - Read-only mode: edits rejected# Visual check in Extension Dev Host (F5):# 1. Open a .tex file → line numbers visible, LaTeX syntax highlighted# 2. Open a .md file in raw mode → markdown syntax highlighted# 3. Edit a file → "Saving..." appears in yellow, then "Saved" in green# 4. Press Cmd+S → immediate save# 5. Open a .log file → read-only (cursor but no editing)# 6. Switch light/dark theme → editor theme updates without reload
Preview — CodeMirror 6 editor (line numbers, syntax highlighting, auto-save)
Important
Problem — The current raw editor is a bare
<textarea>with no line numbers, no syntax highlighting, and no visual affordance that it is a code editor. It looks unprofessional and is painful for editing TeX source, config files, or any structured text.Approach — Replace the
<textarea>RawEditorwith a CodeMirror 6 editor component. CodeMirror 6 provides line numbers, syntax highlighting, a gutter, theme integration, and a proper editing experience (~150KB gzipped). Add language modes for LaTeX (.tex,.bib,.tikz,.sty,.cls), Markdown (.md), and plain text (.txt,.log). Wire auto-save (1s debounce + Cmd+S) with a design-token save indicator. Support a read-only mode for files that should not be edited (.log).Scope — in: CodeMirror 6 integration, line numbers, syntax highlighting, language modes (LaTeX, Markdown, plain text), auto-save with status indicator, read-only mode, theme integration. out: vim/emacs keybindings (can be added later via CM6 extensions), minimap, multiple cursors, diff decorations (those belong in Files Changed if ever added).
Acceptance Criteria
codemirror,@codemirror/lang-markdown,@codemirror/lang-javascript, and@codemirror/legacy-modes(for LaTeX) are added as dependenciesCodeMirrorEditorcomponent accepts: file content (string), language hint (extension string), read-only flag, onSave callback, onChange callback.tex,.bib,.tikz,.sty,.cls), Markdown (.md), plain text (.txt,.log), and any other language CM6 supports out of the box--v2-background-bg-base, text uses--v2-text-text-base, gutter uses--v2-background-bg-layer-01, line numbers use--v2-text-text-muted. These are CSS custom properties from the harmoniqs theme JSON (packages/ui/src/theme/themes/harmoniqs.json), not--amc-*tokens (those are for the dashboard widget SDK only)sdk().client.file.write({ path, content })method (not rawfetch— the currentSessionPreviewTabuses rawfetchto/file/write, but the SDK provides a typed wrapper atsdk.gen.ts:1957); Cmd+S triggers an immediate save--status-running(yellow), "Saved" in--status-done(green), clears after 2 seconds.logfiles)Testing Decisions
Key Decisions
CodeMirror 6, not Monaco. Monaco is VS Code's editor but is ~3MB and designed for standalone use — it bundles its own worker, layout engine, and theme system. CodeMirror 6 is modular (~150KB for our use case), integrates with any DOM layout, and its theme system maps cleanly to CSS custom properties. It is the right fit for an embedded editor in a panel.
LaTeX via
@codemirror/legacy-modes. CM6 does not have a first-party LaTeX language package. Thestex(Structured TeX) mode from CodeMirror 5 is available via@codemirror/legacy-modes/mode/stexand provides adequate syntax highlighting for LaTeX, BibTeX, and TikZ. If a better CM6-native LaTeX package emerges, it is a drop-in replacement.Auto-save, not explicit save. Matches the existing
SessionPreviewTabpattern. The 1-second debounce prevents hammering the server on every keystroke. Cmd+S is the escape hatch for immediate save. Use the SDK's typedfile.writemethod rather than the rawfetch('/file/write', ...)pattern the existing code uses.Bundle size. CodeMirror 6 with line numbers, syntax highlighting, and the
stexlegacy mode adds ~150KB gzipped. Combined withpdfjs-distin slice 3 (~800KB), the total addition across both slices is ~1MB. This is within acceptable bounds for a panel that replaces a bare textarea with a professional editor + PDF viewer.Design-token theme, not a canned CM6 theme. Build a custom CM6 theme that reads from
--v2-*CSS custom properties (the V2 design token system defined inpackages/ui/src/theme/themes/harmoniqs.json). Because the theme referencesvar(--v2-*), it auto-switches between light and dark mode whenThemeProviderupdates — no CM6Compartmentreconfiguration needed. Note:--amc-*tokens are for the dashboard widget SDK only, not the app.Constraints & Invariants
EditorState.readOnly— not a CSSpointer-eventshackCompartmentsystem)Prior Art
RawEditorinsession-preview-tab.tsx— the bare<textarea>being replacedPOST /file/write— existing pattern inSessionPreviewTab@codemirror/merge— considered for diff editing in Files Changed; not needed for this slice but the CM6 dependency is sharedSource
harmoniqs/opencode— newCodeMirrorEditorcomponent inpackages/app/src/components/, consumed by the renderer dispatch (slice 2)Implementation Plan
Files to create
packages/app/src/components/codemirror-editor.tsxCodeMirrorEditorcomponent: CM6 with line numbers, syntax highlighting, auto-savepackages/app/src/components/codemirror-theme.ts--v2-*design tokenspackages/app/src/utils/codemirror-languages.tsFiles to modify
packages/app/package.jsonpackages/app/src/components/session/preview-content-area.tsx<CodeMirrorEditor>packages/app/src/components/session/session-preview-tab.tsxRawEditor(lines 399-427) with<CodeMirrorEditor>for .md raw modeDependencies to add
{ "codemirror": "^6.0.0", "@codemirror/lang-markdown": "^6.0.0", "@codemirror/lang-javascript": "^6.0.0", "@codemirror/legacy-modes": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/search": "^6.0.0" }Integration points (exact)
1. SolidJS + CodeMirror mount pattern — follow the
@pierre/diffsWeb Component pattern atsession-ui/src/components/file-ssr.tsx:90-145(ref + onMount + createEffect + onCleanup):2. Theme using
--v2-*tokens — the codebase uses--v2-*CSS custom properties (NOT--amc-*). Key tokens frompackages/ui/src/theme/themes/harmoniqs.json:Theme switching — the
ThemeProvideratpackages/ui/src/theme/contextapplies--v2-*variables to the root element. Because the CM6 theme referencesvar(--v2-*), it auto-switches with noCompartmentreconfiguration needed. Thelight/darkswitch happens viaAmicodeThemeBridge(app.tsx:412) callingtheme.setColorScheme().3. Language mode dispatch:
4. Auto-save — replicate the debounce pattern from
session-preview-tab.tsx:153-205but use the SDK typed method:5. Save indicator — use
--status-runningand--status-donetokens fromdesign-polish.css:105-117:6. Cmd+S interception — CodeMirror's
keymaphandlesMod-snatively (see the mount code above). The existingRawEditoratsession-preview-tab.tsx:411-416usese.stopPropagation()to prevent the app's global command system from stealing the shortcut. CM6's keymap does this automatically whenrun()returnstrue.Bundle size
~150KB gzipped for
codemirror+ language modes + view/state/commands. Verify withnpx vite build.Verification