Changeset 0.17.2 (#77)

This commit is contained in:
2026-02-28 11:58:27 +00:00
parent 856dc9b0ac
commit a008dac488
26 changed files with 3018 additions and 116 deletions

View File

@@ -6,7 +6,7 @@
========================================== */
:root {
/* ── Core palette ────────────────────────── */
/* ── Core palette (Dark — default) ────────── */
--bg: #0e0e10;
--bg-surface: #18181b;
--bg-raised: #222227;
@@ -60,6 +60,44 @@
--banner-fg: transparent;
}
/* ── Light Theme ─────────────────────────────── */
[data-theme="light"] {
--bg: #ffffff;
--bg-surface: #f8f9fa;
--bg-raised: #f0f1f3;
--bg-hover: #e8e9ec;
--border: #d5d7db;
--border-light: #c0c3c9;
--text: #1a1a2e;
--text-2: #555770;
--text-3: #8b8da3;
--accent: #4a7cdb;
--accent-hover: #3968c4;
--accent-dim: rgba(74,124,219,0.10);
--danger: #dc2626;
--success: #16a34a;
--warning: #ca8a04;
--purple: #7c3aed;
--purple-dim: rgba(124,58,237,0.08);
--text-on-color: #fff;
--text-on-warning: #000;
--accent-light: #2563eb;
--danger-light: #dc2626;
--success-light: #16a34a;
--warning-light: #ca8a04;
--danger-dim: rgba(220,38,38,0.10);
--success-dim: rgba(22,163,74,0.10);
--warning-dim: rgba(202,138,4,0.10);
--overlay: rgba(0,0,0,0.35);
--glass: rgba(0,0,0,0.02);
color-scheme: light;
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; }
body { font-family: var(--font); background: var(--bg); color: var(--text); overflow: hidden; font-size: 14px; }
@@ -912,6 +950,27 @@ a:hover { text-decoration: underline; }
.input-wrap textarea:focus { outline: none; }
.input-wrap textarea::placeholder { color: var(--text-3); }
/* CM6 chat input — match textarea layout within .input-wrap */
#messageInputWrap {
flex: 1; min-width: 0;
}
#messageInputWrap .cm-editor {
background: transparent;
padding: 12px 0 12px 8px;
max-height: 200px;
font-family: var(--font);
font-size: 14px;
line-height: 1.5;
}
#messageInputWrap .cm-editor.cm-focused { outline: none; }
#messageInputWrap .cm-scroller { overflow-y: auto; }
/* CM6 extension editor containers */
.ext-edit-form .cm-editor {
height: 100%;
font-size: 12px;
}
.input-actions { display: flex; align-items: center; gap: 2px; padding: 6px 8px; }
/* Left-side toolbar: attach, tools, KB — single container handles alignment */
@@ -1276,6 +1335,25 @@ select option { background: var(--bg-surface); color: var(--text); }
.range-labels { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-3); }
.checkbox-label { display: flex; align-items: center; gap: 8px; font-size: 14px; cursor: pointer; margin: 0.5rem 0; color: var(--text-2); }
/* ── Theme Toggle ────────────────────────── */
.theme-toggle {
display: flex; gap: 4px; padding: 3px;
background: var(--bg); border: 1px solid var(--border);
border-radius: var(--radius); width: fit-content;
}
.theme-btn {
display: flex; align-items: center; gap: 6px;
padding: 6px 14px; border-radius: calc(var(--radius) - 2px);
border: none; background: none; color: var(--text-3);
font-family: var(--font); font-size: 13px; font-weight: 500;
cursor: pointer; transition: all var(--transition);
}
.theme-btn:hover { color: var(--text); background: var(--bg-hover); }
.theme-btn.active {
color: var(--text); background: var(--bg-raised);
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
}
/* ── Modal ────────────────────────────────── */
.modal-overlay {

54
src/editor/build.mjs Normal file
View File

@@ -0,0 +1,54 @@
// ==========================================
// Chat Switchboard — CM6 Build Script
// ==========================================
// Bundles CodeMirror 6 into a single IIFE that
// exposes window.CM. Run via scripts/build-editor.sh
// or directly: node build.mjs [--outdir=path]
//
// Output:
// codemirror.bundle.js (~295KB min, ~90KB gzip)
// codemirror.bundle.css (~5-10KB)
// ==========================================
import { build } from 'esbuild';
import { resolve, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
// Parse --outdir=<path> from argv
let outdir = resolve(__dirname, 'dist');
for (const arg of process.argv.slice(2)) {
if (arg.startsWith('--outdir=')) {
outdir = resolve(arg.slice('--outdir='.length));
}
}
try {
const result = await build({
entryPoints: [resolve(__dirname, 'index.mjs')],
bundle: true,
minify: true,
format: 'iife',
target: ['es2020'],
outfile: resolve(outdir, 'codemirror.bundle.js'),
// Metafile for optional size analysis
metafile: true,
// Log level
logLevel: 'info',
});
// Print bundle size summary
if (result.metafile) {
const outputs = result.metafile.outputs;
for (const [file, info] of Object.entries(outputs)) {
const kb = (info.bytes / 1024).toFixed(1);
console.log(` ${file}: ${kb} KB`);
}
}
console.log(`✅ CM6 bundle → ${outdir}`);
} catch (err) {
console.error('❌ CM6 build failed:', err.message);
process.exit(1);
}

310
src/editor/chat-input.mjs Normal file
View File

@@ -0,0 +1,310 @@
// ==========================================
// Chat Switchboard — CM6 Chat Input Factory
// ==========================================
// Minimal markdown editor for the chat message
// input. No line numbers, no gutter. Enter sends,
// Shift+Enter inserts newline. Auto-growing height.
//
// Usage:
// const editor = CM.chatInput(container, {
// placeholder: 'Send a message...',
// onSubmit: (text) => sendMessage(text),
// onChange: (text) => updateInputTokens(text),
// maxHeight: 200,
// darkMode: true,
// });
//
// editor.getValue()
// editor.setValue(str)
// editor.focus()
// editor.destroy()
// ==========================================
import { EditorView, keymap, drawSelection, dropCursor,
highlightSpecialChars, placeholder as placeholderExt,
ViewPlugin, Decoration } from '@codemirror/view';
import { EditorState, Compartment, RangeSetBuilder } from '@codemirror/state';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { syntaxHighlighting, defaultHighlightStyle, syntaxTree } from '@codemirror/language';
import { chatInputTheme } from './theme.mjs';
// ── Auto-Height Extension ───────────────────
/**
* Extension that auto-grows the editor height to fit content,
* up to maxHeight pixels. Mirrors the existing textarea behavior:
* this.style.height = 'auto';
* this.style.height = Math.min(this.scrollHeight, 200) + 'px';
*/
function autoHeight(maxHeight = 200) {
return EditorView.updateListener.of((update) => {
if (update.docChanged || update.geometryChanged) {
const dom = update.view.dom;
dom.style.height = 'auto';
const scrollH = update.view.contentDOM.scrollHeight;
// Account for .cm-editor padding (12px top + 12px bottom from wrapper CSS)
const padV = 24;
const targetH = Math.min(scrollH + padV, maxHeight);
dom.style.height = targetH + 'px';
dom.style.overflowY = scrollH + padV > maxHeight ? 'auto' : 'hidden';
}
});
}
// ── Fenced Code Block Decorations ───────────
//
// Scans the markdown syntax tree for FencedCode nodes
// and applies line decorations so they render as visual
// code blocks (dark background, monospace, rounded corners)
// inside the chat input — matching the claude.ai UX.
const codeBlockDeco = ViewPlugin.fromClass(class {
decorations;
constructor(view) {
this.decorations = this.buildDecorations(view);
}
update(update) {
// Rebuild when content changes, viewport shifts, or the parser
// finishes an incremental parse (tree reference changes)
if (update.docChanged || update.viewportChanged ||
syntaxTree(update.startState) !== syntaxTree(update.state)) {
this.decorations = this.buildDecorations(update.view);
}
}
buildDecorations(view) {
const builder = new RangeSetBuilder();
const tree = syntaxTree(view.state);
const doc = view.state.doc;
tree.iterate({
enter(node) {
if (node.name !== 'FencedCode') return;
const startLine = doc.lineAt(node.from);
const endLine = doc.lineAt(node.to);
for (let ln = startLine.number; ln <= endLine.number; ln++) {
const line = doc.line(ln);
let cls;
if (ln === startLine.number) {
cls = 'cm-codeblock cm-codeblock-open';
} else if (ln === endLine.number) {
cls = 'cm-codeblock cm-codeblock-close';
} else {
cls = 'cm-codeblock cm-codeblock-mid';
}
// Single-line code block (just ```)
if (startLine.number === endLine.number) {
cls = 'cm-codeblock cm-codeblock-open cm-codeblock-close';
}
builder.add(line.from, line.from, Decoration.line({ class: cls }));
}
},
});
return builder.finish();
}
}, {
decorations: (v) => v.decorations,
});
// ── Code Block Shortcuts ────────────────────
/**
* Input handler: triple backtick → fenced code block.
* Triggers when user types the third ` completing ```
* at the start of a line (possibly preceded only by whitespace/text).
*/
function tripleBacktickHandler() {
return EditorView.inputHandler.of((view, from, to, text) => {
if (text !== '`') return false;
const line = view.state.doc.lineAt(from);
const textBeforeCursor = view.state.doc.sliceString(line.from, from);
// Match `` at end of line (completing ```) — only at line start
if (/^``$/.test(textBeforeCursor.trimStart()) && textBeforeCursor.trimStart() === '``') {
const leadingWhitespace = textBeforeCursor.match(/^(\s*)/)[1];
const block = leadingWhitespace + '```\n' + leadingWhitespace + '\n' + leadingWhitespace + '```';
view.dispatch({
changes: { from: line.from, to, insert: block },
selection: { anchor: line.from + leadingWhitespace.length + 4 },
});
return true;
}
return false;
});
}
/**
* Keybinding: Ctrl/Cmd+E wraps selection in inline code backticks,
* or inserts a pair and places cursor between them.
*/
function inlineCodeKeymap() {
return keymap.of([{
key: 'Mod-e',
run: (view) => {
const { from, to } = view.state.selection.main;
if (from !== to) {
const text = view.state.doc.sliceString(from, to);
view.dispatch({
changes: { from, to, insert: '`' + text + '`' },
selection: { anchor: from + 1, head: to + 1 },
});
} else {
view.dispatch({
changes: { from, to: from, insert: '``' },
selection: { anchor: from + 1 },
});
}
return true;
},
}]);
}
// ── Chat Input Factory ──────────────────────
/**
* Create a chat message input with markdown highlighting.
*
* @param {HTMLElement} target - Container element
* @param {Object} opts
* @param {string} [opts.placeholder='Send a message...']
* @param {Function} [opts.onSubmit] - Called with text on Enter (without Shift)
* @param {Function} [opts.onChange] - Called with text on every edit
* @param {number} [opts.maxHeight=200] - Max height in pixels before scrolling
* @param {boolean} [opts.darkMode] - Auto-detected from body if omitted
* @returns {{ getValue, setValue, focus, getView, destroy }}
*/
export function chatInput(target, opts = {}) {
const {
placeholder: placeholderText = 'Send a message...',
onSubmit = null,
onChange = null,
maxHeight = 200,
darkMode = document.documentElement.getAttribute('data-theme') !== 'light',
} = opts;
// Build extensions
const extensions = [
// Markdown highlighting
markdown({ base: markdownLanguage }),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
// Code block shortcuts
tripleBacktickHandler(), // ``` → fenced code block
inlineCodeKeymap(), // Ctrl/Cmd+E → inline code
// Code block visual decorations (WYSIWYG style)
codeBlockDeco,
// Base editing
highlightSpecialChars(),
history(),
drawSelection(),
dropCursor(),
closeBrackets(),
// Theme — minimal chrome, no gutters
chatInputTheme,
// Placeholder
placeholderExt(placeholderText),
// Auto-growing height
autoHeight(maxHeight),
// Keymaps — chat-specific: Enter sends, Shift+Enter newline
keymap.of([
// Enter to submit (must be before defaultKeymap)
{
key: 'Enter',
run: (view) => {
if (onSubmit) {
const text = view.state.doc.toString();
onSubmit(text);
return true;
}
return false;
},
},
// Shift+Enter to insert newline
{
key: 'Shift-Enter',
run: (view) => {
view.dispatch(
view.state.replaceSelection('\n')
);
return true;
},
},
...closeBracketsKeymap,
...defaultKeymap,
...historyKeymap,
]),
];
// Change listener
if (onChange) {
extensions.push(EditorView.updateListener.of((update) => {
if (update.docChanged) {
onChange(update.state.doc.toString());
}
}));
}
// Enable spell check and standard text input attributes
extensions.push(
EditorView.contentAttributes.of({
spellcheck: 'true',
autocapitalize: 'sentences',
autocorrect: 'on',
})
);
// Single-line display mode: wrap long lines
extensions.push(EditorView.lineWrapping);
const view = new EditorView({
state: EditorState.create({ doc: '', extensions }),
parent: target,
});
// ── Public API ──────────────────────────
return {
getValue() {
return view.state.doc.toString();
},
setValue(text) {
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: text || '' },
});
},
focus() {
view.focus();
},
/** Access the underlying EditorView */
getView() {
return view;
},
/** Clean up */
destroy() {
view.destroy();
},
};
}

