Files
HomerandClaude Opus 5.5 4b4e63f622 The app's own interface: agent chats first, two explorers, a viewer
Plain HTML, CSS and ES modules in ui/, no build step. The sidebar opens on agent
chats (this computer first), with chats on the server as the second tab; the
transcript draws the /api/v2 steps, follows a reply as it is written, answers
cards, and starts agent chats in one of the agent's folders. Two explorers kept
apart (this computer's, the server's) and a viewer: Markdown drawn by a safe
renderer that never parses HTML, code in read-only CodeMirror with the server's
token colours, HTML in the sealed frame, show calls opened in it. Settings for
the sign-in, the agent and its folders, notifications and updates; no admin
pages. Layout on fixed and shared tracks; the sidebar is a drawer below 48rem,
the viewer goes over the chat below 64rem. All 124 sentences in the server's
nine languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 04:55:03 +00:00

105 lines
4.4 KiB
JavaScript

// Source in the viewer: CodeMirror, read-only, from the server's own vendored build (ui/vendor,
// copied and pinned by scripts/fetch-vendor.sh). The look is the server's editor's: colours from
// the theme's tokens, never CodeMirror's own. Loaded the first time a file is shown.
let cm = null
async function load() {
if (cm) return cm
const [state, view, language, search, commands, highlight] = await Promise.all([
import("../vendor/cm-state.mjs"),
import("../vendor/cm-view.mjs"),
import("../vendor/cm-language.mjs"),
import("../vendor/cm-search.mjs"),
import("../vendor/cm-commands.mjs"),
import("../vendor/lezer-highlight.mjs"),
])
const { EditorView } = view
const { HighlightStyle, syntaxHighlighting } = language
const { tags } = highlight
// As the server's static/js/editor.js has them.
const look = EditorView.theme({
"&": { color: "var(--ink)", backgroundColor: "var(--code-bg)", fontSize: "var(--text-sm)", height: "100%" },
".cm-scroller": { fontFamily: "var(--font-mono)", lineHeight: "var(--leading-normal)" },
".cm-content": { caretColor: "var(--accent)" },
".cm-gutters": { backgroundColor: "var(--code-bg)", color: "var(--ink-faint)", border: "none" },
".cm-activeLine, .cm-activeLineGutter": { backgroundColor: "var(--surface-hover)" },
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection": { backgroundColor: "var(--selection)" },
"&.cm-focused": { outline: "none" },
".cm-panels": { backgroundColor: "var(--surface)", color: "var(--ink)" },
".cm-searchMatch": { backgroundColor: "var(--accent-soft)" },
})
const colours = HighlightStyle.define([
{ tag: [tags.keyword, tags.operatorKeyword, tags.modifier], color: "var(--accent)" },
{ tag: [tags.string, tags.special(tags.string)], color: "var(--leaf)" },
{ tag: [tags.comment], color: "var(--ink-faint)", fontStyle: "italic" },
{ tag: [tags.number, tags.bool, tags.null, tags.atom], color: "var(--warning)" },
{ tag: [tags.function(tags.variableName), tags.definition(tags.variableName)], color: "var(--ink)", fontWeight: "600" },
{ tag: [tags.heading], color: "var(--accent)", fontWeight: "700" },
{ tag: [tags.link, tags.url], color: "var(--accent)", textDecoration: "underline" },
{ tag: [tags.tagName, tags.typeName, tags.className], color: "var(--accent-hover)" },
{ tag: [tags.invalid], color: "var(--danger)" },
])
cm = {
state,
view,
base: [
view.lineNumbers(),
view.highlightActiveLine(),
view.highlightActiveLineGutter(),
view.drawSelection(),
view.keymap.of([...search.searchKeymap, ...commands.defaultKeymap]),
search.highlightSelectionMatches(),
state.EditorState.readOnly.of(true),
view.EditorView.editable.of(false),
view.EditorView.lineWrapping,
look,
syntaxHighlighting(colours),
],
}
return cm
}
/** The language support for a name as `show` gives it; nothing for one without support. */
async function language(name) {
try {
switch (name) {
case "markdown":
return (await import("../vendor/cm-lang-markdown.mjs")).markdown()
case "python":
return (await import("../vendor/cm-lang-python.mjs")).python()
case "javascript":
case "jsx":
return (await import("../vendor/cm-lang-javascript.mjs")).javascript({ jsx: name === "jsx" })
case "typescript":
case "tsx":
return (await import("../vendor/cm-lang-javascript.mjs")).javascript({ typescript: true, jsx: name === "tsx" })
case "html":
return (await import("../vendor/cm-lang-html.mjs")).html()
case "css":
case "scss":
return (await import("../vendor/cm-lang-css.mjs")).css()
case "json":
return (await import("../vendor/cm-lang-json.mjs")).json()
default:
return []
}
} catch {
return []
}
}
/** Show `text` in `parent`, read-only, at `line` when given. Returns something to `destroy()`. */
export async function codeView(parent, text, { lang = "", line = 0 } = {}) {
const { state, view, base } = await load()
const ed = new view.EditorView({
parent,
state: state.EditorState.create({ doc: text, extensions: [base, await language(lang)] }),
})
if (line > 0 && line <= ed.state.doc.lines) {
const at = ed.state.doc.line(line).from
ed.dispatch({ selection: { anchor: at }, effects: view.EditorView.scrollIntoView(at, { y: "center" }) })
}
return ed
}