fix: session-cwd attach race, terminal failure surfacing, white-screen hardening; CodeMirror editor

- cwd resolution falls back (attached header → client summary cwd → process
  cwd) across the JSON API, media route and terminal upgrade, so explorer/
  git/terminal work from first paint instead of 'session ... has no working
  directory' while the web client hydrates the session (refresh-only fix).
- terminal client threads the summary cwd into the WS URL and stops the
  reconnect loop after three unreasoned failures, showing the close code
  with a manual retry; a 1011+reason refusal already showed the reason.
- white screen: persisted state is structurally sanitized on load (malformed
  shapes fall back to default) and the restored width is clamped to the
  viewport; the layout margin is capped at the viewport; the client entry
  wraps every lifecycle step in try/catch and pins a visible diagnostic bar
  on load/mount/interception failure instead of failing silently.
- editor: textarea replaced with CodeMirror 6 — line wrapping, per-extension
  syntax highlighting (js/ts/jsx/tsx, json, python, html, css, xml, yaml,
  sql, java, c/c++, rust, go, php, shell, toml, nginx, dockerfile,
  properties), one-dark theme, dirty dot + Ctrl/Cmd+S save kept.
- tests: +9 (cwd fallback chain over the API route, state sanitization,
  language mapping); 36 pass, typecheck + build + vm materialize clean.
This commit is contained in:
Menghuan1918
2026-08-08 15:12:56 +08:00
parent 8950e2ae69
commit 47ebffcf9b
17 changed files with 1063 additions and 140 deletions
+92
View File
@@ -0,0 +1,92 @@
/**
* Syntax highlighting for the file editor: extension → CodeMirror language
* mapping. The key derivation is pure and unit-tested; the factories pull in
* the CodeMirror language packages (bundled into the client).
*/
import { Language, LanguageSupport, StreamLanguage } from '@codemirror/language'
import { javascript } from '@codemirror/lang-javascript'
import { json } from '@codemirror/lang-json'
import { python } from '@codemirror/lang-python'
import { html } from '@codemirror/lang-html'
import { css } from '@codemirror/lang-css'
import { xml } from '@codemirror/lang-xml'
import { yaml } from '@codemirror/lang-yaml'
import { sql } from '@codemirror/lang-sql'
import { java } from '@codemirror/lang-java'
import { cpp } from '@codemirror/lang-cpp'
import { rust } from '@codemirror/lang-rust'
import { go } from '@codemirror/lang-go'
import { php } from '@codemirror/lang-php'
import { shell } from '@codemirror/legacy-modes/mode/shell'
import { toml } from '@codemirror/legacy-modes/mode/toml'
import { nginx } from '@codemirror/legacy-modes/mode/nginx'
import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile'
import { properties } from '@codemirror/legacy-modes/mode/properties'
/** The lowercased file extension of a path ('' when none). */
export function extOf(path: string): string {
const at = path.lastIndexOf('.')
if (at === -1) return ''
const base = path.slice(at + 1).toLowerCase()
return base.includes('/') || base.includes('\\') ? '' : base
}
/** Language key for an extension, or null for plain text. Pure (tested). */
export function languageKeyForExt(ext: string): string | null {
switch (ext) {
case 'js': case 'mjs': case 'cjs': return 'js'
case 'jsx': return 'jsx'
case 'ts': case 'mts': case 'cts': return 'ts'
case 'tsx': return 'tsx'
case 'json': case 'jsonc': return 'json'
case 'py': case 'pyw': return 'python'
case 'html': case 'htm': return 'html'
case 'css': return 'css'
case 'xml': case 'xsl': return 'xml'
case 'yaml': case 'yml': return 'yaml'
case 'sql': return 'sql'
case 'java': return 'java'
case 'c': case 'h': return 'c'
case 'cc': case 'cpp': case 'cxx': case 'hpp': case 'hh': case 'hxx': return 'cpp'
case 'rs': return 'rust'
case 'go': return 'go'
case 'php': return 'php'
case 'sh': case 'bash': case 'zsh': return 'shell'
case 'toml': return 'toml'
case 'nginx': case 'conf': return 'nginx'
case 'dockerfile': case 'docker': return 'dockerfile'
case 'properties': case 'env': return 'properties'
default: return null
}
}
const FACTORIES: Record<string, () => Language | LanguageSupport> = {
js: () => javascript({ jsx: true }),
jsx: () => javascript({ jsx: true }),
ts: () => javascript({ typescript: true }),
tsx: () => javascript({ typescript: true, jsx: true }),
json: () => json(),
python: () => python(),
html: () => html(),
css: () => css(),
xml: () => xml(),
yaml: () => yaml(),
sql: () => sql(),
java: () => java(),
c: () => cpp(),
cpp: () => cpp(),
rust: () => rust(),
go: () => go(),
php: () => php(),
shell: () => StreamLanguage.define(shell),
toml: () => StreamLanguage.define(toml),
nginx: () => StreamLanguage.define(nginx),
dockerfile: () => StreamLanguage.define(dockerFile),
properties: () => StreamLanguage.define(properties),
}
/** The CodeMirror language support for a path, or null for plain text. */
export function languageForPath(path: string): Language | LanguageSupport | null {
const key = languageKeyForExt(extOf(path))
return key === null ? null : FACTORIES[key]!()
}