241
src/editor/code-editor.mjs Normal file
View File

@@ -0,0 +1,241 @@
// ==========================================
// Chat Switchboard — CM6 Code Editor Factory
// ==========================================
// Full-featured code editor: line numbers, bracket
// matching, search/replace, auto-indent, syntax
// highlighting. Used for extension editing and
// future code surfaces.
//
// Usage:
// const editor = CM.codeEditor(container, {
// language: 'javascript',
// value: 'const x = 1;',
// lineNumbers: true,
// darkMode: true,
// readOnly: false,
// keymap: 'standard', // 'standard' | 'vim' | 'emacs'
// onChange: (value) => {},
// });
//
// editor.getValue()
// editor.setValue(str)
// editor.focus()
// editor.destroy()
// ==========================================
import { EditorView, keymap, lineNumbers, highlightActiveLine,
highlightActiveLineGutter, drawSelection, dropCursor,
rectangularSelection, crosshairCursor,
highlightSpecialChars, placeholder as placeholderExt } from '@codemirror/view';
import { EditorState, Compartment } from '@codemirror/state';
import { defaultKeymap, indentWithTab, history, historyKeymap,
undo, redo } from '@codemirror/commands';
import { bracketMatching, indentOnInput, foldGutter, foldKeymap,
syntaxHighlighting, defaultHighlightStyle,
HighlightStyle } from '@codemirror/language';
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
import { autocompletion, completionKeymap, closeBrackets,
closeBracketsKeymap } from '@codemirror/autocomplete';
import { oneDark } from '@codemirror/theme-one-dark';
import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { sql } from '@codemirror/lang-sql';
import { html } from '@codemirror/lang-html';
import { css } from '@codemirror/lang-css';
import { yaml } from '@codemirror/lang-yaml';
import { go } from '@codemirror/lang-go';
import { python } from '@codemirror/lang-python';
import { rust } from '@codemirror/lang-rust';
import { vim } from '@replit/codemirror-vim';
import { emacs } from '@replit/codemirror-emacs';
import { switchboardTheme } from './theme.mjs';
// ── Language Registry ────────────────────────
const LANGUAGES = {
javascript, js: javascript,
json,
markdown, md: markdown,
sql,
html,
css,
yaml, yml: yaml,
go,
python, py: python,
rust, rs: rust,
};
/**
* Resolve a language name to a CM6 LanguageSupport instance.
* Returns null if the language isn't bundled.
*/
function resolveLanguage(name) {
if (!name) return null;
const factory = LANGUAGES[name.toLowerCase()];
return factory ? factory() : null;
}
// ── Code Editor Factory ─────────────────────
/**
* Create a full-featured code editor.
*
* @param {HTMLElement} target - Container element (editor replaces its content)
* @param {Object} opts
* @param {string} [opts.language] - Language mode name
* @param {string} [opts.value=''] - Initial content
* @param {boolean} [opts.lineNumbers=true]
* @param {boolean} [opts.darkMode] - Auto-detected from body if omitted
* @param {boolean} [opts.readOnly=false]
* @param {string} [opts.keymap='standard'] - 'standard' | 'vim' | 'emacs'
* @param {string} [opts.placeholder] - Placeholder text
* @param {Function} [opts.onChange] - Called with new value on every edit
* @returns {{ getValue, setValue, focus, getView, destroy }}
*/
export function codeEditor(target, opts = {}) {
const {
language = null,
value = '',
lineNumbers: showLineNumbers = true,
darkMode = document.documentElement.getAttribute('data-theme') !== 'light',
readOnly = false,
keymap: keymapMode = 'standard',
placeholder: placeholderText = '',
onChange = null,
} = opts;
// Compartments for reconfigurable extensions
const langCompartment = new Compartment();
const keymapCompartment = new Compartment();
const readOnlyCompartment = new Compartment();
const themeCompartment = new Compartment();
// Build extensions list
const extensions = [
// Base editing
highlightSpecialChars(),
history(),
drawSelection(),
dropCursor(),
EditorState.allowMultipleSelections.of(true),
indentOnInput(),
bracketMatching(),
closeBrackets(),
autocompletion(),
rectangularSelection(),
crosshairCursor(),
highlightActiveLine(),
highlightSelectionMatches(),
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
// Keymaps
keymap.of([
...closeBracketsKeymap,
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
indentWithTab,
]),
// Theme
switchboardTheme,
themeCompartment.of(darkMode ? [oneDark] : []),
// Configurable compartments
langCompartment.of(resolveLanguage(language) || []),
keymapCompartment.of(_resolveKeymapExt(keymapMode)),
readOnlyCompartment.of(EditorState.readOnly.of(readOnly)),
// Optional features
...(showLineNumbers ? [lineNumbers(), highlightActiveLineGutter(), foldGutter()] : []),
...(placeholderText ? [placeholderExt(placeholderText)] : []),
];
// Change listener
if (onChange) {
extensions.push(EditorView.updateListener.of((update) => {
if (update.docChanged) {
onChange(update.state.doc.toString());
}
}));
}
// Create the editor
const view = new EditorView({
state: EditorState.create({ doc: value, extensions }),
parent: target,
});
// ── Public API ──────────────────────────
return {
getValue() {
return view.state.doc.toString();
},
setValue(text) {
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: text },
});
},
focus() {
view.focus();
},
/** Access the underlying EditorView for advanced use */
getView() {
return view;
},
/** Switch language mode at runtime */
setLanguage(name) {
const lang = resolveLanguage(name);
view.dispatch({
effects: langCompartment.reconfigure(lang || []),
});
},
/** Switch keymap mode at runtime */
setKeymap(mode) {
view.dispatch({
effects: keymapCompartment.reconfigure(_resolveKeymapExt(mode)),
});
},
/** Toggle read-only state */
setReadOnly(ro) {
view.dispatch({
effects: readOnlyCompartment.reconfigure(
EditorState.readOnly.of(ro)
),
});
},
/** Toggle dark/light mode (controls oneDark syntax theme) */
setDarkMode(dark) {
view.dispatch({
effects: themeCompartment.reconfigure(dark ? [oneDark] : []),
});
},
/** Clean up — removes the editor from the DOM */
destroy() {
view.destroy();
},
};
}
// ── Keybinding Resolution ───────────────────
function _resolveKeymapExt(mode) {
if (mode === 'vim') return vim();
if (mode === 'emacs') return emacs();
return [];
}

