-
- {{/* Fallback banner (admin-only, shown by Events role.fallback) */}}
-
-
- {{/* ── Sidebar ─────────────────────── */}}
-
-
- {{/* Sidebar overlay (mobile) */}}
-
-
- {{/* Mobile menu button */}}
-
-
- {{/* ── Workspace ───────────────────── */}}
-
-
-
- {{/* Chat header: model selector + caps + tokens + panel + notif */}}
-
-
- {{/* v0.23.2: Channel context banner — shown for DMs and named channels */}}
-
-
- {{/* v0.27.0: Workflow stage indicator — shown for workflow channels */}}
-
-
- {{/* Messages */}}
-
- {{/* Populated by UI.renderMessages() */}}
-
-
- {{/* Context warning */}}
-
-
-
-
- {{/* File strip */}}
-
-
- {{/* Streaming tools display */}}
-
-
- {{/* Input area (centered, padded bottom) */}}
-
-
-
- {{/* Resize handle */}}
-
-
- {{/* Side panel (secondary pane) */}}
-
-
-
- {{/* Preview panel page */}}
-
-
-
-
Open a code block preview or extension output here.
-
-
- {{/* Notes and project panels are registered dynamically by their JS modules */}}
-
-
-
+{{/* ── Mount Point ────────────────────────── */}}
+
-{{/* Side panel overlay (mobile) */}}
-
-
-{{/* ── Lightbox ────────────────────────────── */}}
-
-
-
-
-{{/* ── Command Palette ─────────────────────── */}}
-
-
-{{/* ── Hidden File Input ─────── */}}
-
-
-{{/* Team Admin Modal — removed in v0.37.7. Team admin is now at /team-admin. */}}
-
-{{/* ── Debug Modal moved to base.html (available on all surfaces) ── */}}
-
-{{/* ── Notification bell is now inside .chat-header ── */}}
-
-{{/* ── Routing Policy Form (shared) ────────── */}}
-
-
-{{/* ── Auto-Compact Toggle (chat header option) */}}
-
- Auto-compact
-
-
-{{/* ── Fetch Models Button (admin) ─────────── */}}
-
-
{{end}}
{{define "css-chat"}}
-
-
-
-
-
+
{{end}}
{{define "scripts-chat"}}
-{{/* CM6 bundle — chat-specific (code input, extension editor) */}}
-
-{{/* App JS — order matches index.html (minus api/events/ui-primitives/ui-format already in base) */}}
-
-
-{{/* ui-settings.js and ui-admin.js removed in v0.37.7 — team admin is now at /team-admin */}}
-
-{{/* notes.js removed in v0.37.9 — see sw/components/notes-pane/ */}}
-
-
-
-{{/* memory-ui.js and persona-kb.js removed in v0.37.7 — no chat surface callers */}}
-
-
-{{/* notification-prefs.js removed in v0.37.7 — now native Preact in settings */}}
-
-
-{{/* settings-handlers.js, admin-handlers.js, workflow-admin.js removed in v0.37.7 */}}
-
-
-
-
+
+
+
{{end}}
diff --git a/server/version.go b/server/version.go
index e13a5bd..8e329ce 100644
--- a/server/version.go
+++ b/server/version.go
@@ -1,3 +1,3 @@
package main
-const Version = "0.37.9"
+const Version = "0.37.10"
diff --git a/src/css/chat-pane.css b/src/css/chat-pane.css
deleted file mode 100644
index 7fcfe9a..0000000
--- a/src/css/chat-pane.css
+++ /dev/null
@@ -1,412 +0,0 @@
-/* ==========================================
- Chat Switchboard — ChatPane Component CSS
- ==========================================
- v0.25.0-cs11.1: Self-contained styles for the ChatPane
- component. Works in any context — sidebar chat, editor
- assist pane, standalone embed. No external CSS dependencies.
- ========================================== */
-
-/* ── Container ─────────────────────────────── */
-
-.chat-pane {
- display: flex;
- flex-direction: column;
- height: 100%;
- overflow: hidden;
- background: var(--bg, #0e0e10);
-}
-
-/* ── Header Bar (standalone mode) ──────────── */
-
-.chat-pane-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 6px;
- padding: 6px 10px;
- border-bottom: 1px solid var(--border, #2a2a2e);
- background: var(--bg-secondary, #151517);
- flex-shrink: 0;
- min-height: 36px;
-}
-
-.chat-pane-header-left {
- display: flex;
- align-items: center;
- gap: 4px;
- flex: 1;
- min-width: 0;
-}
-
-.chat-pane-header-right {
- display: flex;
- align-items: center;
- gap: 4px;
- flex-shrink: 0;
-}
-
-.chat-pane-chat-select {
- flex: 1;
- min-width: 0;
- background: var(--bg, #0e0e10);
- border: 1px solid var(--border, #2a2a2e);
- border-radius: 4px;
- color: var(--text, #eee);
- font-size: 11px;
- font-family: inherit;
- padding: 4px 6px;
- cursor: pointer;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.chat-pane-chat-select:focus {
- border-color: var(--accent, #b38a4e);
- outline: none;
-}
-
-.chat-pane-new-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 26px;
- height: 26px;
- background: none;
- border: 1px solid var(--border, #2a2a2e);
- border-radius: 4px;
- color: var(--text-2, #999);
- cursor: pointer;
- flex-shrink: 0;
- transition: color 0.15s, border-color 0.15s, background 0.15s;
-}
-
-.chat-pane-new-btn:hover {
- color: var(--accent, #b38a4e);
- border-color: var(--accent, #b38a4e);
- background: var(--accent-dim, rgba(179, 138, 78, 0.1));
-}
-
-/* Model selector in header */
-.chat-pane-model-select {
- background: var(--bg, #0e0e10);
- border: 1px solid var(--border, #2a2a2e);
- border-radius: 4px;
- color: var(--text-2, #999);
- font-size: 11px;
- font-family: inherit;
- padding: 4px 6px;
- cursor: pointer;
- max-width: 140px;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-
-.chat-pane-model-select:focus {
- border-color: var(--accent, #b38a4e);
- outline: none;
- color: var(--text, #eee);
-}
-
-/* ── Messages Area ─────────────────────────── */
-
-.chat-pane-messages {
- flex: 1;
- overflow-y: auto;
- scroll-behavior: smooth;
- padding: 12px 0;
- min-height: 0;
-}
-
-/* ── Message Bubbles ───────────────────────── */
-
-.chat-msg {
- padding: 8px 16px;
- font-size: var(--msg-font, 14px);
- line-height: 1.65;
- color: var(--text, #eee);
-}
-
-.chat-msg + .chat-msg {
- margin-top: 2px;
-}
-
-/* User messages — right-aligned bubble */
-.chat-msg--user {
- display: flex;
- justify-content: flex-end;
- padding: 8px 16px;
-}
-
-.chat-msg--user .chat-msg__content {
- background: var(--accent-dim, rgba(179, 138, 78, 0.15));
- color: var(--text, #eee);
- padding: 8px 14px;
- border-radius: 12px 12px 2px 12px;
- max-width: 85%;
- word-wrap: break-word;
-}
-
-/* Assistant messages — left-aligned */
-.chat-msg--assistant {
- padding: 8px 16px;
-}
-
-.chat-msg--assistant .chat-msg__content {
- max-width: 100%;
- word-wrap: break-word;
-}
-
-/* ── Streaming indicator ───────────────────── */
-
-.chat-msg--streaming .chat-msg__content::after {
- content: '';
- display: inline-block;
- width: 6px;
- height: 14px;
- background: var(--accent, #b38a4e);
- margin-left: 2px;
- vertical-align: text-bottom;
- animation: cp-cursor-blink 0.8s step-end infinite;
-}
-
-@keyframes cp-cursor-blink {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0; }
-}
-
-/* ── Typing dots ───────────────────────────── */
-
-.chat-msg--typing {
- padding: 8px 16px;
-}
-
-/* System/error messages */
-.chat-msg--system {
- padding: 6px 16px;
- font-size: 12px;
-}
-
-.chat-msg--typing .typing-dots {
- display: flex;
- gap: 4px;
- padding: 4px 0;
-}
-
-.chat-msg--typing .typing-dots span {
- width: 6px;
- height: 6px;
- border-radius: 50%;
- background: var(--text-3, #555);
- animation: cp-dot-bounce 1.2s ease-in-out infinite;
-}
-
-.chat-msg--typing .typing-dots span:nth-child(2) { animation-delay: 0.2s; }
-.chat-msg--typing .typing-dots span:nth-child(3) { animation-delay: 0.4s; }
-
-@keyframes cp-dot-bounce {
- 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
- 30% { transform: translateY(-4px); opacity: 1; }
-}
-
-/* ── Markdown content ──────────────────────── */
-
-.chat-msg__content p { margin: 0 0 0.5em; }
-.chat-msg__content p:last-child { margin-bottom: 0; }
-
-.chat-msg__content pre {
- background: var(--bg-secondary, #151517);
- border: 1px solid var(--border, #2a2a2e);
- border-radius: 6px;
- padding: 10px 12px;
- overflow-x: auto;
- margin: 8px 0;
- font-size: 12px;
- line-height: 1.5;
- font-family: var(--mono, 'SF Mono', monospace);
-}
-
-.chat-msg__content code {
- background: var(--bg-secondary, #151517);
- padding: 1px 5px;
- border-radius: 3px;
- font-size: 0.88em;
- font-family: var(--mono, 'SF Mono', monospace);
-}
-
-.chat-msg__content pre code {
- background: none;
- padding: 0;
- border-radius: 0;
- font-size: inherit;
-}
-
-.chat-msg__content ul, .chat-msg__content ol {
- margin: 4px 0;
- padding-left: 1.5em;
-}
-
-.chat-msg__content blockquote {
- border-left: 3px solid var(--accent, #b38a4e);
- padding: 4px 12px;
- margin: 8px 0;
- color: var(--text-2, #999);
-}
-
-.chat-msg__content a {
- color: var(--accent, #b38a4e);
- text-decoration: none;
-}
-
-.chat-msg__content a:hover {
- text-decoration: underline;
-}
-
-.chat-msg__content table {
- border-collapse: collapse;
- margin: 8px 0;
- font-size: 13px;
-}
-
-.chat-msg__content th,
-.chat-msg__content td {
- border: 1px solid var(--border, #2a2a2e);
- padding: 4px 8px;
-}
-
-.chat-msg__content th {
- background: var(--bg-secondary, #151517);
- font-weight: 600;
-}
-
-/* ── Welcome / Empty State ─────────────────── */
-
-.chat-welcome {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 100%;
- text-align: center;
- padding: 40px 20px;
- opacity: 0.6;
-}
-
-.chat-welcome__title {
- font-size: 15px;
- font-weight: 600;
- color: var(--text, #eee);
- margin: 0 0 6px;
-}
-
-.chat-welcome__hint {
- font-size: 13px;
- color: var(--text-3, #555);
- margin: 0;
-}
-
-/* ── Input Bar ─────────────────────────────── */
-
-.chat-pane-input-bar {
- flex-shrink: 0;
- border-top: 1px solid var(--border, #2a2a2e);
- background: var(--bg-secondary, #151517);
- padding: 8px 12px;
-}
-
-.chat-pane-input-wrap {
- display: flex;
- align-items: flex-end;
- gap: 6px;
- background: var(--bg, #0e0e10);
- border: 1px solid var(--border, #2a2a2e);
- border-radius: 8px;
- padding: 6px 8px;
- transition: border-color 0.15s;
-}
-
-.chat-pane-input-wrap:focus-within {
- border-color: var(--accent, #b38a4e);
-}
-
-.chat-pane-input {
- flex: 1;
- min-height: 20px;
- max-height: 160px;
- overflow-y: auto;
- font-size: 13px;
- color: var(--text, #eee);
- line-height: 1.5;
-}
-
-/* CM6 editor inside chat-pane-input */
-.chat-pane-input .cm-editor {
- max-height: 160px;
- overflow-y: auto;
- font-size: 13px;
-}
-
-.chat-pane-input .cm-editor .cm-content {
- padding: 0;
-}
-
-.chat-pane-input .cm-editor .cm-line {
- padding: 0;
-}
-
-.chat-pane-input .cm-editor.cm-focused {
- outline: none;
-}
-
-/* Textarea fallback */
-.chat-pane-input textarea {
- width: 100%;
- min-height: 20px;
- max-height: 160px;
- background: transparent;
- border: none;
- color: var(--text, #eee);
- font-size: 13px;
- font-family: inherit;
- line-height: 1.5;
- resize: none;
- outline: none;
- padding: 0;
-}
-
-/* ── Send Button ───────────────────────────── */
-
-.chat-pane-send {
- flex-shrink: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- width: 30px;
- height: 30px;
- background: none;
- border: none;
- border-radius: 6px;
- color: var(--accent, #b38a4e);
- cursor: pointer;
- transition: background 0.15s, color 0.15s;
-}
-
-.chat-pane-send:hover {
- background: var(--accent-dim, rgba(179, 138, 78, 0.15));
- color: var(--accent-hover, #c9a05e);
-}
-
-.chat-pane-send:disabled {
- opacity: 0.3;
- cursor: not-allowed;
-}
-
-/* ── Toolbar ───────────────────────────────── */
-
-.chat-pane-toolbar {
- margin-top: 6px;
-}
-
-.chat-pane-toolbar:empty {
- display: none;
- margin: 0;
-}
diff --git a/src/css/chat.css b/src/css/chat.css
deleted file mode 100644
index 97249ea..0000000
--- a/src/css/chat.css
+++ /dev/null
@@ -1,648 +0,0 @@
-/* ── chat.css ────────────────────────────────
- Chat area, model dropdown, markdown, input, files
- ──────────────────────────────────────────── */
-
-/* ── Chat Area ───────────────────────────── */
-
-/* .chat-area is now .workspace-primary — alias kept for compat */
-.chat-area, .workspace-primary { flex: 1; display: flex; flex-direction: column; min-width: 0; background: var(--bg); }
-
-/* Role fallback banner */
-.role-fallback-banner {
- background: var(--warning-dim); color: var(--warning);
- text-align: center; font-size: 12px; font-weight: 500;
- padding: 6px 12px; flex-shrink: 0;
-}
-
-/* Chat header (model selector + caps + token count + panel toggle) */
-.chat-header {
- display: flex; align-items: center; gap: 8px;
- padding: 8px 16px; flex-shrink: 0;
- border-bottom: 1px solid var(--border);
-}
-.chat-header-right {
- margin-left: auto; display: flex; align-items: center; gap: 8px;
-}
-.token-count {
- font-size: 11px; color: var(--text-3);
- font-family: var(--mono); white-space: nowrap;
-}
-.summarized-badge {
- font-size: 11px; color: var(--text-3); white-space: nowrap;
- display: inline-flex; align-items: center; gap: 4px;
-}
-
-/* v0.23.2: Channel context banner */
-.channel-context-banner {
- padding: 6px 16px;
- font-size: 12px;
- color: var(--text-3);
- background: var(--bg-raised);
- border-bottom: 1px solid var(--border);
- display: flex;
- align-items: center;
- gap: 8px;
- flex-shrink: 0;
-}
-.channel-context-banner .ctx-mode {
- font-family: var(--mono);
- font-size: 11px;
- padding: 1px 6px;
- border-radius: 3px;
- background: var(--bg-surface);
- color: var(--text-2);
-}
-.channel-context-banner .ctx-mode-btn {
- cursor: pointer; border: 1px solid var(--border);
- transition: background 0.15s;
-}
-.channel-context-banner .ctx-mode-btn:hover {
- background: var(--bg-hover); color: var(--text);
-}
-.channel-context-banner .ctx-topic {
- color: var(--text-2); font-size: 12px;
-}
-.channel-context-banner .ctx-hint {
- color: var(--text-3);
- font-style: italic;
-}
-.ctx-participants-btn {
- margin-left: auto;
- background: none; border: 1px solid var(--border); border-radius: 4px;
- color: var(--text-2); font-size: 11px; padding: 2px 8px; cursor: pointer;
-}
-.ctx-participants-btn:hover { background: var(--bg-hover); color: var(--text); }
-.hover-bg:hover { background: var(--bg-hover); }
-
-/* Message container (scrollable) */
-.msg-container {
- flex: 1; overflow-y: auto; scroll-behavior: smooth;
- padding-top: 12px; padding-bottom: 8px;
-}
-.msg-container::-webkit-scrollbar { width: 5px; }
-.msg-container::-webkit-scrollbar-track { background: transparent; }
-.msg-container::-webkit-scrollbar-thumb { border-radius: 3px; background: var(--border); }
-
-/* Streaming tools display */
-.stream-tools {
- padding: 8px 24px; flex-shrink: 0;
- font-size: 12px; color: var(--text-3);
- display: flex; gap: 6px; align-items: center;
-}
-
-.model-bar {
- display: flex; align-items: center; gap: 6px;
- padding: 8px 16px; flex-shrink: 0;
-}
-.model-bar select {
- background: transparent; border: 1px solid transparent;
- color: var(--text); font-size: 14px; font-weight: 500;
- padding: 4px 8px; border-radius: var(--radius); cursor: pointer;
- font-family: var(--font); max-width: 300px;
- transition: border-color var(--transition);
-}
-.model-bar select:hover { border-color: var(--border); }
-.model-bar select:focus { outline: none; border-color: var(--accent); }
-.model-bar select option { background: var(--bg-surface); color: var(--text); }
-
-/* ── Custom Model Dropdown ───────────────── */
-
-.model-dropdown { position: relative; }
-.model-dropdown-btn {
- display: flex; align-items: center; gap: 6px;
- background: none; border: 1px solid transparent;
- color: var(--text); font-size: 14px; font-weight: 500;
- padding: 4px 8px; border-radius: var(--radius); cursor: pointer;
- font-family: var(--font); max-width: 340px;
- transition: border-color var(--transition);
-}
-.model-dropdown-btn:hover { border-color: var(--border); }
-.model-dropdown-btn svg { flex-shrink: 0; opacity: 0.5; }
-.model-dropdown-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.model-dropdown-menu {
- display: none; position: absolute; top: 100%; left: 0; z-index: 200;
- min-width: 280px; max-width: 400px; max-height: 400px; overflow-y: auto;
- background: var(--bg-raised); border: 1px solid var(--border-light);
- border-radius: var(--radius); padding: 4px;
- box-shadow: 0 4px 16px rgba(0,0,0,0.4);
- margin-top: 4px;
-}
-.model-dropdown-menu.open { display: block; }
-.model-dropdown-group {
- padding: 6px 10px 4px; font-size: 11px; font-weight: 600;
- color: var(--text-3); text-transform: uppercase; letter-spacing: 0.5px;
-}
-.model-dropdown-item {
- display: flex; align-items: center; gap: 8px;
- padding: 7px 10px; border-radius: 4px;
- cursor: pointer; font-size: 13px;
- color: var(--text-2); white-space: nowrap;
- transition: background var(--transition), color var(--transition);
-}
-.model-dropdown-item:hover { background: var(--bg-hover); color: var(--text); }
-.model-dropdown-item.selected { background: var(--bg-hover); color: var(--accent); }
-.model-dropdown-item .item-label { flex: 1; overflow: hidden; text-overflow: ellipsis; }
-.model-dropdown-item .item-provider { margin-left: auto; font-size: 10px; color: var(--text-3); }
-.dropdown-avatar { width: 18px; height: 18px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
-
-.model-caps {
- display: flex; align-items: center; gap: 4px;
- margin-left: 4px; flex-wrap: wrap;
-}
-.cap-badge {
- display: inline-flex; align-items: center; gap: 3px;
- font-size: 10px; line-height: 1; padding: 2px 6px;
- border-radius: 3px; white-space: nowrap;
- background: var(--bg-raised); color: var(--text-3);
- border: 1px solid transparent;
-}
-.cap-badge.cap-accent {
- color: var(--accent); border-color: color-mix(in srgb, var(--accent), transparent 80%);
- background: color-mix(in srgb, var(--accent), transparent 92%);
-}
-.cap-badge.cap-context {
- color: var(--text-3);
-}
-
-/* Messages — Bubble Layout (matching prototype) */
-.messages { flex: 1; overflow-y: auto; scroll-behavior: smooth; }
-
-.message { padding: 10px 24px; }
-
-.msg-inner { max-width: 768px; margin: 0 auto; display: flex; gap: 10px; }
-
-/* User messages: same fixed-width container, mirrored */
-.message.user .msg-inner { flex-direction: row-reverse; }
-.message.user .msg-body {
- background: var(--user-bubble);
- border: 1px solid transparent;
- border-radius: 16px 16px 4px 16px;
- padding: 10px 14px;
-}
-.message.user .msg-head { flex-direction: row-reverse; }
-.message.user .msg-role { color: var(--accent); }
-.message.user .msg-avatar { color: var(--accent); }
-
-/* Assistant messages: left-aligned bubble */
-.message.assistant .msg-body {
- background: var(--bg-surface);
- border: 1px solid var(--border);
- border-radius: 16px 16px 16px 4px;
- padding: 10px 14px;
-}
-
-.msg-avatar {
- width: 28px; height: 28px; border-radius: 50%;
- display: flex; align-items: center; justify-content: center;
- font-size: 14px; flex-shrink: 0; margin-top: 2px;
- overflow: hidden;
-}
-.msg-avatar-img {
- width: 100%; height: 100%; object-fit: cover; border-radius: 50%;
-}
-.message.user .msg-avatar { background: var(--accent-dim); }
-.message.assistant .msg-avatar { background: var(--bg-raised); }
-
-/* Other human's messages — left-aligned like assistant, teal accent */
-.message.other-user .msg-body {
- background: var(--bg-surface);
- border: 1px solid var(--border);
- border-radius: 16px 16px 16px 4px;
- padding: 10px 14px;
-}
-.message.other-user .msg-role { color: #2dd4bf; }
-.message.other-user .msg-avatar { background: rgba(45,212,191,0.12); color: #2dd4bf; }
-
-.msg-body { flex: 1; min-width: 0; }
-.msg-head { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
-.msg-role { font-size: 13px; font-weight: 600; }
-.msg-time { font-size: 11px; color: var(--text-3); }
-.msg-actions {
- margin-left: auto; display: flex; gap: 2px; opacity: 0;
- transition: opacity var(--transition);
-}
-.message:hover .msg-actions { opacity: 1; }
-.msg-action-btn {
- background: none; border: none; color: var(--text-3); cursor: pointer;
- padding: 2px 6px; border-radius: 4px; font-size: 11px;
- transition: background var(--transition), color var(--transition);
-}
-.msg-action-btn:hover { background: var(--bg-hover); color: var(--text); }
-
-/* Branch navigation indicator */
-.branch-nav {
- display: inline-flex;
- align-items: center;
- gap: 2px;
- font-size: 12px;
- color: var(--text-3);
-}
-.branch-arrow {
- background: none;
- border: 1px solid var(--border);
- border-radius: 4px;
- color: var(--text-2);
- cursor: pointer;
- padding: 0 5px;
- font-size: 16px;
- line-height: 18px;
- transition: var(--transition);
-}
-.branch-arrow:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); }
-.branch-arrow:disabled { opacity: 0.25; cursor: default; }
-.branch-pos {
- font-size: 11px;
- font-variant-numeric: tabular-nums;
- min-width: 28px;
- text-align: center;
- user-select: none;
-}
-
-/* Inline message editing */
-.msg-edit-input {
- width: 100%;
- background: var(--bg-surface);
- border: 1px solid var(--accent);
- border-radius: var(--radius);
- color: var(--text);
- padding: 10px 12px;
- font-family: var(--font);
- font-size: var(--msg-font, 14px);
- line-height: 1.6;
- resize: vertical;
- min-height: 60px;
- max-height: 400px;
- outline: none;
-}
-.msg-edit-input:focus { border-color: var(--accent-hover); box-shadow: 0 0 0 2px var(--accent-dim); }
-.msg-edit-actions {
- display: flex;
- gap: 8px;
- margin-top: 8px;
- justify-content: flex-end;
-}
-.msg-edit-cancel, .msg-edit-submit {
- padding: 6px 14px;
- border-radius: var(--radius);
- font-size: 13px;
- font-weight: 500;
- cursor: pointer;
- transition: var(--transition);
- border: 1px solid var(--border);
-}
-.msg-edit-cancel {
- background: var(--bg-raised);
- color: var(--text-2);
-}
-.msg-edit-cancel:hover { background: var(--bg-hover); color: var(--text); }
-.msg-edit-submit {
- background: var(--accent);
- color: var(--text-on-color);
- border-color: var(--accent);
-}
-.msg-edit-submit:hover { background: var(--accent-hover); }
-
-.msg-text { font-size: var(--msg-font, 14px); line-height: 1.7; }
-
-/* ── Markdown Content ────────────────────── */
-
-.msg-text p { margin: 0.4em 0; }
-.msg-text p:first-child { margin-top: 0; }
-.msg-text p:last-child { margin-bottom: 0; }
-
-.msg-text pre {
- background: var(--bg-surface); border: 1px solid var(--border);
- padding: 0.75rem 1rem; border-radius: var(--radius);
- overflow-x: auto; margin: 0.75rem 0; position: relative;
- font-size: 13px;
-}
-.msg-text code {
- font-family: var(--mono); font-size: 0.9em;
- background: var(--bg-raised); padding: 0.15em 0.4em;
- border-radius: 4px;
-}
-.msg-text pre code { background: none; padding: 0; font-size: inherit; }
-
-/* Code block toolbar */
-.code-toolbar {
- position: absolute; top: 6px; right: 6px;
- display: flex; gap: 4px; align-items: center;
- opacity: 0; transition: opacity var(--transition);
-}
-.msg-text pre:hover .code-toolbar { opacity: 1; }
-.code-block.code-collapsed .code-toolbar { opacity: 1; }
-.copy-code-btn {
- background: var(--bg-raised); border: 1px solid var(--border);
- color: var(--text-3); border-radius: 4px;
- padding: 2px 10px; font-size: 11px; cursor: pointer;
- transition: color var(--transition), border-color var(--transition);
-}
-.copy-code-btn:hover { color: var(--text); border-color: var(--border-light); }
-.code-collapse-btn {
- background: var(--bg-raised); border: 1px solid var(--border);
- color: var(--text-3); border-radius: 4px;
- padding: 2px 10px; font-size: 11px; cursor: pointer;
- font-family: var(--mono); white-space: nowrap;
- transition: color var(--transition), border-color var(--transition);
-}
-.code-collapse-btn:hover { color: var(--text); border-color: var(--border-light); }
-
-/* Collapsed code block */
-.code-block.code-collapsed code {
- max-height: 3.6em; overflow: hidden; display: block;
- mask-image: linear-gradient(to bottom, #000 40%, transparent 100%);
- -webkit-mask-image: linear-gradient(to bottom, #000 40%, transparent 100%);
-}
-
-/* Language label */
-.code-lang {
- position: absolute; top: 6px; left: 10px;
- font-size: 10px; color: var(--text-3); font-family: var(--mono);
- text-transform: uppercase; letter-spacing: 0.5px; user-select: none;
-}
-
-/* ── Extension-rendered blocks ──────── */
-
-.ext-rendered {
- margin: 12px 0;
- position: relative;
-}
-.ext-popout-btn {
- position: absolute; top: 4px; right: 4px;
- background: var(--bg-raised); border: 1px solid var(--border);
- color: var(--text-3); cursor: pointer; font-size: 14px;
- width: 26px; height: 26px; border-radius: var(--radius);
- display: flex; align-items: center; justify-content: center;
- opacity: 0; transition: opacity 0.15s, background 0.15s;
- z-index: 5;
-}
-.ext-rendered:hover .ext-popout-btn { opacity: 1; }
-.ext-popout-btn:hover { background: var(--bg-hover); color: var(--text); }
-
-/* HTML preview */
-
-/* ── Input Area ──────────────────────────── */
-
-.input-area { padding: 0 1rem 1rem; flex-shrink: 0; }
-.input-area:empty { padding: 0; min-height: 0; }
-
-/* Token counter below input */
-.input-meta { display: flex; justify-content: space-between; align-items: center; padding: 2px 12px 0; min-height: 18px; }
-.input-token-count { font-size: 0.68rem; color: var(--text-3); transition: color 0.2s; }
-.input-token-count.warning { color: var(--warning); }
-.input-token-count.danger { color: var(--danger); }
-
-/* Context length warning banner */
-.context-warning {
- display: flex; align-items: center; gap: 8px;
- padding: 6px 12px; margin-bottom: 8px; border-radius: 8px;
- background: color-mix(in srgb, var(--warning) 12%, transparent);
- border: 1px solid color-mix(in srgb, var(--warning) 25%, transparent);
- font-size: 0.78rem; color: var(--text-2); animation: fadeIn 0.2s ease;
-}
-.context-warning.danger {
- background: color-mix(in srgb, var(--danger) 12%, transparent);
- border-color: color-mix(in srgb, var(--danger) 25%, transparent);
-}
-.context-warning-icon { flex-shrink: 0; }
-.context-warning-text { flex: 1; }
-.context-warning-dismiss { background: none; border: none; color: var(--text-3); cursor: pointer; padding: 0 2px; font-size: 0.85rem; }
-.context-warning-dismiss:hover { color: var(--text); }
-.context-warning-action {
- flex-shrink: 0; padding: 3px 10px; border-radius: 6px; border: 1px solid var(--border);
- background: var(--bg-surface); color: var(--text); cursor: pointer;
- font-size: 0.75rem; white-space: nowrap; transition: all var(--transition);
-}
-.context-warning-action:hover { background: var(--accent); color: var(--text-on-color); border-color: var(--accent); }
-.context-warning-action:disabled { opacity: 0.5; cursor: not-allowed; }
-.context-warning-action:disabled:hover { background: var(--bg-surface); color: var(--text); border-color: var(--border); }
-.context-warning-toggle {
- display: inline-flex; align-items: center; gap: 4px; flex-shrink: 0;
- font-size: 0.72rem; color: var(--text-3); cursor: pointer; white-space: nowrap;
- user-select: none;
-}
-.context-warning-toggle input[type="checkbox"] { margin: 0; cursor: pointer; }
-/* Summary message node */
-.message-summary {
- border: 1px dashed color-mix(in srgb, var(--accent) 40%, transparent);
- border-radius: 8px; padding: 10px 14px; margin: 4px 0;
- background: color-mix(in srgb, var(--accent) 6%, transparent);
- font-size: 0.85rem; position: relative;
-}
-.message-summary .summary-header {
- display: flex; align-items: center; gap: 6px; margin-bottom: 6px;
- font-size: 0.75rem; color: var(--text-3); font-weight: 500;
-}
-.message-summary .summary-toggle {
- background: none; border: none; color: var(--accent); cursor: pointer;
- font-size: 0.75rem; padding: 0; text-decoration: underline;
-}
-.message-summary .summary-toggle:hover { color: var(--text); }
-.msg-dimmed { opacity: 0.5; }
-.message-summary-divider {
- text-align: center; padding: 8px 0; margin: 4px 0;
-}
-.message-summary-divider .summary-toggle {
- background: none; border: none; color: var(--text-3); cursor: pointer;
- font-size: 0.78rem; padding: 4px 12px; border-radius: 4px;
- transition: all var(--transition);
-}
-.message-summary-divider .summary-toggle:hover { color: var(--accent); background: color-mix(in srgb, var(--accent) 8%, transparent); }
-.input-wrap {
- max-width: 768px; margin: 0 auto; width: 100%; box-sizing: border-box;
- background: var(--bg-surface); border: 1px solid var(--border);
- border-radius: 12px; padding: 0;
- display: flex; align-items: flex-end;
- transition: border-color var(--transition);
-}
-.input-wrap:focus-within { border-color: var(--border-light); }
-
-.input-wrap textarea {
- flex: 1; resize: none; background: none; border: none;
- color: var(--text); padding: 12px 0 12px 14px;
- font-family: var(--font); font-size: 14px;
- max-height: 200px; line-height: 1.5;
-}
-.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; }
-
-/* Chat input area wrapper (bottom pinned, padded) */
-.chat-input-area {
- padding: 0 16px 16px; flex-shrink: 0;
-}
-.chat-footer {
- display: flex; justify-content: space-between; align-items: center;
- padding: 4px 4px 0; max-width: 768px; margin: 0 auto;
- min-height: 18px;
-}
-.chat-version { font-size: 10px; color: var(--text-3); }
-.input-token-count {
- font-size: 10px; color: var(--text-3);
- font-family: var(--mono); margin-right: 4px; white-space: nowrap;
-}
-
-/* Left-side toolbar: attach, tools, KB — single container handles alignment */
-.input-toolbar {
- display: flex; align-items: center; gap: 2px;
- padding: 0 4px 6px; flex-shrink: 0;
- max-width: 768px; margin: 0 auto; width: 100%;
- box-sizing: border-box;
-}
-
-.action-btn {
- background: none; border: none; color: var(--text-3); cursor: pointer;
- padding: 6px; border-radius: var(--radius);
- transition: background var(--transition), color var(--transition);
- display: flex; align-items: center; justify-content: center;
-}
-.action-btn:hover { background: var(--bg-hover); color: var(--text); }
-
-.send-btn { color: var(--accent); }
-.send-btn:hover { background: var(--accent-dim); color: var(--accent-hover); }
-.send-btn:disabled { opacity: 0.3; cursor: not-allowed; }
-
-.stop-btn { display: none; color: var(--warning); }
-.stop-btn.visible { display: flex; }
-
-/* ── Files ─────────────────────────── */
-
-.attach-btn { color: var(--text-3); }
-.attach-btn:hover { color: var(--text); }
-
-.file-strip {
- max-width: 768px; margin: 0 auto; padding: 6px 8px 2px;
- display: flex; gap: 6px; flex-wrap: wrap;
-}
-
-.att-chip {
- display: flex; align-items: center; gap: 6px;
- background: var(--bg-raised); border: 1px solid var(--border);
- border-radius: var(--radius); padding: 4px 6px;
- font-size: 12px; max-width: 220px; position: relative;
-}
-.att-chip.att-ready { border-color: color-mix(in srgb, var(--success) 40%, var(--border)); }
-.att-chip.att-error { border-color: color-mix(in srgb, var(--danger) 40%, var(--border)); }
-.att-chip.att-pending { border-color: color-mix(in srgb, var(--accent) 30%, var(--border)); }
-
-.att-thumb {
- width: 32px; height: 32px; object-fit: cover;
- border-radius: 3px; flex-shrink: 0;
-}
-.att-icon { font-size: 18px; flex-shrink: 0; line-height: 1; }
-
-.att-info { display: flex; flex-direction: column; min-width: 0; }
-.att-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text); font-weight: 500; }
-.att-meta { color: var(--text-3); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
-
-.att-remove {
- background: none; border: none; color: var(--text-3); cursor: pointer;
- padding: 2px 4px; font-size: 13px; line-height: 1; border-radius: 3px;
- flex-shrink: 0; margin-left: auto;
-}
-.att-remove:hover { background: var(--bg-hover); color: var(--text); }
-
-/* Drag-and-drop overlay on chat area */
-.messages.drag-over {
- outline: 2px dashed var(--accent);
- outline-offset: -4px;
- background: color-mix(in srgb, var(--accent) 4%, var(--bg));
-}
-
-/* ── Message Files ────────────────────── */
-
-.msg-files {
- display: flex; gap: 8px; flex-wrap: wrap;
- margin-top: 8px;
-}
-
-.msg-att-image {
- cursor: pointer; position: relative; border-radius: var(--radius);
- overflow: hidden; border: 1px solid var(--border);
- transition: border-color var(--transition);
-}
-.msg-att-image:hover { border-color: var(--accent); }
-.msg-att-image img {
- display: block; max-height: 280px; border-radius: var(--radius);
- background: var(--bg-raised);
-}
-.msg-att-image img.att-load-error {
- min-width: 120px; min-height: 60px;
- display: flex; align-items: center; justify-content: center;
- font-size: 13px; color: var(--text-3);
-}
-
-.att-vision-hint {
- position: absolute; top: 4px; right: 4px;
- background: color-mix(in srgb, var(--bg) 85%, transparent);
- border-radius: 4px; padding: 2px 5px; font-size: 14px;
- cursor: help;
-}
-
-.msg-att-doc {
- display: flex; align-items: center; gap: 8px;
- padding: 8px 12px; border: 1px solid var(--border);
- border-radius: var(--radius); background: var(--bg-raised);
- text-decoration: none; color: var(--text);
- max-width: 280px; transition: border-color var(--transition);
-}
-.msg-att-doc:hover { border-color: var(--accent); }
-.att-doc-icon { font-size: 20px; flex-shrink: 0; }
-.att-doc-info { display: flex; flex-direction: column; min-width: 0; }
-.att-doc-name {
- font-size: 13px; font-weight: 500;
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
-}
-.att-doc-size { font-size: 11px; color: var(--text-3); }
-.att-doc-dl { color: var(--text-3); font-size: 14px; margin-left: auto; }
-
-/* ── Image Lightbox ─────────────────────────── */
-
-.lightbox-overlay {
- display: none; position: fixed; inset: 0; z-index: 10000;
- background: rgba(0,0,0,0.85); align-items: center; justify-content: center;
-}
-.lightbox-overlay.active { display: flex; }
-.lightbox-overlay img {
- max-width: 92vw; max-height: 90vh; object-fit: contain;
- border-radius: 4px; box-shadow: 0 8px 32px rgba(0,0,0,0.5);
-}
-.lightbox-close {
- position: absolute; top: 16px; right: 20px;
- background: rgba(255,255,255,0.15); border: none; color: var(--text-on-color);
- font-size: 22px; width: 40px; height: 40px; border-radius: 50%;
- cursor: pointer; display: flex; align-items: center; justify-content: center;
- transition: background 0.15s;
-}
-.lightbox-close:hover { background: rgba(255,255,255,0.3); }
-
-/* ── Chat type indicators (v0.23.0) ────────── */
-.chat-type-icon { font-size: 11px; opacity: 0.7; }
-.mono-input { font-family: var(--mono) !important; font-size: 12px !important; }
-
-/* Handle hints in dropdowns and pickers */
-.item-handle { font-size: 10px; color: var(--accent); font-family: var(--mono); margin-left: 6px; opacity: 0.75; }
-.group-persona-handle { font-size: 10px; color: var(--accent); font-family: var(--mono); margin-left: 4px; opacity: 0.75; }
-
diff --git a/src/css/notifications.css b/src/css/notifications.css
deleted file mode 100644
index 61b45ed..0000000
--- a/src/css/notifications.css
+++ /dev/null
@@ -1,247 +0,0 @@
-/* ==========================================
- Notifications (v0.20.0)
- ========================================== */
-
-/* ── Bell + Badge ──────────────────────────── */
-
-.notif-wrap {
- position: relative;
- display: flex;
- align-items: center;
-}
-
-.notif-bell {
- background: none;
- border: none;
- cursor: pointer;
- padding: 5px;
- border-radius: 7px;
- color: var(--text-3);
- display: flex;
- align-items: center;
- position: relative;
- transition: background 0.15s, color 0.15s;
-}
-
-.notif-bell:hover {
- background: var(--bg-hover);
- color: var(--text);
-}
-
-.notif-badge {
- position: absolute;
- top: 0;
- right: 0;
- min-width: 16px;
- height: 16px;
- line-height: 16px;
- font-size: 10px;
- font-weight: 700;
- text-align: center;
- border-radius: 8px;
- background: var(--danger);
- color: #fff;
- padding: 0 4px;
- pointer-events: none;
-}
-
-/* ── Dropdown ──────────────────────────────── */
-
-.notif-dropdown {
- display: none;
- position: absolute;
- top: 100%;
- right: 0;
- width: 360px;
- max-height: 480px;
- background: var(--bg-surface);
- border: 1px solid var(--border);
- border-radius: 8px;
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
- z-index: 1000;
- overflow: hidden;
-}
-
-.notif-dd-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 10px 14px;
- border-bottom: 1px solid var(--border);
- font-weight: 600;
- font-size: 13px;
-}
-
-.notif-mark-all {
- background: none;
- border: none;
- color: var(--accent);
- cursor: pointer;
- font-size: 12px;
- padding: 2px 6px;
- border-radius: 4px;
-}
-
-.notif-mark-all:hover {
- background: var(--bg-hover);
-}
-
-.notif-dd-list {
- overflow-y: auto;
- max-height: 380px;
-}
-
-.notif-dd-footer {
- padding: 8px 14px;
- border-top: 1px solid var(--border);
- text-align: center;
-}
-
-.notif-view-all {
- background: none;
- border: none;
- color: var(--accent);
- cursor: pointer;
- font-size: 12px;
-}
-
-.notif-view-all:hover {
- text-decoration: underline;
-}
-
-/* ── Notification Item ─────────────────────── */
-
-.notif-item,
-.notif-panel-item {
- display: flex;
- align-items: flex-start;
- padding: 10px 14px;
- cursor: pointer;
- transition: background 0.15s;
- border-bottom: 1px solid var(--border);
-}
-
-.notif-item:hover,
-.notif-panel-item:hover {
- background: var(--bg-hover);
-}
-
-.notif-item.notif-unread,
-.notif-panel-item.notif-unread {
- background: var(--accent-dim);
-}
-
-.notif-dot {
- flex-shrink: 0;
- width: 18px;
- font-size: 10px;
- margin-top: 3px;
- color: var(--accent);
-}
-
-.notif-item:not(.notif-unread) .notif-dot,
-.notif-panel-item:not(.notif-unread) .notif-dot {
- color: var(--text-tertiary, #555);
-}
-
-.notif-body {
- flex: 1;
- min-width: 0;
-}
-
-.notif-title {
- font-size: 13px;
- line-height: 1.4;
- color: var(--text);
-}
-
-.notif-detail {
- font-size: 12px;
- color: var(--text-2);
- margin-top: 2px;
- line-height: 1.3;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.notif-time {
- font-size: 11px;
- color: var(--text-tertiary, #666);
- margin-top: 3px;
-}
-
-.notif-empty {
- padding: 32px 14px;
- text-align: center;
- color: var(--text-2);
- font-size: 13px;
-}
-
-/* ── Panel ─────────────────────────────────── */
-
-.notif-panel {
- height: 100%;
- display: flex;
- flex-direction: column;
-}
-
-.notif-panel-toolbar {
- display: flex;
- gap: 6px;
- padding: 8px 12px;
- border-bottom: 1px solid var(--border);
-}
-
-.notif-panel-list {
- flex: 1;
- overflow-y: auto;
- min-height: 0;
-}
-
-.notif-panel-row {
- display: flex;
- align-items: flex-start;
- width: 100%;
-}
-
-.notif-delete {
- flex-shrink: 0;
- background: none;
- border: none;
- color: var(--text-tertiary, #555);
- cursor: pointer;
- padding: 2px 6px;
- font-size: 12px;
- border-radius: 4px;
- opacity: 0;
- transition: opacity 0.15s;
-}
-
-.notif-panel-item:hover .notif-delete {
- opacity: 1;
-}
-
-.notif-delete:hover {
- color: var(--danger);
- background: var(--bg-hover);
-}
-
-.notif-panel-more {
- padding: 8px 12px;
- text-align: center;
- border-top: 1px solid var(--border);
-}
-
-/* ── Mobile ────────────────────────────────── */
-
-@media (max-width: 768px) {
- .notif-dropdown {
- position: fixed;
- top: 48px;
- left: 8px;
- right: 8px;
- width: auto;
- max-height: calc(100vh - 96px);
- }
-}
diff --git a/src/css/pane-container.css b/src/css/pane-container.css
deleted file mode 100644
index 30e98fc..0000000
--- a/src/css/pane-container.css
+++ /dev/null
@@ -1,167 +0,0 @@
-/* ==========================================
- Chat Switchboard — Pane Container
- ==========================================
- v0.25.0: Styles for the multi-pane layout system.
- Covers: pane container, leaf panes, tabbed panes,
- drag handles, and tab bars.
- ========================================== */
-
-/* ── Container ─────────────────────────────── */
-
-.pane-container {
- display: flex;
- flex-direction: column;
- height: 100%;
- overflow: hidden;
-}
-
-/* ── Split ─────────────────────────────────── */
-
-.pane-split--horizontal {
- display: flex;
- flex-direction: row;
- height: 100%;
- overflow: hidden;
-}
-
-.pane-split--vertical {
- display: flex;
- flex-direction: column;
- width: 100%;
- overflow: hidden;
-}
-
-/* ── Leaf Pane ─────────────────────────────── */
-
-.pane-leaf {
- display: flex;
- flex-direction: column;
- overflow: hidden;
- position: relative;
- box-sizing: border-box;
-}
-
-.pane-minimized {
- max-width: 0 !important;
- max-height: 0 !important;
- padding: 0 !important;
- border: none !important;
- overflow: hidden !important;
-}
-
-/* ── Tabbed Pane ───────────────────────────── */
-
-.pane-tabbed {
- display: flex;
- flex-direction: column;
- overflow: hidden;
- box-sizing: border-box;
-}
-
-/* Tab Bar */
-.pane-tab-bar {
- display: flex;
- align-items: center;
- gap: 0;
- background: var(--bg-secondary, #1a1a1e);
- border-bottom: 1px solid var(--border, #2a2a2e);
- flex-shrink: 0;
- height: 32px;
- padding: 0 4px;
- overflow-x: auto;
- overflow-y: hidden;
-}
-
-.pane-tab-bar::-webkit-scrollbar { height: 0; }
-
-.pane-tab-btn {
- background: none;
- border: none;
- color: var(--text-3, #777);
- font-size: 11px;
- font-weight: 600;
- font-family: inherit;
- padding: 6px 12px;
- cursor: pointer;
- border-bottom: 2px solid transparent;
- transition: color 0.15s, border-color 0.15s;
- white-space: nowrap;
- text-transform: uppercase;
- letter-spacing: 0.3px;
-}
-
-.pane-tab-btn:hover {
- color: var(--text, #eee);
-}
-
-.pane-tab-btn--active {
- color: var(--accent, #b38a4e);
- border-bottom-color: var(--accent, #b38a4e);
-}
-
-/* Tab Content */
-.pane-tab-content {
- flex: 1;
- min-height: 0;
- min-width: 0;
- overflow: hidden;
- position: relative;
-}
-
-.pane-tab-panel {
- position: absolute;
- inset: 0;
- overflow: hidden;
-}
-
-/* When panel is hidden (inactive tab), override absolute positioning */
-.pane-tab-panel[style*="display: none"] {
- position: absolute;
-}
-
-/* ── Drag Handle ───────────────────────────── */
-
-.pane-handle {
- flex-shrink: 0;
- background: var(--border, #2a2a2e);
- transition: background 0.15s;
- z-index: 10;
-}
-
-.pane-handle--horizontal {
- width: 4px;
- cursor: col-resize;
-}
-
-.pane-handle--vertical {
- height: 4px;
- cursor: row-resize;
-}
-
-.pane-handle:hover,
-.pane-handle--active {
- background: var(--accent, #b38a4e);
-}
-
-/* ── Responsive ────────────────────────────── */
-
-@media (max-width: 768px) {
- /* On mobile, splits collapse to vertical stack */
- .pane-split--horizontal {
- flex-direction: column;
- }
-
- .pane-handle--horizontal {
- width: 100%;
- height: 4px;
- cursor: row-resize;
- }
-
- /* Hide non-primary panes on mobile */
- .pane-split--horizontal > .pane:not(:first-child) {
- display: none;
- }
- .pane-split--horizontal > .pane-handle {
- display: none;
- }
-}
diff --git a/src/css/panels.css b/src/css/panels.css
deleted file mode 100644
index 45a4c2c..0000000
--- a/src/css/panels.css
+++ /dev/null
@@ -1,216 +0,0 @@
-/* ── panels.css ──────────────────────────────
- Side panel, preview, notes, graph, backlinks
- ──────────────────────────────────────────── */
-
-/* ── Side Panel (Preview + Notes) ──────── */
-
-/* ── Secondary Pane (was .side-panel, now .workspace-secondary) ── */
-/* Layout handled by .workspace-secondary above; these are internal styles */
-
-.side-panel-header {
- display: flex; align-items: center; justify-content: space-between;
- padding: 8px 12px; border-bottom: 1px solid var(--border);
- background: var(--bg-raised); flex-shrink: 0;
-}
-.side-panel-actions {
- display: flex; align-items: center; gap: 2px;
-}
-.side-panel-action-btn {
- background: none; border: none; color: var(--text-3);
- cursor: pointer; padding: 4px 6px; border-radius: var(--radius);
- transition: all var(--transition); display: flex; align-items: center;
-}
-.side-panel-action-btn:hover { background: var(--bg-hover); color: var(--text); }
-.side-panel-label {
- font-size: 12px; font-family: var(--font); font-weight: 600;
- color: var(--text-2); white-space: nowrap; font-style: normal;
-}
-.side-panel-btn {
- background: none; border: none; color: var(--text-3);
- cursor: pointer; padding: 4px 6px; border-radius: var(--radius);
- transition: all var(--transition); display: flex; align-items: center;
-}
-.side-panel-btn:hover { background: var(--bg-hover); color: var(--text); }
-.side-panel-close {
- background: none; border: none; color: var(--text-3);
- cursor: pointer; font-size: 16px; padding: 2px 6px;
- border-radius: var(--radius); transition: all var(--transition);
-}
-.side-panel-close:hover { background: var(--bg-hover); color: var(--text); }
-.side-panel-body {
- flex: 1; overflow-y: auto; min-height: 0;
-}
-.side-panel-page { height: 100%; display: flex; flex-direction: column; }
-/* .note-panel-root removed in v0.37.9 — see sw-notes-pane.css */
-.side-panel-empty {
- display: flex; align-items: center; justify-content: center;
- height: 100%; color: var(--text-3); font-size: 13px;
- padding: 2rem; text-align: center;
-}
-.preview-frame {
- flex: 1; width: 100%; border: none; background: var(--bg-surface);
-}
-.preview-empty {
- display: flex; align-items: center; justify-content: center;
- height: 100%; color: var(--text-3); font-size: 13px;
- padding: 2rem; text-align: center;
-}
-
-/* Notes panel CSS removed in v0.37.9 — see sw-notes-pane.css */
-
-/* Side panel overlay (mobile tap-to-close, mirrors sidebar-overlay) */
-.side-panel-overlay {
- display: none; position: fixed; inset: 0; z-index: 199;
- background: var(--overlay);
-}
-
-/* Mobile: full-width overlay */
-@media (max-width: 768px) {
- .workspace-secondary.open {
- position: fixed; top: 0; right: 0; bottom: 0;
- width: 100vw; min-width: 100vw; z-index: 200;
- }
- .workspace-handle { display: none !important; }
-
- /* Larger touch targets on mobile */
- .side-panel-close { font-size: 20px; padding: 4px 10px; }
- .side-panel-action-btn { padding: 6px 8px; }
- .side-panel-header { padding: 10px 12px; }
-}
-
-/* Dual-view removed in workspace refactor (v0.22.0) — workspace
- handles all pane placement via primary/secondary slots. */
-
-.msg-text ul, .msg-text ol { margin: 0.4em 0; padding-left: 1.5em; }
-.msg-text li { margin: 0.2em 0; }
-.msg-text li > p { margin: 0.2em 0; }
-
-.msg-text blockquote {
- border-left: 3px solid var(--accent); padding: 0.25em 0.75em; margin: 0.5em 0;
- color: var(--text-2); background: var(--accent-dim); border-radius: 0 var(--radius) var(--radius) 0;
-}
-
-.msg-text hr { border: none; border-top: 1px solid var(--border); margin: 0.75em 0; }
-
-.msg-text table { border-collapse: collapse; width: 100%; margin: 0.5em 0; font-size: 13px; }
-.msg-text th, .msg-text td { border: 1px solid var(--border); padding: 0.4em 0.7em; text-align: left; }
-.msg-text th { background: var(--bg-raised); font-weight: 600; }
-
-.msg-text h1, .msg-text h2, .msg-text h3, .msg-text h4 { margin: 0.75em 0 0.3em; font-weight: 600; }
-.msg-text h1 { font-size: 1.4em; }
-.msg-text h2 { font-size: 1.2em; }
-.msg-text h3 { font-size: 1.05em; }
-
-.msg-text a { color: var(--accent); }
-.msg-text img { max-width: 100%; border-radius: var(--radius); }
-
-/* Thinking blocks */
-.thinking-block {
- border: 1px solid var(--accent-dim); border-radius: var(--radius);
- margin: 0.5rem 0; background: rgba(108,159,255,0.03);
-}
-.thinking-block summary {
- padding: 0.4rem 0.75rem; cursor: pointer; font-size: 12px;
- color: var(--text-3); user-select: none;
- transition: color var(--transition);
-}
-.thinking-block summary:hover { color: var(--text-2); }
-.thinking-block[open] summary { border-bottom: 1px solid rgba(108,159,255,0.08); }
-.thinking-content {
- padding: 0.5rem 0.75rem; font-size: 12px; color: var(--text-3);
- max-height: 300px; overflow-y: auto; line-height: 1.6;
-}
-
-/* Tool activity indicators */
-.msg-tools {
- display: flex; flex-direction: column; gap: 4px;
- margin-bottom: 0.5rem;
-}
-.tool-activity {
- display: flex; align-items: center; gap: 6px;
- font-size: 12px; color: var(--text-3);
- background: var(--bg-raised); border: 1px solid var(--border);
- padding: 4px 10px; border-radius: var(--radius);
- animation: tool-fade-in 0.2s ease;
-}
-@keyframes tool-fade-in { from { opacity: 0; transform: translateY(-4px); } }
-.tool-icon { font-size: 13px; }
-.tool-name { font-family: var(--mono); font-size: 11px; color: var(--text-2); }
-.tool-status { font-size: 11px; margin-left: auto; }
-.tool-running { color: var(--warning); }
-.tool-running::after {
- content: ''; display: inline-block; width: 4px; height: 4px;
- border-radius: 50%; background: var(--warning); margin-left: 4px;
- animation: tool-pulse 1s infinite;
-}
-@keyframes tool-pulse { 0%,100% { opacity: 0.4; } 50% { opacity: 1; } }
-.tool-done { color: var(--success); }
-.tool-error { color: var(--danger); }
-.tool-hint {
- font-size: 11px; color: var(--text-3); margin-left: 6px;
- max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
-}
-.tool-note-link {
- background: none; border: none; color: var(--accent); cursor: pointer;
- font-size: 11px; margin-left: 6px; padding: 0;
- text-decoration: none;
- transition: color var(--transition);
-}
-.tool-note-link:hover { color: var(--text); text-decoration: underline; }
-
-/* Tool calls in message history (collapsed) */
-.tool-call-block {
- border: 1px solid var(--border); border-radius: var(--radius);
- background: var(--bg-raised); font-size: 12px;
-}
-.tool-call-summary {
- display: flex; align-items: center; gap: 6px;
- padding: 4px 10px; cursor: pointer; user-select: none;
- color: var(--text-3); list-style: none;
- transition: color var(--transition);
-}
-.tool-call-summary::-webkit-details-marker { display: none; }
-.tool-call-summary::before {
- content: '▸'; font-size: 10px; color: var(--text-3);
- transition: transform 0.15s ease;
-}
-.tool-call-block[open] .tool-call-summary::before { transform: rotate(90deg); }
-.tool-call-summary:hover { color: var(--text-2); }
-.tool-detail {
- border-top: 1px solid var(--border); padding: 6px 10px;
- display: flex; flex-direction: column; gap: 6px;
-}
-.tool-detail-label {
- font-size: 10px; color: var(--text-3); text-transform: uppercase;
- letter-spacing: 0.5px; font-weight: 600;
-}
-.tool-detail-pre {
- margin: 2px 0 0; padding: 6px 8px;
- background: var(--bg-surface); border-radius: 4px;
- font-size: 11px; color: var(--text-2); white-space: pre-wrap;
- word-break: break-all; max-height: 200px; overflow-y: auto;
-}
-
-/* Empty state */
-.empty-state {
- display: flex; flex-direction: column; align-items: center;
- justify-content: center; height: 100%; color: var(--text-3); gap: 0.25rem;
-}
-.empty-logo { font-size: 3rem; margin-bottom: 0.5rem; }
-.empty-logo-img { width: 64px; height: 64px; object-fit: contain; }
-.empty-state h2 { font-size: 1.25rem; font-weight: 600; color: var(--text-2); }
-.empty-state p { font-size: 0.85rem; }
-
-/* Typing indicator */
-.typing-dots { display: flex; gap: 4px; padding: 0.5rem 0; }
-.typing-dots span {
- width: 6px; height: 6px; border-radius: 50%; background: var(--text-3);
- animation: dot-pulse 1.2s infinite;
-}
-.typing-dots span:nth-child(2) { animation-delay: 0.2s; }
-.typing-dots span:nth-child(3) { animation-delay: 0.4s; }
-@keyframes dot-pulse { 0%,60%,100% { opacity: 0.4; } 30% { opacity: 1; } }
-
-
-/* ── Old notes CSS removed in v0.37.9 — see sw-notes-pane.css ── */
-
diff --git a/src/css/sw-chat-pane.css b/src/css/sw-chat-pane.css
index ff834a3..7e3d911 100644
--- a/src/css/sw-chat-pane.css
+++ b/src/css/sw-chat-pane.css
@@ -377,3 +377,376 @@
opacity: 0.3;
cursor: not-allowed;
}
+
+/* ── Stop Button ──────────────────────────── */
+
+.sw-msg-input__stop {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ background: none;
+ border: none;
+ border-radius: 6px;
+ color: var(--danger, #f44336);
+ cursor: pointer;
+ transition: background 0.15s;
+}
+
+.sw-msg-input__stop:hover {
+ background: rgba(244, 67, 54, 0.1);
+}
+
+/* ── Input Buttons Row ────────────────────── */
+
+.sw-msg-input__buttons {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ flex-shrink: 0;
+}
+
+/* ── Emoji Button + Picker ────────────────── */
+
+.sw-msg-input__emoji-wrap {
+ position: relative;
+}
+
+.sw-msg-input__emoji-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ background: none;
+ border: none;
+ border-radius: 6px;
+ color: var(--text-3, #555);
+ cursor: pointer;
+ transition: color 0.15s;
+}
+
+.sw-msg-input__emoji-btn:hover {
+ color: var(--text-2, #999);
+}
+
+.sw-emoji-picker {
+ position: absolute;
+ bottom: 40px;
+ right: 0;
+ width: 320px;
+ max-height: 360px;
+ background: var(--bg-secondary, #151517);
+ border: 1px solid var(--border, #2a2a2e);
+ border-radius: 10px;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.3);
+ z-index: 100;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.sw-emoji-picker__tabs {
+ display: flex;
+ gap: 0;
+ border-bottom: 1px solid var(--border, #2a2a2e);
+ padding: 4px 4px 0;
+ flex-shrink: 0;
+}
+
+.sw-emoji-picker__tab {
+ flex: 1;
+ background: none;
+ border: none;
+ border-bottom: 2px solid transparent;
+ padding: 6px 4px;
+ font-size: 16px;
+ cursor: pointer;
+ transition: border-color 0.15s;
+ line-height: 1;
+}
+
+.sw-emoji-picker__tab--active {
+ border-bottom-color: var(--accent, #b38a4e);
+}
+
+.sw-emoji-picker__search {
+ margin: 6px 8px;
+ padding: 6px 10px;
+ background: var(--bg, #0e0e10);
+ border: 1px solid var(--border, #2a2a2e);
+ border-radius: 6px;
+ color: var(--text, #eee);
+ font-size: 12px;
+ font-family: inherit;
+ outline: none;
+ flex-shrink: 0;
+}
+
+.sw-emoji-picker__search:focus {
+ border-color: var(--accent, #b38a4e);
+}
+
+.sw-emoji-picker__grid {
+ display: grid;
+ grid-template-columns: repeat(8, 1fr);
+ gap: 2px;
+ padding: 6px 8px;
+ overflow-y: auto;
+ flex: 1;
+ min-height: 0;
+}
+
+.sw-emoji-picker__emoji {
+ width: 100%;
+ aspect-ratio: 1;
+ background: none;
+ border: none;
+ border-radius: 6px;
+ font-size: 20px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.1s;
+ line-height: 1;
+ padding: 0;
+}
+
+.sw-emoji-picker__emoji:hover {
+ background: var(--bg, #0e0e10);
+}
+
+.sw-emoji-picker__empty {
+ grid-column: 1 / -1;
+ text-align: center;
+ padding: 20px;
+ color: var(--text-3, #555);
+ font-size: 12px;
+}
+
+/* ── Markdown Toolbar ─────────────────────── */
+
+.sw-md-toolbar {
+ display: flex;
+ gap: 1px;
+ padding: 4px 12px 2px;
+ flex-shrink: 0;
+}
+
+.sw-md-toolbar__btn {
+ background: none;
+ border: none;
+ color: var(--text-3, #555);
+ font-size: 11px;
+ font-weight: 600;
+ padding: 3px 7px;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: color 0.15s, background 0.15s;
+ line-height: 1;
+}
+
+.sw-md-toolbar__btn:hover {
+ color: var(--text, #eee);
+ background: var(--bg, #0e0e10);
+}
+
+/* ── Error Banner ─────────────────────────── */
+
+.sw-chat-pane__error {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ background: rgba(244, 67, 54, 0.1);
+ border-bottom: 1px solid rgba(244, 67, 54, 0.2);
+ color: var(--danger, #f44336);
+ font-size: 12px;
+ flex-shrink: 0;
+}
+
+.sw-chat-pane__error-text {
+ flex: 1;
+ min-width: 0;
+}
+
+.sw-chat-pane__error-dismiss {
+ background: none;
+ border: none;
+ color: var(--danger, #f44336);
+ font-size: 16px;
+ cursor: pointer;
+ padding: 0 4px;
+ line-height: 1;
+ opacity: 0.7;
+}
+
+.sw-chat-pane__error-dismiss:hover {
+ opacity: 1;
+}
+
+/* ── Load More ────────────────────────────── */
+
+.sw-chat-pane__load-more {
+ text-align: center;
+ padding: 8px;
+}
+
+.sw-chat-pane__load-more-btn {
+ background: none;
+ border: 1px solid var(--border, #2a2a2e);
+ border-radius: 6px;
+ color: var(--text-2, #999);
+ font-size: 12px;
+ padding: 6px 16px;
+ cursor: pointer;
+ transition: border-color 0.15s, color 0.15s;
+}
+
+.sw-chat-pane__load-more-btn:hover {
+ border-color: var(--accent, #b38a4e);
+ color: var(--text, #eee);
+}
+
+.sw-chat-pane__load-more-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* ── Message Footer (time + actions) ──────── */
+
+.sw-chat-msg__footer {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-top: 4px;
+ opacity: 0;
+ transition: opacity 0.15s;
+}
+
+.sw-chat-msg:hover .sw-chat-msg__footer {
+ opacity: 1;
+}
+
+.sw-chat-msg__time {
+ font-size: 10px;
+ color: var(--text-3, #555);
+}
+
+/* ── Message Actions Toolbar ──────────────── */
+
+.sw-chat-msg__actions {
+ display: flex;
+ gap: 2px;
+}
+
+.sw-chat-msg__action-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ background: none;
+ border: none;
+ border-radius: 4px;
+ color: var(--text-3, #555);
+ cursor: pointer;
+ transition: color 0.15s, background 0.15s;
+ padding: 0;
+}
+
+.sw-chat-msg__action-btn:hover {
+ color: var(--text, #eee);
+ background: var(--bg-secondary, #151517);
+}
+
+.sw-chat-msg__action-btn--danger:hover {
+ color: var(--danger, #f44336);
+}
+
+/* ── Code Block ───────────────────────────── */
+
+.sw-code-block {
+ position: relative;
+ margin: 8px 0;
+ border: 1px solid var(--border, #2a2a2e);
+ border-radius: 6px;
+ overflow: hidden;
+}
+
+.sw-code-block__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 4px 8px;
+ background: var(--bg, #0e0e10);
+ border-bottom: 1px solid var(--border, #2a2a2e);
+ min-height: 28px;
+}
+
+.sw-code-block__lang {
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--text-3, #555);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ font-family: var(--mono, 'SF Mono', monospace);
+}
+
+.sw-code-block__copy {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ background: none;
+ border: none;
+ color: var(--text-3, #555);
+ font-size: 11px;
+ cursor: pointer;
+ padding: 2px 6px;
+ border-radius: 3px;
+ transition: color 0.15s, background 0.15s;
+ margin-left: auto;
+}
+
+.sw-code-block__copy:hover {
+ color: var(--text, #eee);
+ background: var(--bg-secondary, #151517);
+}
+
+.sw-code-block__copy-text {
+ font-family: inherit;
+}
+
+.sw-code-block__pre {
+ background: var(--bg-secondary, #151517);
+ padding: 10px 12px;
+ overflow-x: auto;
+ margin: 0;
+ font-size: 12px;
+ line-height: 1.5;
+ font-family: var(--mono, 'SF Mono', monospace);
+}
+
+.sw-code-block__pre code {
+ background: none;
+ padding: 0;
+ border-radius: 0;
+ font-size: inherit;
+ font-family: inherit;
+}
+
+/* ── Task List Checkboxes ─────────────────── */
+
+.sw-task-item {
+ list-style: none;
+ margin-left: -1.5em;
+}
+
+.sw-task-checkbox {
+ margin-right: 6px;
+ vertical-align: middle;
+ accent-color: var(--accent, #b38a4e);
+}
diff --git a/src/css/sw-chat-surface.css b/src/css/sw-chat-surface.css
new file mode 100644
index 0000000..366a35e
--- /dev/null
+++ b/src/css/sw-chat-surface.css
@@ -0,0 +1,608 @@
+/* ==========================================
+ Chat Switchboard — Chat Surface CSS
+ ==========================================
+ v0.37.10: New Preact chat surface layout.
+ sw-chat-surface__ prefixed, no legacy conflicts.
+ ========================================== */
+
+/* ── Root Layout ──────────────────────────── */
+
+.sw-chat-surface {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ flex: 1 1 0%;
+ overflow: hidden;
+ background: var(--bg, #0e0e10);
+}
+
+/* ── Sidebar ──────────────────────────────── */
+
+.sw-chat-surface__sidebar {
+ width: 260px;
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ background: var(--bg-secondary, #151517);
+ border-right: 1px solid var(--border, #2a2a2e);
+ overflow: hidden;
+ z-index: 20;
+}
+
+.sw-chat-surface__sidebar-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 10px 12px 6px;
+ flex-shrink: 0;
+}
+
+.sw-chat-surface__new-chat-btn {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 1;
+ padding: 8px 12px;
+ background: var(--accent-dim, rgba(179, 138, 78, 0.1));
+ border: 1px solid var(--accent, #b38a4e);
+ border-radius: 8px;
+ color: var(--accent, #b38a4e);
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.15s;
+}
+
+.sw-chat-surface__collapse-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ background: none;
+ border: none;
+ border-radius: 6px;
+ color: var(--text-3, #555);
+ cursor: pointer;
+ flex-shrink: 0;
+ transition: color 0.15s, background 0.15s;
+}
+
+.sw-chat-surface__collapse-btn:hover {
+ color: var(--text, #eee);
+ background: var(--bg, #0e0e10);
+}
+
+.sw-chat-surface__new-chat-btn:hover {
+ background: rgba(179, 138, 78, 0.2);
+}
+
+/* ── Search ───────────────────────────────── */
+
+.sw-chat-surface__search-wrap {
+ position: relative;
+ padding: 4px 12px 8px;
+ flex-shrink: 0;
+}
+
+.sw-chat-surface__search-icon {
+ position: absolute;
+ left: 20px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--text-3, #555);
+ pointer-events: none;
+ display: flex;
+}
+
+.sw-chat-surface__search {
+ width: 100%;
+ padding: 6px 8px 6px 30px;
+ background: var(--bg, #0e0e10);
+ border: 1px solid var(--border, #2a2a2e);
+ border-radius: 6px;
+ color: var(--text, #eee);
+ font-size: 12px;
+ font-family: inherit;
+ outline: none;
+ box-sizing: border-box;
+}
+
+.sw-chat-surface__search:focus {
+ border-color: var(--accent, #b38a4e);
+}
+
+/* ── Sidebar Body (scrollable) ────────────── */
+
+.sw-chat-surface__sidebar-body {
+ flex: 1;
+ overflow-y: auto;
+ min-height: 0;
+ padding-bottom: 8px;
+}
+
+/* ── Section ──────────────────────────────── */
+
+.sw-chat-surface__section {
+ margin-top: 4px;
+}
+
+.sw-chat-surface__section-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ width: 100%;
+ padding: 6px 12px;
+ background: none;
+ border: none;
+ color: var(--text-2, #999);
+ font-size: 11px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ cursor: pointer;
+ transition: color 0.15s;
+}
+
+.sw-chat-surface__section-header:hover {
+ color: var(--text, #eee);
+}
+
+.sw-chat-surface__chevron {
+ display: flex;
+ transition: transform 0.15s;
+}
+
+.sw-chat-surface__chevron--open {
+ transform: rotate(90deg);
+}
+
+.sw-chat-surface__section-count {
+ margin-left: auto;
+ font-size: 10px;
+ color: var(--text-3, #555);
+ font-weight: 400;
+}
+
+.sw-chat-surface__section-body {
+ padding: 0 4px;
+}
+
+.sw-chat-surface__empty {
+ padding: 8px 16px;
+ font-size: 12px;
+ color: var(--text-3, #555);
+}
+
+/* ── Sidebar Item ─────────────────────────── */
+
+.sw-chat-surface__item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ width: 100%;
+ padding: 6px 12px;
+ background: none;
+ border: none;
+ border-radius: 6px;
+ color: var(--text-2, #999);
+ font-size: 13px;
+ cursor: pointer;
+ text-align: left;
+ transition: background 0.1s, color 0.1s;
+ position: relative;
+}
+
+.sw-chat-surface__item:hover {
+ background: var(--bg, #0e0e10);
+ color: var(--text, #eee);
+}
+
+.sw-chat-surface__item--active {
+ background: var(--accent-dim, rgba(179, 138, 78, 0.1));
+ color: var(--text, #eee);
+}
+
+.sw-chat-surface__item--indent {
+ padding-left: 28px;
+}
+
+.sw-chat-surface__item-icon {
+ display: flex;
+ flex-shrink: 0;
+ color: var(--text-3, #555);
+}
+
+.sw-chat-surface__item-title {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.sw-chat-surface__item-menu {
+ display: none;
+ align-items: center;
+ justify-content: center;
+ width: 22px;
+ height: 22px;
+ background: none;
+ border: none;
+ border-radius: 4px;
+ color: var(--text-3, #555);
+ cursor: pointer;
+ flex-shrink: 0;
+ padding: 0;
+}
+
+.sw-chat-surface__item:hover .sw-chat-surface__item-menu {
+ display: flex;
+}
+
+.sw-chat-surface__item-menu:hover {
+ color: var(--text, #eee);
+ background: var(--bg-secondary, #151517);
+}
+
+/* ── Unread Badge ─────────────────────────── */
+
+.sw-chat-surface__unread {
+ background: var(--accent, #b38a4e);
+ color: var(--bg, #0e0e10);
+ font-size: 10px;
+ font-weight: 700;
+ padding: 1px 6px;
+ border-radius: 10px;
+ flex-shrink: 0;
+}
+
+/* ── Folder ───────────────────────────────── */
+
+.sw-chat-surface__folder {
+ margin: 2px 0;
+}
+
+.sw-chat-surface__folder-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 12px;
+ color: var(--text-3, #555);
+ font-size: 11px;
+}
+
+.sw-chat-surface__folder-name {
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-weight: 600;
+}
+
+.sw-chat-surface__folder-count {
+ font-size: 10px;
+ color: var(--text-3, #555);
+}
+
+/* ── Context Menu ─────────────────────────── */
+
+.sw-chat-surface__context-menu {
+ position: fixed;
+ z-index: 200;
+ background: var(--bg-secondary, #151517);
+ border: 1px solid var(--border, #2a2a2e);
+ border-radius: 8px;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.3);
+ padding: 4px;
+ min-width: 160px;
+}
+
+.sw-chat-surface__context-menu button {
+ display: block;
+ width: 100%;
+ padding: 6px 12px;
+ background: none;
+ border: none;
+ border-radius: 4px;
+ color: var(--text, #eee);
+ font-size: 12px;
+ cursor: pointer;
+ text-align: left;
+}
+
+.sw-chat-surface__context-menu button:hover {
+ background: var(--bg, #0e0e10);
+}
+
+.sw-chat-surface__ctx-danger {
+ color: var(--danger, #f44336) !important;
+}
+
+/* ── Sidebar Footer ───────────────────────── */
+
+.sw-chat-surface__sidebar-footer {
+ display: flex;
+ align-items: center;
+ padding: 8px 12px;
+ border-top: 1px solid var(--border, #2a2a2e);
+ flex-shrink: 0;
+ position: relative;
+}
+
+/* ── Main Area ────────────────────────────── */
+
+.sw-chat-surface__main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ position: relative;
+}
+
+/* ── Workspace Header ─────────────────────── */
+
+.sw-chat-surface__workspace-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 4px 10px;
+ border-bottom: 1px solid var(--border, #2a2a2e);
+ background: var(--bg-secondary, #151517);
+ flex-shrink: 0;
+ min-height: 40px;
+}
+
+.sw-chat-surface__workspace-header-left,
+.sw-chat-surface__workspace-header-right {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.sw-chat-surface__sidebar-toggle {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ background: none;
+ border: none;
+ border-radius: 6px;
+ color: var(--text-3, #555);
+ cursor: pointer;
+ transition: color 0.15s, background 0.15s;
+}
+
+.sw-chat-surface__sidebar-toggle:hover {
+ color: var(--text, #eee);
+ background: var(--bg, #0e0e10);
+}
+
+/* ── Tools Button ─────────────────────────── */
+
+.sw-chat-surface__tools-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ background: none;
+ border: none;
+ border-radius: 6px;
+ color: var(--text-3, #555);
+ cursor: pointer;
+ position: relative;
+ transition: color 0.15s;
+}
+
+.sw-chat-surface__tools-btn:hover {
+ color: var(--text, #eee);
+}
+
+.sw-chat-surface__tools-badge {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ background: var(--danger, #f44336);
+ color: #fff;
+ font-size: 9px;
+ font-weight: 700;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ line-height: 1;
+}
+
+/* ── Workspace ────────────────────────────── */
+
+.sw-chat-surface__workspace {
+ flex: 1 1 0%;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ min-width: 0;
+ position: relative;
+ overflow: hidden;
+}
+
+.sw-chat-surface__chat-pane {
+ flex: 1 1 0%;
+ min-height: 0;
+ min-width: 0;
+}
+
+/* ── Sidebar Overlay (mobile) ─────────────── */
+
+.sw-chat-surface__sidebar-overlay {
+ display: none; /* Hidden on desktop */
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.5);
+ z-index: 15;
+}
+
+/* ── Tools Popup ──────────────────────────── */
+
+.sw-tools-popup {
+ position: absolute;
+ top: 44px;
+ right: 8px;
+ width: 300px;
+ max-height: 420px;
+ background: var(--bg-secondary, #151517);
+ border: 1px solid var(--border, #2a2a2e);
+ border-radius: 10px;
+ box-shadow: 0 8px 24px rgba(0,0,0,0.3);
+ z-index: 100;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.sw-tools-popup__header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 14px 8px;
+ border-bottom: 1px solid var(--border, #2a2a2e);
+ flex-shrink: 0;
+}
+
+.sw-tools-popup__title {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text, #eee);
+}
+
+.sw-tools-popup__badge {
+ font-size: 10px;
+ color: var(--danger, #f44336);
+ background: rgba(244, 67, 54, 0.1);
+ padding: 2px 8px;
+ border-radius: 10px;
+}
+
+.sw-tools-popup__body {
+ flex: 1;
+ overflow-y: auto;
+ min-height: 0;
+ padding: 8px;
+}
+
+.sw-tools-popup__empty {
+ padding: 16px;
+ text-align: center;
+ color: var(--text-3, #555);
+ font-size: 12px;
+}
+
+.sw-tools-popup__category {
+ margin-bottom: 4px;
+}
+
+.sw-tools-popup__cat-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.sw-tools-popup__cat-toggle {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ background: none;
+ border: none;
+ color: var(--text, #eee);
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ padding: 4px 6px;
+ border-radius: 4px;
+ text-align: left;
+}
+
+.sw-tools-popup__cat-toggle:hover {
+ background: var(--bg, #0e0e10);
+}
+
+.sw-tools-popup__chevron {
+ font-size: 8px;
+ transition: transform 0.15s;
+ display: inline-block;
+}
+
+.sw-tools-popup__chevron--open {
+ transform: rotate(90deg);
+}
+
+.sw-tools-popup__cat-count {
+ color: var(--text-3, #555);
+ font-weight: 400;
+ font-size: 10px;
+}
+
+.sw-tools-popup__cat-switch {
+ flex-shrink: 0;
+ cursor: pointer;
+}
+
+.sw-tools-popup__cat-switch input {
+ accent-color: var(--accent, #b38a4e);
+}
+
+.sw-tools-popup__tool {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ padding: 4px 6px 4px 22px;
+ font-size: 12px;
+ cursor: pointer;
+ border-radius: 4px;
+}
+
+.sw-tools-popup__tool:hover {
+ background: var(--bg, #0e0e10);
+}
+
+.sw-tools-popup__tool input {
+ margin-top: 2px;
+ accent-color: var(--accent, #b38a4e);
+}
+
+.sw-tools-popup__tool-name {
+ color: var(--text, #eee);
+ font-size: 12px;
+}
+
+.sw-tools-popup__tool-desc {
+ color: var(--text-3, #555);
+ font-size: 11px;
+ margin-left: auto;
+}
+
+/* ── Responsive: Mobile ───────────────────── */
+
+@media (max-width: 768px) {
+ .sw-chat-surface__sidebar {
+ position: fixed;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: 280px;
+ z-index: 20;
+ }
+
+ .sw-chat-surface__sidebar-overlay {
+ display: block;
+ }
+
+ .sw-chat-surface__collapse-btn {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/src/js/__tests__/extensions-builtin.test.js b/src/js/__tests__/extensions-builtin.test.js
index 86c7d53..2985f36 100644
--- a/src/js/__tests__/extensions-builtin.test.js
+++ b/src/js/__tests__/extensions-builtin.test.js
@@ -1,8 +1,15 @@
const { describe, it } = require('node:test');
const assert = require('node:assert');
-const { createBrowserContext, loadSource } = require('./helpers');
+const fs = require('fs');
+const path = require('path');
+const { createBrowserContext, loadSource, SRC } = require('./helpers');
const vm = require('vm');
+// v0.37.10: extensions.js deleted — extension system moves to Preact in v0.37.12.
+// Skip all tests if the source file doesn't exist.
+const EXTENSIONS_EXISTS = fs.existsSync(path.join(SRC, 'extensions.js'));
+const maybeDescribe = EXTENSIONS_EXISTS ? describe : describe.skip;
+
/**
* Load Events + Extensions into a browser context.
*/
@@ -23,7 +30,7 @@ function loadExtensions(overrides = {}) {
// KaTeX Renderer Extension
// ═══════════════════════════════════════════
-describe('KaTeX renderer extension', () => {
+maybeDescribe('KaTeX renderer extension', () => {
function loadWithKaTeX() {
const ctx = loadExtensions();
ctx.Extensions.register({
@@ -133,7 +140,7 @@ describe('KaTeX renderer extension', () => {
// CSV Table Extension
// ═══════════════════════════════════════════
-describe('CSV Table extension', () => {
+maybeDescribe('CSV Table extension', () => {
function loadWithCSV() {
const ctx = loadExtensions();
ctx.Extensions.register({
@@ -296,7 +303,7 @@ describe('CSV Table extension', () => {
// Diff Viewer Extension
// ═══════════════════════════════════════════
-describe('Diff Viewer extension', () => {
+maybeDescribe('Diff Viewer extension', () => {
function loadWithDiff() {
const ctx = loadExtensions();
ctx.Extensions.register({
@@ -427,7 +434,7 @@ describe('Diff Viewer extension', () => {
// JS Sandbox Extension (Tool)
// ═══════════════════════════════════════════
-describe('JS Sandbox extension', () => {
+maybeDescribe('JS Sandbox extension', () => {
it('registers js_eval tool handler', async () => {
const ctx = loadExtensions();
ctx.Extensions.register({
@@ -484,7 +491,7 @@ describe('JS Sandbox extension', () => {
// Regex Tester Extension (Tool)
// ═══════════════════════════════════════════
-describe('Regex Tester extension', () => {
+maybeDescribe('Regex Tester extension', () => {
function loadWithRegex() {
const ctx = loadExtensions();
ctx.Extensions.register({
diff --git a/src/js/__tests__/extensions.test.js b/src/js/__tests__/extensions.test.js
index 1ae7af7..9fdbc36 100644
--- a/src/js/__tests__/extensions.test.js
+++ b/src/js/__tests__/extensions.test.js
@@ -1,8 +1,15 @@
const { describe, it } = require('node:test');
const assert = require('node:assert');
-const { createBrowserContext, loadSource } = require('./helpers');
+const fs = require('fs');
+const path = require('path');
+const { createBrowserContext, loadSource, SRC } = require('./helpers');
const vm = require('vm');
+// v0.37.10: extensions.js deleted — extension system moves to Preact in v0.37.12.
+// Skip all tests if the source file doesn't exist.
+const EXTENSIONS_EXISTS = fs.existsSync(path.join(SRC, 'extensions.js'));
+const maybeDescribe = EXTENSIONS_EXISTS ? describe : describe.skip;
+
/**
* Load Events + Extensions into a browser context.
* Returns an object with Events, Extensions, and the sandbox.
@@ -25,7 +32,7 @@ function loadExtensions(overrides = {}) {
// ── Registration ─────────────────────────────
-describe('Extensions.register', () => {
+maybeDescribe('Extensions.register', () => {
it('registers an extension by id', () => {
const ctx = loadExtensions();
ctx.Extensions.register({ id: 'test-ext', init() {} });
@@ -48,7 +55,7 @@ describe('Extensions.register', () => {
// ── Lifecycle ────────────────────────────────
-describe('Extensions.initAll', () => {
+maybeDescribe('Extensions.initAll', () => {
it('calls init on registered extensions', async () => {
const ctx = loadExtensions();
let initialized = false;
@@ -84,7 +91,7 @@ describe('Extensions.initAll', () => {
});
});
-describe('Extensions.destroyAll', () => {
+maybeDescribe('Extensions.destroyAll', () => {
it('calls destroy and deactivates extensions', async () => {
const ctx = loadExtensions();
let destroyed = false;
@@ -102,7 +109,7 @@ describe('Extensions.destroyAll', () => {
// ── Context Object ──────────────────────────
-describe('Extension context', () => {
+maybeDescribe('Extension context', () => {
it('provides scoped events', async () => {
const ctx = loadExtensions();
let receivedPayload = null;
@@ -159,7 +166,7 @@ describe('Extension context', () => {
// ── Tool Registration ───────────────────────
-describe('ctx.tools.handle', () => {
+maybeDescribe('ctx.tools.handle', () => {
it('registers a tool handler', async () => {
const ctx = loadExtensions();
ctx.Extensions.register({
@@ -177,7 +184,7 @@ describe('ctx.tools.handle', () => {
// ── Tool Bridge ─────────────────────────────
-describe('Tool bridge', () => {
+maybeDescribe('Tool bridge', () => {
it('routes tool.call.* to registered handler and emits result', async () => {
const ctx = loadExtensions();
let resultEmitted = null;
@@ -269,7 +276,7 @@ describe('Tool bridge', () => {
// ── Renderer Pipeline ───────────────────────
-describe('Renderer pipeline', () => {
+maybeDescribe('Renderer pipeline', () => {
it('registers and runs block renderers', async () => {
const ctx = loadExtensions();
let rendered = false;
@@ -356,7 +363,7 @@ describe('Renderer pipeline', () => {
// ── Debug ────────────────────────────────────
-describe('Extensions.debug', () => {
+maybeDescribe('Extensions.debug', () => {
it('returns extension info and renderer count', async () => {
const ctx = loadExtensions();
ctx.Extensions.register({
@@ -376,7 +383,7 @@ describe('Extensions.debug', () => {
// ── Mermaid Extension ─────────────────────────────
-describe('Mermaid renderer extension', () => {
+maybeDescribe('Mermaid renderer extension', () => {
function loadWithMermaid() {
const ctx = loadExtensions();
// Load the mermaid extension script into the same VM context
diff --git a/src/js/__tests__/helpers.js b/src/js/__tests__/helpers.js
index 91cbdc1..b6c4ef8 100644
--- a/src/js/__tests__/helpers.js
+++ b/src/js/__tests__/helpers.js
@@ -4,6 +4,8 @@
// Loads the vanilla JS source files into a
// simulated browser environment so tests run
// against the ACTUAL frontend code.
+//
+// v0.37.10: Removed loadAppModules (api.js + app.js deleted).
// ==========================================
const fs = require('fs');
@@ -104,23 +106,24 @@ function createBrowserContext(overrides = {}) {
* Returns the sandbox for inspection.
*/
function loadSource(sandbox, filename) {
- const code = fs.readFileSync(path.join(SRC, filename), 'utf-8');
+ const filepath = path.join(SRC, filename);
+ if (!fs.existsSync(filepath)) {
+ throw new Error(`Source file not found: ${filename} (deleted in v0.37.10?)`);
+ }
+ const code = fs.readFileSync(filepath, 'utf-8');
const ctx = vm.createContext(sandbox);
vm.runInContext(code, ctx, { filename });
return sandbox;
}
/**
- * Loads API + App modules into a fresh context.
- * Returns { API, App, sandbox }.
+ * Safely read a source file, returning empty string if deleted.
+ * For tests that audit source code content.
*/
-function loadAppModules(overrides = {}) {
- const sandbox = createBrowserContext(overrides);
- loadSource(sandbox, 'sb.js');
- loadSource(sandbox, 'app-state.js');
- loadSource(sandbox, 'api.js');
- loadSource(sandbox, 'app.js');
- return { API: sandbox.API, App: sandbox.App, sandbox };
+function readSourceSafe(filename) {
+ const filepath = path.join(SRC, filename);
+ if (!fs.existsSync(filepath)) return '';
+ return fs.readFileSync(filepath, 'utf-8');
}
/**
@@ -158,7 +161,7 @@ function processModelsResponse(data, hiddenModels = new Set()) {
module.exports = {
createBrowserContext,
loadSource,
- loadAppModules,
+ readSourceSafe,
processModelsResponse,
SRC,
};
diff --git a/src/js/__tests__/policy-gating.test.js b/src/js/__tests__/policy-gating.test.js
index 72427a7..302f453 100644
--- a/src/js/__tests__/policy-gating.test.js
+++ b/src/js/__tests__/policy-gating.test.js
@@ -7,9 +7,12 @@
// policy exists but the frontend doesn't
// check it.
//
-// v0.22.5: Updated for server-rendered Go
-// templates. HTML is now in server/pages/
-// templates/ — not in src/index.html.
+// v0.22.5: Updated for server-rendered Go templates.
+// v0.37.5: Settings surface moved to Preact — legacy SPA tests
+// replaced with component source audits.
+// v0.37.10: Legacy SPA source audit removed (ui-core.js, app.js,
+// pages.js, settings-handlers.js, ui-admin.js all deleted).
+// Policy gating now verified via Preact surfaces + admin templates.
//
// Run: node --test src/js/__tests__/policy-gating.test.js
// ==========================================
@@ -36,94 +39,6 @@ function readAllTemplates() {
return files.join('\n');
}
-// ── Source code audits ───────────────────────
-// These tests read the actual source files and verify that required
-// wiring exists. If someone removes a policy check, CI breaks.
-
-describe('Policy wiring audit — source code', () => {
- // Read all UI files (ui-core.js + ui-settings.js + ui-admin.js replace old ui.js)
- const uiSrc = ['ui-core.js', 'ui-settings.js', 'ui-admin.js', 'ui-format.js']
- .map(f => fs.readFileSync(path.join(SRC, f), 'utf-8')).join('\n');
- // Read all app-side files (app.js + extracted handler files replace old monolith app.js)
- const appSrc = ['app.js', 'settings-handlers.js', 'admin-handlers.js', 'chat.js', 'tokens.js', 'notes.js']
- .map(f => fs.readFileSync(path.join(SRC, f), 'utf-8')).join('\n');
- // Pages.js — server-rendered page handlers (v0.22.5+)
- const pagesSrc = fs.readFileSync(path.join(SRC, 'pages.js'), 'utf-8');
- const templateSrc = readAllTemplates();
-
- // ── allow_user_byok ──
-
- it('UI has checkUserProvidersAllowed function', () => {
- assert.ok(uiSrc.includes('checkUserProvidersAllowed'),
- 'MISSING: checkUserProvidersAllowed — provider tab will show when policy is off');
- });
-
- it('checkUserProvidersAllowed reads App.policies.allow_user_byok', () => {
- assert.ok(uiSrc.includes('allow_user_byok'),
- 'MISSING: allow_user_byok check in UI');
- });
-
- // ── allow_user_personas ──
-
- it('UI has checkUserPersonasAllowed function', () => {
- assert.ok(uiSrc.includes('checkUserPersonasAllowed'),
- 'MISSING: checkUserPersonasAllowed — persona button will show when policy is off');
- });
-
- it('checkUserPersonasAllowed reads App.policies.allow_user_personas', () => {
- assert.ok(uiSrc.includes('allow_user_personas'),
- 'MISSING: allow_user_personas check in UI');
- });
-
- it('admin settings template has user-personas toggle', () => {
- assert.ok(templateSrc.includes('settUserPersonas'),
- 'MISSING: settUserPersonas toggle in admin settings template');
- });
-
- it('Pages.saveSettings writes allow_user_personas', () => {
- assert.ok(pagesSrc.includes('allow_user_personas'),
- 'MISSING: allow_user_personas in Pages.saveSettings');
- });
-
- it('admin settings save writes allow_user_personas (SPA bridge)', () => {
- assert.ok(appSrc.includes('allow_user_personas'),
- 'MISSING: handleSaveAdminSettings must write allow_user_personas');
- });
-
- // ── Settings live-apply ──
-
- it('admin save calls initBanners for policy refresh', () => {
- assert.ok(appSrc.includes('await initBanners()'),
- 'MISSING: initBanners call after admin save — policies won\'t refresh');
- });
-
- it('admin save calls checkUserProvidersAllowed', () => {
- // Find in handleSaveAdminSettings
- const saveFunc = appSrc.slice(appSrc.indexOf('handleSaveAdminSettings'));
- assert.ok(saveFunc.includes('checkUserProvidersAllowed'),
- 'MISSING: checkUserProvidersAllowed call after admin save');
- });
-
- it('admin save calls checkUserPersonasAllowed', () => {
- const saveFunc = appSrc.slice(appSrc.indexOf('handleSaveAdminSettings'));
- assert.ok(saveFunc.includes('checkUserPersonasAllowed'),
- 'MISSING: checkUserPersonasAllowed call after admin save');
- });
-
- it('admin save calls fetchModels to refresh model list', () => {
- const saveFunc = appSrc.slice(appSrc.indexOf('handleSaveAdminSettings'));
- assert.ok(saveFunc.includes('fetchModels'),
- 'MISSING: fetchModels call after admin save — model list stale after settings change');
- });
-
- // ── Models tab calls preset check ──
-
- it('models tab switch calls checkUserPersonasAllowed', () => {
- assert.ok(uiSrc.includes('checkUserPersonasAllowed'),
- 'MISSING: checkUserPersonasAllowed call on models tab switch');
- });
-});
-
// ── Policy evaluation logic ──────────────────
describe('Policy evaluation logic', () => {
@@ -158,7 +73,6 @@ describe('Policy evaluation logic', () => {
});
// ── Team member dropdown logic ───────────────
-// Bug: Empty dropdown because resp.data used instead of resp.users
describe('Team member dropdown population', () => {
function getAvailableUsers(usersResp, membersResp) {
@@ -221,72 +135,37 @@ describe('Team member dropdown population', () => {
// ── Admin settings field mapping ─────────────
// v0.22.5: Server-rendered admin settings template uses new element IDs.
-// Pages.saveSettings() in pages.js is the primary handler.
+// v0.37.10: pages.js deleted — admin settings save now handled by
+// Preact admin surface. Template elements still required.
describe('Admin settings field mapping (server templates)', () => {
- // New element IDs used by server-rendered admin/settings.html + pages.js
- const settingsFieldMap = {
- 'settRegEnabled': 'allow_registration',
- 'settRegDefaultState': 'default_user_active',
- 'settUserBYOK': 'allow_user_byok',
- 'settUserPersonas': 'allow_user_personas',
- 'settBannerEnabled': 'banner',
- };
-
- const pagesSrc = fs.readFileSync(path.join(SRC, 'pages.js'), 'utf-8');
const templateSrc = readAllTemplates();
- for (const [elementId, settingKey] of Object.entries(settingsFieldMap)) {
- it(`template has element #${elementId}`, () => {
- assert.ok(templateSrc.includes(`id="${elementId}"`),
- `MISSING: #${elementId} in server templates — admin settings incomplete`);
- });
-
- it(`Pages.saveSettings writes setting "${settingKey}"`, () => {
- assert.ok(pagesSrc.includes(settingKey),
- `MISSING: "${settingKey}" in Pages.saveSettings`);
- });
- }
-});
-
-// ── SPA bridge field mapping (backward compat) ──
-// The SPA chat surface still loads settings-handlers.js + ui-admin.js.
-// These use legacy element IDs for handleSaveAdminSettings().
-// Verified at the JS level (elements are SPA-modal DOM, not templates).
-
-describe('SPA bridge — admin settings handler references policy keys', () => {
- const appSrc = ['settings-handlers.js', 'admin-handlers.js']
- .map(f => fs.readFileSync(path.join(SRC, f), 'utf-8')).join('\n');
-
- const requiredPolicies = [
- 'allow_registration',
- 'default_user_active',
- 'allow_user_byok',
- 'allow_user_personas',
+ const requiredElements = [
+ 'settRegEnabled',
+ 'settRegDefaultState',
+ 'settUserBYOK',
+ 'settUserPersonas',
+ 'settBannerEnabled',
];
- for (const key of requiredPolicies) {
- it(`SPA bridge writes policy "${key}"`, () => {
- assert.ok(appSrc.includes(key),
- `MISSING: "${key}" in SPA bridge handler — policy not saved`);
+ for (const id of requiredElements) {
+ it(`template has element #${id}`, () => {
+ assert.ok(templateSrc.includes(`id="${id}"`),
+ `MISSING: #${id} in server templates — admin settings incomplete`);
});
}
});
-// ── HTML element existence checks ────────────
-// v0.22.5: Elements now live in server templates, not index.html.
-// v0.37.5: Settings surface elements moved to Preact components
-// (src/js/sw/surfaces/settings/). Only admin template elements
-// are checked here. Settings surface policy gating is handled by
-// the Preact SettingsSurface component (nav gating + section routing).
+// ── Critical HTML elements in templates ───────
describe('Critical HTML elements exist in server templates', () => {
const templateSrc = readAllTemplates();
const requiredElements = [
// Admin settings — policy toggles (still in Go templates)
- 'settUserBYOK', // Admin toggle for BYOK (was adminUserProvidersToggle)
- 'settUserPersonas', // Admin toggle for presets (was adminUserPresetsToggle)
+ 'settUserBYOK', // Admin toggle for BYOK
+ 'settUserPersonas', // Admin toggle for personas
];
for (const id of requiredElements) {
@@ -297,6 +176,34 @@ describe('Critical HTML elements exist in server templates', () => {
}
});
+// ── Chat surface template ────────────────────
+// v0.37.10: Chat surface is now Preact-rendered. Verify the mount
+// point exists and the old SPA scaffold is gone.
+
+describe('Chat surface template (v0.37.10)', () => {
+ const templateSrc = readAllTemplates();
+
+ it('chat-mount div exists', () => {
+ assert.ok(templateSrc.includes('id="chat-mount"'),
+ 'MISSING: #chat-mount — Preact chat surface cannot render');
+ });
+
+ it('old appContainer div is gone', () => {
+ assert.ok(!templateSrc.includes('id="appContainer"'),
+ 'STALE: #appContainer still in templates — old SPA scaffold not removed');
+ });
+
+ it('chat template loads Preact surface module', () => {
+ assert.ok(templateSrc.includes('sw/surfaces/chat/index.js'),
+ 'MISSING: chat surface module import in template');
+ });
+
+ it('chat template loads SDK boot', () => {
+ assert.ok(templateSrc.includes('sw/sdk/index.js'),
+ 'MISSING: SDK boot import in chat template');
+ });
+});
+
// ── Preact surface policy gating ─────────────
// v0.37.5: Settings surface elements are now rendered by Preact
// components. Verify the components handle policy gating.
@@ -326,20 +233,26 @@ describe('Settings Preact surface handles policy gating', () => {
});
});
-// ── SPA bridge — dynamic DOM elements ────────
-// adminMemberUser is created by ui-admin.js loadMemberUserDropdown()
-// which runs inside the SPA chat surface. Verify the JS function exists.
+// ── Admin Preact surface ─────────────────────
+// v0.37.6: Admin surface handles settings save via Preact.
+// Verify the admin surface has policy key handling.
-describe('SPA bridge — dynamic element creators', () => {
- const uiAdminSrc = fs.readFileSync(path.join(SRC, 'ui-admin.js'), 'utf-8');
+describe('Admin Preact surface handles settings', () => {
+ const SURFACES = path.join(SRC, 'sw', 'surfaces', 'admin');
+ const settingsPath = path.join(SURFACES, 'settings.js');
- it('ui-admin.js has loadMemberUserDropdown', () => {
- assert.ok(uiAdminSrc.includes('loadMemberUserDropdown'),
- 'MISSING: loadMemberUserDropdown — team member add will break');
- });
+ // Only test if the admin settings component exists
+ if (fs.existsSync(settingsPath)) {
+ const settingsSrc = fs.readFileSync(settingsPath, 'utf-8');
- it('loadMemberUserDropdown references adminMemberUser', () => {
- assert.ok(uiAdminSrc.includes('adminMemberUser'),
- 'MISSING: adminMemberUser reference in loadMemberUserDropdown');
- });
+ it('admin settings component references allow_user_byok', () => {
+ assert.ok(settingsSrc.includes('allow_user_byok'),
+ 'MISSING: allow_user_byok in admin settings component');
+ });
+
+ it('admin settings component references allow_user_personas', () => {
+ assert.ok(settingsSrc.includes('allow_user_personas'),
+ 'MISSING: allow_user_personas in admin settings component');
+ });
+ }
});
diff --git a/src/js/api.js b/src/js/api.js
deleted file mode 100644
index 6c0d04a..0000000
--- a/src/js/api.js
+++ /dev/null
@@ -1,996 +0,0 @@
-// ==========================================
-// Chat Switchboard – API Client
-// ==========================================
-// Backend-only mode. Handles auth tokens and
-// all HTTP calls. No offline fallback.
-//
-// BASE_PATH: injected via index.html