diff --git a/pwa/bun.lock b/pwa/bun.lock index 69b29fc..9253d22 100644 --- a/pwa/bun.lock +++ b/pwa/bun.lock @@ -4,6 +4,9 @@ "workspaces": { "": { "name": "pwa", + "dependencies": { + "highlight.js": "^11.11.1", + }, "devDependencies": { "@types/bun": "latest", "typescript": "^5", @@ -68,6 +71,8 @@ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], diff --git a/pwa/package.json b/pwa/package.json index 4114efe..309aa0b 100644 --- a/pwa/package.json +++ b/pwa/package.json @@ -11,5 +11,8 @@ "@types/bun": "latest", "typescript": "^5", "vite": "^8.0.16" + }, + "dependencies": { + "highlight.js": "^11.11.1" } } diff --git a/pwa/src/chat.ts b/pwa/src/chat.ts index 34f2c99..5df5eb1 100644 --- a/pwa/src/chat.ts +++ b/pwa/src/chat.ts @@ -1,38 +1,76 @@ import { state, escapeHTML, renderPart } from './state'; import { handleDisconnect } from './connect'; +declare const lucide: { createIcons: () => void } | undefined; +function initIcons() { try { lucide?.createIcons(); } catch {} } + const app = () => document.querySelector('#app')!; export function renderChat(): void { const session = state.sessions.find((s) => s.id === state.activeSessionID); - const title = session?.title || 'Chat'; app().innerHTML = ` -
-
${escapeHTML(title)}
-
- - -
+
+
+
+
+
+
+
-
-
-
`; document.getElementById('new-session-btn')!.onclick = handleNewSession; document.getElementById('disconnect-btn')!.onclick = () => handleDisconnect(); + // Populate model info from first message if available + if (state.messages.length > 0) { + const m = state.messages.find(msg => msg.model); + if (m) { + const el = document.getElementById('sidebar-model'); + if (el) el.textContent = m.model; + } + } + renderMessages(); renderPermission(); renderInputBar(); scrollToBottom(); + initIcons(); } -declare const lucide: { createIcons: () => void } | undefined; - -function initIcons() { try { lucide?.createIcons(); } catch {} } - export function renderMessages(): void { const container = document.getElementById('messages'); if (!container) return; @@ -75,7 +113,7 @@ export function renderInputBar(): void { if (state.agentBusy) { bar.innerHTML = ` - + `; document.getElementById('stop-btn')!.onclick = handleCancel; initIcons(); diff --git a/pwa/src/state.ts b/pwa/src/state.ts index 357517e..3ccbcc8 100644 --- a/pwa/src/state.ts +++ b/pwa/src/state.ts @@ -34,6 +34,57 @@ export const state: AppState = { agentBusy: false, }; +import hljs from 'highlight.js/lib/core'; +// Register common languages +import typescript from 'highlight.js/lib/languages/typescript'; +import javascript from 'highlight.js/lib/languages/javascript'; +import python from 'highlight.js/lib/languages/python'; +import go from 'highlight.js/lib/languages/go'; +import rust from 'highlight.js/lib/languages/rust'; +import css from 'highlight.js/lib/languages/css'; +import json from 'highlight.js/lib/languages/json'; +import bash from 'highlight.js/lib/languages/bash'; +import xml from 'highlight.js/lib/languages/xml'; +import yaml from 'highlight.js/lib/languages/yaml'; +import sql from 'highlight.js/lib/languages/sql'; +import markdown from 'highlight.js/lib/languages/markdown'; +import swift from 'highlight.js/lib/languages/swift'; + +hljs.registerLanguage('typescript', typescript); +hljs.registerLanguage('javascript', javascript); +hljs.registerLanguage('python', python); +hljs.registerLanguage('go', go); +hljs.registerLanguage('rust', rust); +hljs.registerLanguage('css', css); +hljs.registerLanguage('json', json); +hljs.registerLanguage('bash', bash); +hljs.registerLanguage('xml', xml); +hljs.registerLanguage('html', xml); +hljs.registerLanguage('yaml', yaml); +hljs.registerLanguage('sql', sql); +hljs.registerLanguage('markdown', markdown); +hljs.registerLanguage('swift', swift); + +function highlightCode(code: string, lang?: string): string { + if (lang && hljs.getLanguage(lang)) { + try { return hljs.highlight(code, { language: lang }).value; } catch {} + } + try { return hljs.highlightAuto(code).value; } catch {} + return escapeHTML(code); +} + +function guessLang(filePath: string): string | undefined { + const ext = filePath.split('.').pop()?.toLowerCase(); + const map: Record = { + ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript', + py: 'python', go: 'go', rs: 'rust', css: 'css', scss: 'css', + json: 'json', sh: 'bash', bash: 'bash', zsh: 'bash', + html: 'html', xml: 'xml', svg: 'xml', yml: 'yaml', yaml: 'yaml', + sql: 'sql', md: 'markdown', swift: 'swift', rb: 'ruby', + }; + return ext ? map[ext] : undefined; +} + export function escapeHTML(str: string): string { const d = document.createElement('div'); d.textContent = str; @@ -49,6 +100,300 @@ export function renderMarkdown(text: string): string { return html; } +const TOOL_LABELS: Record = { + bash: 'Bash', edit: 'Edit', multiedit: 'Multi-Edit', write: 'Write', + view: 'View', glob: 'Glob', grep: 'Grep', ls: 'List', + fetch: 'Fetch', agentic_fetch: 'Agentic Fetch', web_fetch: 'Web Fetch', + web_search: 'Search', download: 'Download', sourcegraph: 'Sourcegraph', + todos: 'To-Do', question: 'Question', agent: 'Agent', + lsp_diagnostics: 'Diagnostics', lsp_definition: 'Definition', + lsp_references: 'References', lsp_symbols: 'Symbols', + lsp_rename: 'Rename', lsp_replace_symbol: 'Replace Symbol', + lsp_call_hierarchy: 'Call Hierarchy', lsp_restart: 'LSP Restart', + job_output: 'Job Output', job_kill: 'Job Kill', + crush_logs: 'Crush Logs', crush_info: 'Crush Info', + read_mcp_resource: 'MCP Read', list_mcp_resources: 'MCP List', +}; + +const TOOL_ICONS: Record = { + bash: 'terminal', edit: 'file-pen', multiedit: 'files', write: 'file-plus', + view: 'eye', glob: 'search', grep: 'regex', ls: 'folder-tree', + fetch: 'globe', agentic_fetch: 'bot', web_fetch: 'globe', web_search: 'search', + download: 'download', todos: 'list-checks', question: 'circle-help', + agent: 'bot', sourcegraph: 'code', + lsp_diagnostics: 'stethoscope', lsp_definition: 'arrow-right-from-line', + lsp_references: 'list-filter', lsp_symbols: 'braces', + lsp_rename: 'text-cursor-input', lsp_replace_symbol: 'replace', + lsp_call_hierarchy: 'git-branch', lsp_restart: 'refresh-cw', + job_output: 'scroll-text', job_kill: 'x', + crush_logs: 'file-text', crush_info: 'info', + read_mcp_resource: 'book-open', list_mcp_resources: 'list', +}; + +function toolLabel(name: string): string { + if (TOOL_LABELS[name]) return TOOL_LABELS[name]; + if (name.startsWith('mcp_')) return name.replace(/^mcp_/, '').replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); + return name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); +} + +function toolIcon(name: string): string { + if (TOOL_ICONS[name]) return TOOL_ICONS[name]; + if (name.startsWith('mcp_')) return 'puzzle'; + return 'wrench'; +} + +function parseInput(input: string): Record | null { + try { return JSON.parse(input); } catch { return null; } +} + +function renderToolCallHeader(name: string, params: Record | null, finished: boolean | undefined): string { + const label = toolLabel(name); + const icon = toolIcon(name); + const spinner = finished ? '' : ' '; + let detail = ''; + + switch (name) { + case 'bash': { + const cmd = ((params?.command as string) || '').replace(/\n/g, ' ').replace(/\t/g, ' '); + detail = cmd ? ` ${escapeHTML(cmd.length > 80 ? cmd.slice(0, 80) + '…' : cmd)}` : ''; + break; + } + case 'edit': + case 'write': + case 'view': { + const fp = params?.file_path as string; + if (fp) detail = ` ${escapeHTML(fp)}`; + break; + } + case 'multiedit': { + const fp = params?.file_path as string; + const count = Array.isArray(params?.edits) ? (params.edits as unknown[]).length : 0; + if (fp) detail = ` ${escapeHTML(fp)}${count ? ` (${count} edits)` : ''}`; + break; + } + case 'grep': { + const pat = params?.pattern as string; + const path = params?.path as string; + if (pat) detail = ` "${escapeHTML(pat)}"${path ? ' in ' + escapeHTML(path) : ''}`; + break; + } + case 'glob': { + const pat = params?.pattern as string; + if (pat) detail = ` ${escapeHTML(pat)}`; + break; + } + case 'ls': { + const path = (params?.path as string) || '.'; + detail = ` ${escapeHTML(path)}`; + break; + } + case 'fetch': + case 'web_fetch': + case 'agentic_fetch': { + const url = params?.url as string; + if (url) detail = ` ${escapeHTML(url.length > 60 ? url.slice(0, 60) + '…' : url)}`; + break; + } + case 'web_search': { + const q = params?.query as string; + if (q) detail = ` "${escapeHTML(q)}"`; + break; + } + case 'todos': { + const todos = params?.todos as Array<{ status?: string }>; + if (Array.isArray(todos)) { + const done = todos.filter(t => t.status === 'completed').length; + detail = ` ${done}/${todos.length}`; + } + break; + } + default: { + // For LSP/MCP/generic tools, show first meaningful param + if (params) { + const keys = Object.keys(params).filter(k => !['description'].includes(k)); + const first = keys[0]; + if (first && typeof params[first] === 'string') { + const v = params[first] as string; + detail = ` ${escapeHTML(v.length > 60 ? v.slice(0, 60) + '…' : v)}`; + } + } + } + } + + return `
${escapeHTML(label)}${detail}${spinner}
`; +} + +function renderToolResultBody(name: string, content: string, metadata: string | undefined): string { + if (!content && !metadata) return ''; + + let meta: Record | null = null; + if (metadata) { + try { meta = JSON.parse(metadata); } catch {} + } + + switch (name) { + case 'bash': { + const output = (meta?.output as string) || content; + if (!output || output === 'no output') return '
no output
'; + return `
${highlightCode(output.slice(0, 3000), 'bash')}
`; + } + case 'edit': + case 'multiedit': { + const oldContent = meta?.old_content as string; + const newContent = meta?.new_content as string; + const filePath = (meta?.file_path as string) || ''; + const lang = filePath ? guessLang(filePath) : undefined; + if (oldContent && newContent) { + return `
${renderColoredDiff(oldContent, newContent, lang)}
`; + } + if (content) return `
${highlightCode(content.slice(0, 1500), lang)}
`; + return ''; + } + case 'write': { + // Try to get file path from the matching tool_call to guess language + if (content && content !== 'File successfully written.') { + return `
${highlightCode(content.slice(0, 2000))}
`; + } + return ''; + } + case 'view': { + const viewContent = (meta?.content as string) || content; + if (viewContent) { + return `
${highlightCode(viewContent.slice(0, 3000))}
`; + } + return ''; + } + case 'todos': { + if (meta?.todos) { + const todos = meta.todos as Array<{ content: string; status: string; active_form?: string }>; + const lines = todos.map(t => { + const icon = t.status === 'completed' ? '✓' : t.status === 'in_progress' ? '→' : '○'; + const cls = t.status === 'completed' ? 'done' : t.status === 'in_progress' ? 'active' : ''; + const text = t.status === 'in_progress' && t.active_form ? t.active_form : t.content; + return `
${icon} ${escapeHTML(text)}
`; + }).join(''); + return `
${lines}
`; + } + if (content) return `
${escapeHTML(content.slice(0, 1000))}
`; + return ''; + } + default: { + if (!content) return ''; + const maxLen = 2000; + const truncated = content.length > maxLen ? content.slice(0, maxLen) + '\n…' : content; + return `
${highlightCode(truncated)}
`; + } + } +} + +function renderColoredDiff(oldStr: string, newStr: string, lang?: string): string { + const oldLines = oldStr.split('\n'); + const newLines = newStr.split('\n'); + + const hlOld = oldLines.map(l => highlightCode(l, lang)); + const hlNew = newLines.map(l => highlightCode(l, lang)); + + const lcs = computeLCS(oldLines, newLines); + const raw: Array<{ type: 'ctx' | 'add' | 'del'; html: string }> = []; + let oi = 0, ni = 0, li = 0; + + while (oi < oldLines.length || ni < newLines.length) { + if (li < lcs.length && oi < oldLines.length && oldLines[oi] === lcs[li] && ni < newLines.length && newLines[ni] === lcs[li]) { + raw.push({ type: 'ctx', html: ` ${hlOld[oi]}` }); + oi++; ni++; li++; + } else if (li < lcs.length && ni < newLines.length && newLines[ni] === lcs[li]) { + raw.push({ type: 'del', html: `- ${hlOld[oi]}` }); + oi++; + } else if (li < lcs.length && oi < oldLines.length && oldLines[oi] === lcs[li]) { + raw.push({ type: 'add', html: `+ ${hlNew[ni]}` }); + ni++; + } else { + if (oi < oldLines.length && (ni >= newLines.length || li >= lcs.length)) { + raw.push({ type: 'del', html: `- ${hlOld[oi]}` }); + oi++; + } else if (ni < newLines.length) { + raw.push({ type: 'add', html: `+ ${hlNew[ni]}` }); + ni++; + } + } + } + + // Collapse consecutive context lines, keeping 3 lines of context around changes + const CONTEXT = 3; + const result: string[] = []; + let i = 0; + while (i < raw.length) { + if (raw[i].type === 'ctx') { + // Find the extent of this context run + let end = i; + while (end < raw.length && raw[end].type === 'ctx') end++; + const ctxLen = end - i; + + // Is there a change after this context block? + const hasChangeAfter = end < raw.length; + // Was there a change before this context block? + const hasChangeBefore = i > 0; + + if (!hasChangeAfter && !hasChangeBefore) { + // Entire diff is context — show all + for (let j = i; j < end; j++) result.push(raw[j].html); + } else if (ctxLen <= CONTEXT * 2 + 1) { + // Small enough to show entirely + for (let j = i; j < end; j++) result.push(raw[j].html); + } else { + // Show head + fold + tail + const headEnd = hasChangeBefore ? i + CONTEXT : i; + const tailStart = hasChangeAfter ? end - CONTEXT : end; + + if (hasChangeBefore) { + for (let j = i; j < headEnd; j++) result.push(raw[j].html); + } + const hidden = tailStart - headEnd; + if (hidden > 0) { + result.push(`${hidden} unchanged lines`); + } + if (hasChangeAfter) { + for (let j = tailStart; j < end; j++) result.push(raw[j].html); + } + } + i = end; + } else { + result.push(raw[i].html); + i++; + } + } + + return `
${result.join('\n')}
`; +} + +function computeLCS(a: string[], b: string[]): string[] { + const m = a.length, n = b.length; + // Cap to avoid OOM on huge files + if (m * n > 500000) { + // Fallback: line-by-line comparison for large files + const result: string[] = []; + const max = Math.max(m, n); + for (let i = 0; i < max; i++) { + if (i < m && i < n && a[i] === b[i]) result.push(a[i]); + } + return result; + } + const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + const result: string[] = []; + let i = m, j = n; + while (i > 0 && j > 0) { + if (a[i - 1] === b[j - 1]) { result.unshift(a[i - 1]); i--; j--; } + else if (dp[i - 1][j] > dp[i][j - 1]) i--; + else j--; + } + return result; +} + export function renderPart(part: ContentPart): string { const { type, data } = part; @@ -66,30 +411,23 @@ export function renderPart(part: ContentPart): string { const name = (data.name as string) || 'tool'; const input = (data.input as string) || ''; const finished = data.finished as boolean | undefined; - let inputPreview = ''; - if (input) { - try { - const parsed = JSON.parse(input); - inputPreview = escapeHTML(JSON.stringify(parsed, null, 2).slice(0, 300)); - } catch { - inputPreview = escapeHTML(input.slice(0, 300)); - } - } - return `
-
${escapeHTML(name)}${finished ? '' : ' '}
- ${inputPreview ? `
${inputPreview}
` : ''} -
`; + const params = parseInput(input); + const header = renderToolCallHeader(name, params, finished); + return `
${header}
`; } case 'tool_result': { const name = (data.name as string) || 'result'; const content = (data.content as string) || ''; + const metadata = data.metadata as string | undefined; const isError = data.is_error as boolean | undefined; const cls = isError ? 'err' : 'ok'; const icon = isError ? 'circle-x' : 'circle-check'; + const label = toolLabel(name); + const body = renderToolResultBody(name, content, metadata); return `
-
${escapeHTML(name)}
- ${content ? `
${escapeHTML(content.slice(0, 800))}
` : ''} +
${escapeHTML(label)}
+ ${body}
`; } diff --git a/pwa/src/styles.css b/pwa/src/styles.css index 72fc35c..e438ff9 100644 --- a/pwa/src/styles.css +++ b/pwa/src/styles.css @@ -57,26 +57,55 @@ html, body { .btn-primary:disabled { opacity: 0.3; cursor: not-allowed; } .error-msg { color: var(--red); font-size: 0.82rem; } -/* Header */ -.chat-header { - display: flex; align-items: center; justify-content: space-between; - padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); +/* Chat layout */ +.chat-layout { display: flex; flex: 1; overflow: hidden; } + +/* Sidebar (right context panel) */ +.sidebar { + width: 220px; border-left: 1px solid var(--border); background: var(--surface); + display: flex; flex-direction: column; padding: 0.75rem; gap: 0.1rem; + flex-shrink: 0; overflow-y: auto; font-size: 0.78rem; } -.chat-header .title { font-weight: 600; font-size: 0.8rem; color: var(--fg-dim); } -.chat-header button { +.sidebar-logo { display: flex; align-items: center; gap: 0.4rem; margin-bottom: 0.5rem; } +.sidebar-logo .logo-img-sm { width: 22px; height: 22px; border-radius: 4px; } +.sidebar-logo .logo-meta { font-size: 0.68rem; } +.sidebar-section { padding: 0.2rem 0; } +.sidebar-label { font-size: 0.6rem; color: var(--fg-most); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 0.1rem; } +.sidebar-value { color: var(--fg-dim); word-break: break-all; } +.sidebar-title { color: var(--fg); font-weight: 600; line-height: 1.3; margin-bottom: 0.3rem; } +.sidebar-spacer { flex: 1; } +.sidebar-actions { display: flex; flex-direction: column; gap: 0.3rem; padding-top: 0.5rem; border-top: 1px solid var(--border); } +.sidebar-actions button { background: none; border: 1px solid var(--border); color: var(--fg-dim); font-family: var(--mono); font-size: 0.7rem; cursor: pointer; - padding: 0.25rem 0.5rem; border-radius: 3px; + padding: 0.3rem 0.5rem; border-radius: 3px; text-align: center; } -.chat-header button:hover { border-color: var(--fg-dim); color: var(--fg); } +.sidebar-actions button:hover { border-color: var(--fg-dim); color: var(--fg); } + +/* Chat main area */ +.chat-main { display: flex; flex-direction: column; flex: 1; min-width: 0; } /* Messages */ .messages { flex: 1; overflow-y: auto; padding: 0.75rem 1rem; display: flex; flex-direction: column; gap: 0.5rem; } -@media (min-width: 800px) { - .messages { max-width: 720px; margin-left: auto; margin-right: auto; } +@media (min-width: 900px) { + .messages { padding: 0.75rem calc(50% - 340px); } +} + +/* Input */ +.input-bar { + display: flex; align-items: flex-end; gap: 0.4rem; + padding: 0.5rem 1rem; border-top: 1px solid var(--border); +} +@media (min-width: 900px) { + .input-bar { padding: 0.5rem calc(50% - 340px); } +} + +/* Mobile: hide sidebar */ +@media (max-width: 700px) { + .sidebar { display: none; } } .msg { padding: 0.4rem 0; font-size: 0.95rem; line-height: 1.65; } .msg.user { border-left: 2px solid var(--accent); padding-left: 0.75rem; margin-top: 0.5rem; } @@ -99,14 +128,11 @@ html, body { } .tool-call-header { display: flex; align-items: center; gap: 0.4rem; - padding: 0.3rem 0.5rem; background: var(--surface); - font-size: 0.78rem; border-bottom: 1px solid var(--border); -} -.tool-call .name { color: var(--accent); font-weight: 600; } -.tool-input { - padding: 0.3rem 0.5rem; font-size: 0.72rem; color: var(--fg-dim); - max-height: 120px; overflow-y: auto; white-space: pre-wrap; + padding: 0.35rem 0.5rem; background: var(--surface); + font-size: 0.8rem; } +.tool-call .name { color: var(--accent); font-weight: 600; white-space: nowrap; } +.tool-detail { color: var(--fg-dim); font-size: 0.75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } /* Tool results */ .tool-result { @@ -117,14 +143,59 @@ html, body { .tool-result.err { border-left: 2px solid var(--red); } .tool-result-header { padding: 0.25rem 0.5rem; font-size: 0.72rem; font-weight: 600; - background: var(--surface); border-bottom: 1px solid var(--border); + background: var(--surface); display: flex; align-items: center; gap: 0.3rem; } .tool-result.ok .tool-result-header { color: var(--green); } .tool-result.err .tool-result-header { color: var(--red); } -.tool-result pre { - margin: 0; padding: 0.3rem 0.5rem; max-height: 180px; overflow-y: auto; - border: none; background: transparent; font-size: 0.72rem; +.tool-output { + margin: 0; padding: 0.35rem 0.5rem; max-height: 250px; overflow-y: auto; + font-size: 0.75rem; line-height: 1.3; white-space: pre-wrap; word-break: break-all; } +.tool-output code { font-family: var(--mono); } +.tool-output-empty { padding: 0.25rem 0.5rem; font-size: 0.72rem; color: var(--fg-faint); font-style: italic; } + +/* Diffs */ +.diff-add { color: #629657; background: rgba(98, 150, 87, 0.08); } +.diff-del { color: #a45c59; background: rgba(164, 92, 89, 0.08); } +.diff-ctx { color: var(--fg-dim); } +.diff-fold { color: var(--fg-most); font-style: italic; display: block; padding: 0.15rem 0; } + +/* highlight.js dark theme overrides */ +.hljs { background: transparent; color: var(--fg); } +.hljs-keyword { color: #FF60FF; } +.hljs-string { color: #BF976F; } +.hljs-number { color: #68FFD6; } +.hljs-comment { color: var(--fg-most); font-style: italic; } +.hljs-function { color: #6B50FF; } +.hljs-title { color: #8B75FF; } +.hljs-built_in { color: #FF79D0; } +.hljs-attr { color: #D46EFF; } +.hljs-tag { color: #D46EFF; } +.hljs-name { color: #D46EFF; } +.hljs-type { color: #7272FF; } +.hljs-variable { color: #ECEBF0; } +.hljs-literal { color: #68FFD6; } +.hljs-operator { color: #FF7F90; } +.hljs-punctuation { color: var(--fg-dim); } +.hljs-property { color: #8B75FF; } +.hljs-regexp { color: #FF7F90; } +.hljs-symbol { color: #E8FF27; } +.hljs-meta { color: var(--fg-most); } +.hljs-section { color: #FF60FF; font-weight: bold; } +.hljs-selector-class { color: #D46EFF; } +.hljs-selector-id { color: #6B50FF; } +.hljs-doctag { color: var(--fg-most); } +.hljs-template-tag { color: #E8FF27; } +.hljs-template-variable { color: #68FFD6; } + +/* Todos */ +.todo-list { padding: 0.3rem 0.5rem; } +.todo-item { font-size: 0.78rem; padding: 0.1rem 0; display: flex; gap: 0.4rem; align-items: baseline; } +.todo-icon { width: 1em; text-align: center; flex-shrink: 0; } +.todo-item.done { color: var(--fg-dim); text-decoration: line-through; } +.todo-item.done .todo-icon { color: var(--green); } +.todo-item.active { color: var(--fg); } +.todo-item.active .todo-icon { color: var(--accent); } /* Thinking */ .thinking-block summary { @@ -160,7 +231,10 @@ html, body { /* Input */ .input-bar { display: flex; align-items: flex-end; gap: 0.4rem; - padding: 0.5rem 0.75rem; border-top: 1px solid var(--border); + padding: 0.5rem 1rem; border-top: 1px solid var(--border); +} +@media (min-width: 800px) { + .input-bar { max-width: 720px; margin: 0 auto; width: 100%; } } .input-bar textarea { flex: 1; resize: none; border: 1px solid var(--border); @@ -178,7 +252,7 @@ html, body { .input-bar button [data-lucide] { width: 20px; height: 20px; } .input-bar button:disabled { color: var(--fg-faint); cursor: default; } .input-bar .stop-btn { color: var(--red); } -.input-bar .stop-btn [data-lucide] { width: 16px; height: 16px; fill: currentColor; stroke: none; } +.input-bar .stop-btn [data-lucide] { width: 20px; height: 20px; fill: currentColor; stroke: none; } .spinner { display: inline-block; width: 12px; height: 12px;