61
src/editor/index.mjs Normal file
View File

@@ -0,0 +1,61 @@
// ==========================================
// Chat Switchboard — CM6 Bundle Entrypoint
// ==========================================
// esbuild compiles this into an IIFE that sets
// window.CM with factory functions and utilities.
//
// Consumers:
// if (window.CM) {
// const editor = CM.chatInput(el, opts);
// const code = CM.codeEditor(el, opts);
// }
// ==========================================
import { EditorView } from '@codemirror/view';
import { EditorState } from '@codemirror/state';
import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json';
import { markdown } from '@codemirror/lang-markdown';
import { sql } from '@codemirror/lang-sql';
import { html } from '@codemirror/lang-html';
import { css } from '@codemirror/lang-css';
import { yaml } from '@codemirror/lang-yaml';
import { go } from '@codemirror/lang-go';
import { python } from '@codemirror/lang-python';
import { rust } from '@codemirror/lang-rust';
import { vim } from '@replit/codemirror-vim';
import { emacs } from '@replit/codemirror-emacs';
import { codeEditor } from './code-editor.mjs';
import { chatInput } from './chat-input.mjs';
// ── Expose on window ────────────────────────
window.CM = {
// Version (injected by the build script or matched to app version)
version: '0.17.2',
// Low-level access (for advanced use / debug console)
EditorView,
EditorState,
// Factory: full-featured code editor
// CM.codeEditor(container, { language, value, lineNumbers, darkMode, keymap, onChange })
codeEditor,
// Factory: chat message input (markdown mode, minimal chrome)
// CM.chatInput(container, { placeholder, onSubmit, onChange, maxHeight, darkMode })
chatInput,
// Bundled language modes (for reference / dynamic use)
languages: {
javascript, json, markdown, sql, html, css, yaml, go, python, rust,
},
// Keybinding modes (for reference / settings UI)
keybindings: { vim, emacs },
};
console.log('[CM6] CodeMirror bundle loaded (v' + window.CM.version + ')');

917
src/editor/package-lock.json generated Normal file
View File

@@ -0,0 +1,917 @@
{
"name": "editor",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@codemirror/autocomplete": "^6",
"@codemirror/commands": "^6",
"@codemirror/lang-css": "^6",
"@codemirror/lang-go": "^6",
"@codemirror/lang-html": "^6",
"@codemirror/lang-javascript": "^6",
"@codemirror/lang-json": "^6",
"@codemirror/lang-markdown": "^6",
"@codemirror/lang-python": "^6",
"@codemirror/lang-rust": "^6",
"@codemirror/lang-sql": "^6",
"@codemirror/lang-yaml": "^6",
"@codemirror/language": "^6",
"@codemirror/search": "^6",
"@codemirror/state": "^6",
"@codemirror/theme-one-dark": "^6",
"@codemirror/view": "^6",
"@replit/codemirror-emacs": "^6",
"@replit/codemirror-vim": "^6"
},
"devDependencies": {
"esbuild": "^0.25.0"
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.0.tgz",
"integrity": "sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.10.2",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz",
"integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.4.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/lang-css": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz",
"integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.2",
"@lezer/css": "^1.1.7"
}
},
"node_modules/@codemirror/lang-go": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz",
"integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.0.0",
"@lezer/go": "^1.0.0"
}
},
"node_modules/@codemirror/lang-html": {
"version": "6.4.11",
"resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz",
"integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
"@codemirror/lang-javascript": "^6.0.0",
"@codemirror/language": "^6.4.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/css": "^1.1.0",
"@lezer/html": "^1.3.12"
}
},
"node_modules/@codemirror/lang-javascript": {
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
"integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.6.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0",
"@lezer/javascript": "^1.0.0"
}
},
"node_modules/@codemirror/lang-json": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.2.tgz",
"integrity": "sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/json": "^1.0.0"
}
},
"node_modules/@codemirror/lang-markdown": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz",
"integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.7.1",
"@codemirror/lang-html": "^6.0.0",
"@codemirror/language": "^6.3.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.2.1",
"@lezer/markdown": "^1.0.0"
}
},
"node_modules/@codemirror/lang-python": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz",
"integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.3.2",
"@codemirror/language": "^6.8.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.1",
"@lezer/python": "^1.1.4"
}
},
"node_modules/@codemirror/lang-rust": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz",
"integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@lezer/rust": "^1.0.0"
}
},
"node_modules/@codemirror/lang-sql": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz",
"integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@codemirror/lang-yaml": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.2.tgz",
"integrity": "sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.2.0",
"@lezer/lr": "^1.0.0",
"@lezer/yaml": "^1.0.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.12.2",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz",
"integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.9.4",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.4.tgz",
"integrity": "sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.35.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.6.0",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.6.0.tgz",
"integrity": "sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.37.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz",
"integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/theme-one-dark": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
"integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.39.15",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.15.tgz",
"integrity": "sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@lezer/common": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz",
"integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==",
"license": "MIT"
},
"node_modules/@lezer/css": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.1.tgz",
"integrity": "sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/@lezer/go": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz",
"integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/@lezer/highlight": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.3.0"
}
},
"node_modules/@lezer/html": {
"version": "1.3.13",
"resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz",
"integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/javascript": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz",
"integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.1.3",
"@lezer/lr": "^1.3.0"
}
},
"node_modules/@lezer/json": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.3.tgz",
"integrity": "sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.8.tgz",
"integrity": "sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@lezer/markdown": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz",
"integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0"
}
},
"node_modules/@lezer/python": {
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz",
"integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/rust": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz",
"integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0"
}
},
"node_modules/@lezer/yaml": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.4.tgz",
"integrity": "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.2.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.4.0"
}
},
"node_modules/@marijn/find-cluster-break": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"license": "MIT"
},
"node_modules/@replit/codemirror-emacs": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@replit/codemirror-emacs/-/codemirror-emacs-6.1.0.tgz",
"integrity": "sha512-74DITnht6Cs6sHg02PQ169IKb1XgtyhI9sLD0JeOFco6Ds18PT+dkD8+DgXBDokne9UIFKsBbKPnpFRAz60/Lw==",
"license": "MIT",
"peerDependencies": {
"@codemirror/autocomplete": "^6.0.2",
"@codemirror/commands": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.1",
"@codemirror/view": "^6.3.0"
}
},
"node_modules/@replit/codemirror-vim": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/@replit/codemirror-vim/-/codemirror-vim-6.3.0.tgz",
"integrity": "sha512-aTx931ULAMuJx6xLf7KQDOL7CxD+Sa05FktTDrtLaSy53uj01ll3Zf17JdKsriER248oS55GBzg0CfCTjEneAQ==",
"license": "MIT",
"peerDependencies": {
"@codemirror/commands": "6.x.x",
"@codemirror/language": "6.x.x",
"@codemirror/search": "6.x.x",
"@codemirror/state": "6.x.x",
"@codemirror/view": "6.x.x"
}
},
"node_modules/crelt": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
}
},
"node_modules/style-mod": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"license": "MIT"
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
}
}
}

28
src/editor/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"private": true,
"type": "module",
"devDependencies": {
"esbuild": "^0.25.0"
},
"dependencies": {
"@codemirror/autocomplete": "^6",
"@codemirror/commands": "^6",
"@codemirror/lang-css": "^6",
"@codemirror/lang-go": "^6",
"@codemirror/lang-html": "^6",
"@codemirror/lang-javascript": "^6",
"@codemirror/lang-json": "^6",
"@codemirror/lang-markdown": "^6",
"@codemirror/lang-python": "^6",
"@codemirror/lang-rust": "^6",
"@codemirror/lang-sql": "^6",
"@codemirror/lang-yaml": "^6",
"@codemirror/language": "^6",
"@codemirror/search": "^6",
"@codemirror/state": "^6",
"@codemirror/theme-one-dark": "^6",
"@codemirror/view": "^6",
"@replit/codemirror-vim": "^6",
"@replit/codemirror-emacs": "^6"
}
}

186
src/editor/theme.mjs Normal file
View File

@@ -0,0 +1,186 @@
// ==========================================
// Chat Switchboard — CM6 Theme
// ==========================================
// Maps the app's CSS custom properties to CM6
// theme slots. Works in both light and dark mode.
//
// The theme is applied via EditorView.theme()
// which generates scoped CSS classes. Dark mode
// detection uses body.dark-theme (existing pattern).
// ==========================================
import { EditorView } from '@codemirror/view';
/**
* Switchboard base theme — uses CSS variables so it
* adapts automatically when the theme toggle fires.
*/
export const switchboardTheme = EditorView.theme({
'&': {
backgroundColor: 'var(--bg-surface, var(--bg))',
color: 'var(--text)',
fontSize: '14px',
},
'.cm-content': {
fontFamily: 'var(--mono, monospace)',
caretColor: 'var(--accent)',
padding: '4px 0',
},
'&.cm-focused': {
outline: 'none',
},
'.cm-cursor, .cm-dropCursor': {
borderLeftColor: 'var(--accent)',
borderLeftWidth: '2px',
},
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground': {
backgroundColor: 'var(--accent-muted, rgba(99, 102, 241, 0.2))',
},
'.cm-activeLine': {
backgroundColor: 'var(--bg-hover, rgba(255, 255, 255, 0.03))',
},
'.cm-gutters': {
backgroundColor: 'var(--bg-2, var(--bg))',
color: 'var(--text-3, #666)',
borderRight: '1px solid var(--border)',
minWidth: '40px',
},
'.cm-activeLineGutter': {
backgroundColor: 'var(--bg-hover, rgba(255, 255, 255, 0.05))',
color: 'var(--text-2)',
},
'.cm-foldPlaceholder': {
backgroundColor: 'var(--bg-2)',
color: 'var(--text-3)',
border: '1px solid var(--border)',
borderRadius: '3px',
padding: '0 4px',
},
// Search panel
'.cm-panels': {
backgroundColor: 'var(--bg-2)',
color: 'var(--text)',
borderBottom: '1px solid var(--border)',
},
'.cm-panels.cm-panels-top': {
borderBottom: '1px solid var(--border)',
},
'.cm-panels.cm-panels-bottom': {
borderTop: '1px solid var(--border)',
},
'.cm-search input, .cm-search button': {
backgroundColor: 'var(--bg-surface)',
color: 'var(--text)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius, 4px)',
fontSize: '13px',
},
'.cm-search input:focus': {
borderColor: 'var(--accent)',
outline: 'none',
},
// Autocomplete
'.cm-tooltip': {
backgroundColor: 'var(--bg-2)',
color: 'var(--text)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius, 4px)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
},
'.cm-tooltip-autocomplete > ul > li': {
padding: '4px 8px',
},
'.cm-tooltip-autocomplete > ul > li[aria-selected]': {
backgroundColor: 'var(--accent)',
color: '#fff',
},
// Matching brackets
'.cm-matchingBracket': {
backgroundColor: 'var(--accent-muted, rgba(99, 102, 241, 0.3))',
outline: '1px solid var(--accent)',
},
'.cm-nonmatchingBracket': {
color: 'var(--danger, #e74c3c)',
},
// Scrollbar
'.cm-scroller': {
fontFamily: 'var(--mono, monospace)',
overflow: 'auto',
},
});
/**
* Minimal theme for the chat input — removes gutters,
* line numbers, and active line highlighting.
*/
export const chatInputTheme = EditorView.theme({
'&': {
backgroundColor: 'transparent',
color: 'var(--text)',
fontSize: 'var(--msg-font, 14px)',
},
'.cm-content': {
fontFamily: 'var(--font, system-ui, sans-serif)',
caretColor: 'var(--text)',
padding: '0',
minHeight: '20px',
},
'&.cm-focused': {
outline: 'none',
},
// Cursor: high-contrast, always visible in both themes
'.cm-cursor, .cm-dropCursor': {
borderLeftColor: 'var(--text)',
borderLeftWidth: '2px',
},
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground': {
backgroundColor: 'var(--accent-dim, rgba(99, 102, 241, 0.2))',
},
'.cm-line': {
padding: '0',
},
'.cm-scroller': {
fontFamily: 'var(--font, system-ui, sans-serif)',
overflow: 'auto',
},
// Hide scrollbar but keep scrollable for auto-grow
'.cm-scroller::-webkit-scrollbar': {
display: 'none',
},
// Placeholder — vertically aligned with first line of input
'.cm-placeholder': {
color: 'var(--text-3, #888)',
fontStyle: 'normal',
lineHeight: 'inherit',
pointerEvents: 'none',
},
// Tooltip/autocomplete inherits from base
'.cm-tooltip': {
backgroundColor: 'var(--bg-2)',
color: 'var(--text)',
border: '1px solid var(--border)',
borderRadius: 'var(--radius, 4px)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
},
// ── Fenced code block decorations (WYSIWYG) ──
// Lines inside fenced code blocks get a visual container
'.cm-codeblock': {
backgroundColor: 'var(--bg-surface, #18181b)',
fontFamily: 'var(--mono, monospace)',
fontSize: '13px',
padding: '0 10px',
borderLeft: '2px solid var(--accent, #6c9fff)',
},
'.cm-codeblock.cm-codeblock-open': {
borderTopLeftRadius: 'var(--radius, 6px)',
borderTopRightRadius: 'var(--radius, 6px)',
paddingTop: '6px',
color: 'var(--text-3, #6b6b7b)',
},
'.cm-codeblock.cm-codeblock-close': {
borderBottomLeftRadius: 'var(--radius, 6px)',
borderBottomRightRadius: 'var(--radius, 6px)',
paddingBottom: '6px',
color: 'var(--text-3, #6b6b7b)',
},
});

View File

@@ -172,7 +172,9 @@
<div class="popup-menu kb-popup" id="kbPopup"></div>
</div>
</div>
<textarea id="messageInput" placeholder="Send a message..." rows="1"></textarea>
<div id="messageInputWrap">
<textarea id="messageInput" placeholder="Send a message..." rows="1"></textarea>
</div>
<div class="input-actions">
<button class="action-btn stop-btn" id="stopBtn" title="Stop">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
@@ -409,6 +411,26 @@
</div>
<!-- Appearance Tab -->
<div class="settings-tab-content" id="settingsAppearanceTab" style="display:none">
<section class="settings-section">
<h3>Theme</h3>
<div class="form-group">
<label>Mode</label>
<div class="theme-toggle" id="themeToggle">
<button class="theme-btn" data-theme="light" title="Light">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
Light
</button>
<button class="theme-btn" data-theme="dark" title="Dark">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
Dark
</button>
<button class="theme-btn" data-theme="system" title="System">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
System
</button>
</div>
</div>
</section>
<section class="settings-section">
<h3>Display</h3>
<div class="form-group">
@@ -422,6 +444,18 @@
<div class="range-labels"><span>12px</span><span>20px</span></div>
</div>
</section>
<section class="settings-section">
<h3>Editor</h3>
<div class="form-group">
<label>Keybindings</label>
<div class="theme-toggle" id="keymapToggle">
<button class="theme-btn" data-keymap="standard" title="Standard keybindings">Standard</button>
<button class="theme-btn" data-keymap="vim" title="Vim keybindings">Vim</button>
<button class="theme-btn" data-keymap="emacs" title="Emacs keybindings">Emacs</button>
</div>
<span class="form-hint">Applies to code editors only — chat input always uses standard.</span>
</div>
</section>
</div>
<!-- Providers Tab -->
<div class="settings-tab-content" id="settingsProvidersTab" style="display:none">
@@ -984,6 +1018,7 @@
<!-- Vendor libs -->
<script src="vendor/marked.min.js" onerror="this.onerror=null;this.src='https://cdnjs.cloudflare.com/ajax/libs/marked/16.3.0/lib/marked.umd.min.js'"></script>
<script src="vendor/purify.min.js" onerror="this.onerror=null;this.src='https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.4/purify.min.js'"></script>
<script src="vendor/codemirror/codemirror.bundle.js?v=%%APP_VERSION%%" onerror="console.warn('[CM6] Bundle not available — falling back to textarea')"></script>
<script src="js/debug.js?v=%%APP_VERSION%%"></script>
<script src="js/events.js?v=%%APP_VERSION%%"></script>

View File

@@ -693,9 +693,40 @@ async function deleteAdminExtension(id, name) {
} catch (e) { UI.toast(e.message, 'error'); }
}
// Track CM6 editor instances per extension edit form
var _extEditors = {};
// Listen for theme changes to update CM6 editors
if (typeof Events !== 'undefined') {
Events.on('theme.changed', (data) => {
const isDark = data.resolved === 'dark';
for (const editors of Object.values(_extEditors)) {
editors.manifest?.setDarkMode(isDark);
editors.script?.setDarkMode(isDark);
}
});
// Listen for keymap changes to update CM6 editors in real time
Events.on('keymap.changed', (data) => {
const mode = data.mode || 'standard';
for (const editors of Object.values(_extEditors)) {
editors.manifest?.setKeymap(mode);
editors.script?.setKeymap(mode);
}
});
}
function editAdminExtension(id) {
// Close any existing edit form
document.querySelectorAll('.ext-edit-form').forEach(el => el.remove());
// Close any existing edit form (and destroy CM6 instances)
document.querySelectorAll('.ext-edit-form').forEach(el => {
const eid = el.dataset.extId;
if (_extEditors[eid]) {
_extEditors[eid].manifest?.destroy();
_extEditors[eid].script?.destroy();
delete _extEditors[eid];
}
el.remove();
});
const ext = (UI._adminExtensions || []).find(e => e.id === id);
if (!ext) return UI.toast('Extension not found', 'error');
@@ -716,6 +747,15 @@ function editAdminExtension(id) {
</div>`
: '';
// Use container divs instead of textareas when CM6 is available
const useCM6 = !!window.CM?.codeEditor;
const manifestEditor = useCM6
? `<div id="extEdit-manifest-${id}" style="width:100%;min-height:180px;border:1px solid var(--border);border-radius:var(--radius,4px);overflow:hidden"></div>`
: `<textarea id="extEdit-manifest-${id}" rows="8" style="width:100%;font-family:var(--mono);font-size:12px;tab-size:2">${esc(manifestJSON)}</textarea>`;
const scriptEditor = useCM6
? `<div id="extEdit-script-${id}" style="width:100%;min-height:320px;border:1px solid var(--border);border-radius:var(--radius,4px);overflow:hidden"></div>`
: `<textarea id="extEdit-script-${id}" rows="16" style="width:100%;font-family:var(--mono);font-size:12px;tab-size:2;white-space:pre">${esc(script)}</textarea>`;
const formHTML = `
<div class="ext-edit-form" data-ext-id="${id}" style="border-top:1px solid var(--border);padding:12px 0;margin-top:8px">
${systemWarning}
@@ -729,14 +769,14 @@ function editAdminExtension(id) {
</div>
<div class="form-group" style="margin-bottom:8px">
<label style="font-size:12px;font-weight:600">Manifest JSON <span class="text-muted">(without _script)</span></label>
<textarea id="extEdit-manifest-${id}" rows="8" style="width:100%;font-family:var(--mono);font-size:12px;tab-size:2">${esc(manifestJSON)}</textarea>
${manifestEditor}
</div>
<div class="form-group" style="margin-bottom:8px">
<label style="font-size:12px;font-weight:600">Script <span class="text-muted">(${script.length.toLocaleString()} chars)</span></label>
<textarea id="extEdit-script-${id}" rows="16" style="width:100%;font-family:var(--mono);font-size:12px;tab-size:2;white-space:pre">${esc(script)}</textarea>
${scriptEditor}
</div>
<div style="display:flex;gap:8px;justify-content:flex-end">
<button class="btn-small" onclick="this.closest('.ext-edit-form').remove()">Cancel</button>
<button class="btn-small" onclick="_closeExtEditForm('${id}')">Cancel</button>
<button class="btn-small btn-primary" onclick="saveAdminExtension('${id}')">Save Changes</button>
</div>
</div>
@@ -748,42 +788,80 @@ function editAdminExtension(id) {
if (row.querySelector(`[onclick*="editAdminExtension('${id}')"]`)) {
row.insertAdjacentHTML('afterend', formHTML);
// Tab key inserts tab in script textarea
const scriptEl = document.getElementById(`extEdit-script-${id}`);
if (scriptEl) {
scriptEl.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
const start = scriptEl.selectionStart;
const end = scriptEl.selectionEnd;
scriptEl.value = scriptEl.value.substring(0, start) + ' ' + scriptEl.value.substring(end);
scriptEl.selectionStart = scriptEl.selectionEnd = start + 4;
}
});
if (useCM6) {
// Get user keybinding preference
const prefs = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
const keymapMode = prefs.editorKeymap || 'standard';
// Initialize CM6 editors
_extEditors[id] = {
manifest: CM.codeEditor(document.getElementById(`extEdit-manifest-${id}`), {
language: 'json',
value: manifestJSON,
lineNumbers: true,
keymap: keymapMode,
}),
script: CM.codeEditor(document.getElementById(`extEdit-script-${id}`), {
language: 'javascript',
value: script,
lineNumbers: true,
keymap: keymapMode,
}),
};
} else {
// Fallback: Tab key inserts tab in script textarea
const scriptEl = document.getElementById(`extEdit-script-${id}`);
if (scriptEl) {
scriptEl.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
e.preventDefault();
const start = scriptEl.selectionStart;
const end = scriptEl.selectionEnd;
scriptEl.value = scriptEl.value.substring(0, start) + ' ' + scriptEl.value.substring(end);
scriptEl.selectionStart = scriptEl.selectionEnd = start + 4;
}
});
}
}
break;
}
}
}
function _closeExtEditForm(id) {
if (_extEditors[id]) {
_extEditors[id].manifest?.destroy();
_extEditors[id].script?.destroy();
delete _extEditors[id];
}
document.querySelector(`.ext-edit-form[data-ext-id="${id}"]`)?.remove();
}
async function saveAdminExtension(id) {
const nameEl = document.getElementById(`extEdit-name-${id}`);
const descEl = document.getElementById(`extEdit-desc-${id}`);
const manifestEl = document.getElementById(`extEdit-manifest-${id}`);
const scriptEl = document.getElementById(`extEdit-script-${id}`);
if (!nameEl || !manifestEl || !scriptEl) return;
// Read from CM6 if available, otherwise from textarea
const editors = _extEditors[id];
const manifestText = editors?.manifest
? editors.manifest.getValue()
: document.getElementById(`extEdit-manifest-${id}`)?.value;
const scriptText = editors?.script
? editors.script.getValue()
: document.getElementById(`extEdit-script-${id}`)?.value;
if (!nameEl || manifestText == null || scriptText == null) return;
// Parse manifest and merge _script back in
let manifest;
try {
manifest = JSON.parse(manifestEl.value.trim() || '{}');
manifest = JSON.parse((manifestText || '').trim() || '{}');
} catch (e) {
return UI.toast('Invalid manifest JSON: ' + e.message, 'error');
}
const script = scriptEl.value;
if (script.trim()) {
manifest._script = script;
if (scriptText.trim()) {
manifest._script = scriptText;
}
try {
@@ -793,6 +871,7 @@ async function saveAdminExtension(id) {
manifest: manifest,
});
UI.toast('Extension updated — reload page to apply changes');
_closeExtEditForm(id);
await UI.loadAdminExtensions();
} catch (e) {
UI.toast(e.message, 'error');

View File

@@ -721,7 +721,6 @@ function initAttachments() {
function _initAttachmentListeners() {
const attachBtn = document.getElementById('attachBtn');
const fileInput = document.getElementById('fileInput');
const messageInput = document.getElementById('messageInput');
const chatArea = document.getElementById('chatMessages');
const lightbox = document.getElementById('lightbox');
@@ -741,8 +740,12 @@ function _initAttachmentListeners() {
}
// ── Smart Paste ─────────────────────────
if (messageInput) {
messageInput.addEventListener('paste', (e) => {
// Attach to the active input element (CM6 contentDOM or textarea)
const pasteTarget = (typeof ChatInput !== 'undefined' && ChatInput.getDom())
? ChatInput.getDom()
: document.getElementById('messageInput');
if (pasteTarget) {
pasteTarget.addEventListener('paste', (e) => {
if (!App.storageConfigured) return; // fall through to normal paste
const items = [...(e.clipboardData?.items || [])];

View File

@@ -4,6 +4,75 @@
// Chat management, send, stream, regenerate, edit, branch,
// summarize, per-chat model persistence.
// ── Chat Input Abstraction ──────────────────
// Wraps CM6 editor (when available) or fallback textarea.
// All code that reads/writes the message input goes through this.
const ChatInput = {
_editor: null, // CM6 instance, set during init
_textarea: null, // fallback textarea element
getValue() {
if (this._editor) return this._editor.getValue();
return this._textarea?.value || '';
},
setValue(text) {
if (this._editor) {
this._editor.setValue(text || '');
} else if (this._textarea) {
this._textarea.value = text || '';
this._textarea.style.height = 'auto';
}
},
focus() {
if (this._editor) this._editor.focus();
else this._textarea?.focus();
},
/** Get the DOM element (for paste listeners, etc.) */
getDom() {
if (this._editor) return this._editor.getView().contentDOM;
return this._textarea;
},
/** Get the wrapper DOM element (for resize, etc.) */
getWrapDom() {
if (this._editor) return this._editor.getView().dom;
return this._textarea;
},
/** Initialize — try CM6, fall back to textarea */
init() {
this._textarea = document.getElementById('messageInput');
const wrap = document.getElementById('messageInputWrap');
if (window.CM?.chatInput && wrap) {
// Hide the textarea, create CM6 editor in its place
this._textarea.style.display = 'none';
this._editor = CM.chatInput(wrap, {
placeholder: 'Send a message...',
onSubmit: () => sendMessage(),
onChange: () => updateInputTokens(),
maxHeight: 200,
});
DebugLog?.push?.('chat', '[CM6] Chat input initialized');
} else {
// Fallback: textarea with manual event handling
DebugLog?.push?.('chat', '[CM6] Not available — using textarea fallback');
this._textarea.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
});
this._textarea.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 200) + 'px';
updateInputTokens();
});
}
},
};
// ── Summarize & Continue ──────────────────
async function summarizeAndContinue() {
@@ -266,7 +335,7 @@ async function newChat() {
Tokens._warningDismissed = false;
updateContextWarning(); updateChatTokenCount();
updateInputTokens();
document.getElementById('messageInput').focus();
ChatInput.focus();
if (window.innerWidth <= 768) {
document.getElementById('sidebar').classList.add('collapsed');
const ov = document.getElementById('sidebarOverlay');
@@ -377,15 +446,13 @@ function startRenameChat(chatId) {
// ── Send Message ─────────────────────────────
async function sendMessage() {
const input = document.getElementById('messageInput');
const text = input.value.trim();
const text = ChatInput.getValue().trim();
const hasAttachments = hasStagedAttachments();
// Need text or attachments, not generating, not blocked by uploads
if ((!text && !hasAttachments) || App.isGenerating || isSendBlocked()) return;
input.value = '';
input.style.height = 'auto';
ChatInput.setValue('');
// Snapshot attachment IDs before clearing staged state
const attachmentIds = getStagedAttachmentIds();
@@ -749,16 +816,8 @@ function _initChatListeners() {
UI.toast(`Loaded ${visible} model${visible !== 1 ? 's' : ''}`, 'success');
});
// Input
const input = document.getElementById('messageInput');
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); }
});
input.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 200) + 'px';
updateInputTokens();
});
// Input — CM6 or textarea fallback
ChatInput.init();
// Close modals on overlay click
document.querySelectorAll('.modal-overlay').forEach(overlay => {

View File

@@ -272,6 +272,11 @@ const DebugLog = {
snap.extensions = Extensions.debug();
}
// CM6 editor state
snap.cm6 = window.CM
? { version: window.CM.version, languages: Object.keys(window.CM.languages || {}) }
: { available: false };
return snap;
},

View File

@@ -69,8 +69,7 @@ function updateInputTokens() {
const el = document.getElementById('inputTokenCount');
if (!el) return;
const input = document.getElementById('messageInput');
const inputText = input?.value || '';
const inputText = (typeof ChatInput !== 'undefined') ? ChatInput.getValue() : '';
const inputTokens = Tokens.estimate(inputText);
if (!inputText.trim()) {

View File

@@ -11,17 +11,30 @@ Object.assign(UI, {
const prefs = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
const scale = prefs.scale || 100;
const msgFont = prefs.msgFont || 14;
const theme = prefs.theme || 'dark';
const keymap = prefs.editorKeymap || 'standard';
const scaleEl = document.getElementById('settingsScale');
const msgFontEl = document.getElementById('settingsMsgFont');
if (scaleEl) { scaleEl.value = scale; document.getElementById('scaleValue').textContent = scale + '%'; }
if (msgFontEl) { msgFontEl.value = msgFont; document.getElementById('msgFontValue').textContent = msgFont + 'px'; }
// Highlight active theme button
document.querySelectorAll('#themeToggle .theme-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.theme === theme);
});
// Highlight active keymap button
document.querySelectorAll('#keymapToggle .theme-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.keymap === keymap);
});
},
initAppearance() {
// Load saved prefs on startup
const prefs = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
UI.applyAppearance(prefs.scale || 100, prefs.msgFont || 14);
UI.applyTheme(prefs.theme || 'dark');
// Live preview on slider change
const scaleEl = document.getElementById('settingsScale');
@@ -36,6 +49,70 @@ Object.assign(UI, {
document.getElementById('msgFontValue').textContent = v + 'px';
UI.applyAppearance(parseInt(scaleEl?.value || 100), v);
});
// Theme toggle buttons
document.querySelectorAll('#themeToggle .theme-btn').forEach(btn => {
btn.addEventListener('click', () => {
const theme = btn.dataset.theme;
UI.applyTheme(theme);
document.querySelectorAll('#themeToggle .theme-btn').forEach(b =>
b.classList.toggle('active', b === btn)
);
const p = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
p.theme = theme;
localStorage.setItem('cs-appearance', JSON.stringify(p));
});
});
// Editor keymap toggle buttons
document.querySelectorAll('#keymapToggle .theme-btn').forEach(btn => {
btn.addEventListener('click', () => {
const mode = btn.dataset.keymap;
document.querySelectorAll('#keymapToggle .theme-btn').forEach(b =>
b.classList.toggle('active', b === btn)
);
// Persist
const p = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
p.editorKeymap = mode;
localStorage.setItem('cs-appearance', JSON.stringify(p));
// Notify open editors
if (typeof Events !== 'undefined' && Events.emit) {
Events.emit('keymap.changed', { mode });
}
});
});
// System theme change listener (for "system" mode)
UI._systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
UI._systemThemeQuery.addEventListener('change', () => {
const p = JSON.parse(localStorage.getItem('cs-appearance') || '{}');
if (p.theme === 'system') UI.applyTheme('system');
});
},
/**
* Apply theme to the document.
* @param {'light'|'dark'|'system'} mode
*/
applyTheme(mode) {
let resolved = mode;
if (mode === 'system') {
resolved = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
if (resolved === 'light') {
document.documentElement.setAttribute('data-theme', 'light');
} else {
document.documentElement.removeAttribute('data-theme');
}
// Publish event for CM6 editors and extensions
if (typeof Events !== 'undefined' && Events.emit) {
Events.emit('theme.changed', { mode, resolved });
}
},
/** Get the currently resolved theme ('dark' or 'light') */
getResolvedTheme() {
return document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
},
applyAppearance(scale, msgFont) {

View File

@@ -68,11 +68,12 @@ self.addEventListener('activate', (event) => {
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Never cache API calls, WebSocket upgrades, branding, or extension assets
// Never cache API calls, WebSocket upgrades, branding, extensions, or CM6 bundle
if (url.pathname.includes('/api/') ||
url.pathname.includes('/ws') ||
url.pathname.includes('/branding/') ||
url.pathname.includes('/extensions/') ||
url.pathname.includes('/vendor/codemirror/') ||
event.request.method !== 'GET') {
return;
}