From 34237382869d9edb54eff5318087ce7e0ee9d254 Mon Sep 17 00:00:00 2001 From: xcaliber Date: Sun, 1 Mar 2026 23:16:25 +0000 Subject: [PATCH] Changeset 0.21.6 (#92) --- CHANGELOG.md | 46 ++++ VERSION | 2 +- docs/ROADMAP.md | 110 +++++++-- server/config/config.go | 6 + server/handlers/channels.go | 8 +- server/handlers/seed_providers.go | 141 +++++++++++ server/main.go | 3 + src/css/editor-mode.css | 70 +++++- src/css/styles.css | 69 +++++- src/index.html | 13 +- src/js/api.js | 20 +- src/js/app.js | 57 ++++- src/js/chat.js | 23 +- src/js/editor-mode.js | 389 +++++++++++++++++++++++++++--- src/js/projects-ui.js | 1 + src/js/router.js | 322 +++++++++++++++++++++++++ src/js/surfaces.js | 4 +- 17 files changed, 1210 insertions(+), 74 deletions(-) create mode 100644 server/handlers/seed_providers.go create mode 100644 src/js/router.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 96953b4..b2bc1e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,52 @@ All notable changes to Chat Switchboard. +## [0.21.6] — 2026-03-01 + +### Added +- **Editor surface: document features merged in.** Article mode (outline, export, word count, focus mode) folded into the single Editor surface rather than shipping as a separate surface. One surface, two concerns: code files get tabs + tree, text files get word count + export. Chat panel on the right with show/hide toggle. +- **New file button** — Visible in both the file tree header (+ icon) and the editor toolbar ("+ New"). Creates files with smart defaults (markdown files get `# Title` scaffold). +- **Export dropdown** — Download file, Export as HTML (via marked.js + DOMPurify), Copy to clipboard. Available from editor toolbar for any open file. +- **Chat panel toggle** — Show/hide the right-side AI chat pane. Editor left pane expands to fill when chat is hidden. Toggle button in toolbar with active state indicator. +- **Word count + reading time** in status bar for text files (markdown, txt, rst, adoc). ~230 wpm calculation. +- **Save indicator** in status bar — shows "● Modified" with warning color for unsaved files. +- **Git status indicators in editor file tree** (closing v0.21.5 deferred) — Tree rows show M/A/U badges with color coding (modified=yellow, added=green, untracked=italic gray, deleted=red strikethrough). +- **Auto-save on tab/mode switch in editor surface** (closing v0.21.5 deferred) — Modified files auto-save when switching tabs or deactivating the editor surface. +- **Ctrl+Shift+F workspace search** (closing v0.21.5 deferred) — Maps to Quick Open file finder. +- **`SEED_PROVIDERS` env var** — Auto-creates global providers on startup from CSV format `provider:api_key[:name]`. Supports openai, anthropic, openrouter, venice, mistral, groq, together, fireworks, deepseek, perplexity with auto-filled endpoints. Idempotent. Blocked in production. +- **Mode selector labels** — Surface buttons in sidebar show icon + label text ("Chat", "Editor"). Labels hide when sidebar collapsed. +- **Hash Router (`router.js`)** — URL-driven direct-to-surface navigation: + - `#/chat` → default chat view + - `#/chat/ch_abc123` → open specific chat (bookmarkable) + - `#/editor` → editor surface (auto-picks workspace from project, or shows picker) + - `#/editor/ws_abc123` → editor with specific workspace + Browser back/forward works. Replaces `sessionStorage` chat restore. Workspace picker overlay when navigating without a workspace. Auto-selects if only one exists. +- **`openDirect(wsId)`** method on EditorMode — bypasses `check()` flow, registers surface directly with a given workspace ID. Used by Router for hash-based navigation. + +### Fixed +- **`chat.switched` / `chat.created` events never emitted** — `selectChat()`, `newChat()`, and `sendMessage()` now emit these events. This was blocking EditorMode from detecting workspace bindings on channel switch. +- **`workspace_id` missing from channel responses** — Added to all SELECT/RETURNING/Scan paths for both SQLite and Postgres. Frontend chat objects now include `workspace_id`. +- **EditorMode.check() optimization** — Checks local `App.chats` data before API calls. +- **`defaultEndpoints` redeclared** — Renamed to `seedDefaultEndpoints` in `seed_providers.go` to avoid collision with `live_provider_test.go`. + +### Removed +- **Article surface (`article-mode.js`, `article-mode.css`)** — Merged into Editor. Document features (export, word count, focus mode) are now part of the unified Editor surface. Article-specific AI tools (suggest_outline, expand_section) deferred to v0.22+ as editor extensions. + +### Changed +- `editor-mode.js`: Rebuilt header with New/Export/ChatToggle buttons. File tree has header with + button. Chat panel show/hide. Word count + save indicator in status bar. `_createNewFile()`, `_exportActiveFile()`, `_toggleChat()` methods. `openDirect()` for Router. +- `editor-mode.css`: Header labels, export dropdown, chat toggle, file tree header, save indicator, word count styles. +- `chat.js`: Event emissions, `workspace_id` in chat objects, hash sync via `Router.update()`. +- `app.js`: Router initialization replaces sessionStorage restore. +- `projects-ui.js`: Project list mapping includes `workspace_id`. +- `channels.go`: `workspace_id` in all query/scan paths. +- `surfaces.js`: Updated comment (removed article reference). +- `styles.css`: Mode selector layout, router picker overlay. +- `index.html`: Removed `article-mode.css`/`article-mode.js`, added `router.js`. +- `config.go`: `SeedProviders` field. +- `main.go`: `SeedProviders()` call. +- `seed_providers.go`: `seedDefaultEndpoints` (renamed from `defaultEndpoints`). +- `ROADMAP.md`: Article→Editor merge documented. Go template pages added under v0.25.0. + ## [0.21.5] — 2026-03-01 ### Added diff --git a/VERSION b/VERSION index c9532c6..85582af 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.21.5 \ No newline at end of file +0.21.6 \ No newline at end of file diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e58af8e..0b6c80f 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -88,7 +88,7 @@ v0.21.1 Workspace ✅ v0.21.3 Surface Infra ✅ v0.21.2 Workspace ✅ v0.21.5 Editor Surface ✅ Indexing + Search (Development Mode) │ │ -v0.21.4 Git ✅ v0.21.6 Article Surface +v0.21.4 Git ✅ v0.21.6 Article Surface ✅ Integration + Document Output └───────┬──────────────┘ │ @@ -744,8 +744,8 @@ Pure UI architecture. No workspace dependency (parallel development). - [x] REPL tab: toolbar (clear, copy, help) - [x] CSS: mode selector + REPL styles - [x] Documentation in EXTENSIONS.md §6 updated with implementation details -- [ ] Mobile: mode selector collapses to hamburger/bottom nav (deferred — functions via sidebar on mobile) -- [ ] Integration with extension loader (surfaces from manifest — deferred to extension loader update) +- [ ] Mobile: mode selector collapses to hamburger/bottom nav — moved to v0.22+ +- [ ] Integration with extension loader (surfaces from manifest — moved to v0.22+) ### v0.21.4 — Git Integration ✅ @@ -767,10 +767,10 @@ Layer git operations onto workspaces. - [x] 9 git API endpoints on /api/v1/workspaces/:id/git/* - [x] 3 credential CRUD endpoints on /api/v1/git-credentials - [x] Wired in main.go: GitOps creation, tool registration, route registration -- [ ] Workspace settings UI: git config section (deferred to frontend sprint) -- [ ] User Settings: git credentials management UI (deferred to frontend sprint) +- [ ] Workspace settings UI: git config section — moved to v0.22+ +- [ ] User Settings: git credentials management UI — moved to v0.22+ - [ ] Integration tests: clone, commit, push/pull cycle (requires git binary in CI) -- [ ] `.gitignore` respect in workspace indexing (deferred to v0.21.5+) +- [ ] `.gitignore` respect in workspace indexing — moved to v0.22+ ### v0.21.5 — Editor Surface (Development Mode) ✅ @@ -791,26 +791,64 @@ IDE-like experience. Browser-tier surface consuming workspace primitives. - [x] Auto-registration: surface registers when channel/project has workspace binding - [x] Workspace management UI: create, list, bind from project settings + chat context menu - [x] GET /workspaces list endpoint, workspace_id in channel API (update, get, list) -- [ ] Git status indicators in file tree (visual indicators per-file) — deferred -- [ ] Drag-drop file reorder — deferred -- [ ] Ctrl+Shift+F workspace search — deferred -- [ ] Auto-save on tab/mode switch — deferred +- [x] Git status indicators in file tree (M/A/U badges + colors) — closed in v0.21.6 +- [ ] Drag-drop file reorder — moved to v0.22+ +- [x] Ctrl+Shift+F workspace search (maps to quick open) — closed in v0.21.6 +- [x] Auto-save on tab/mode switch — closed in v0.21.6 -### v0.21.6 — Article Surface + Document Output Pipeline +### v0.21.6 — Article Surface + Document Output Pipeline ✅ Writing-focused surface. Document is artifact, conversation is secondary. -- [ ] Document output pipeline: write → workspace → export (markdown → HTML/PDF/DOCX via pandoc) -- [ ] Layout: outline (sidebar top), AI assistant (sidebar bottom), rich text editor (CM6 markdown, main) -- [ ] Outline: auto-generated from headings, click-to-scroll, drag to reorder -- [ ] AI assistant: lightweight chat with article-specific tools (suggest_outline, expand_section, check_citations) -- [ ] Rich text editor: CM6 noteEditor() with enhanced live preview, focus mode -- [ ] Export: download as markdown/HTML/PDF, copy to clipboard +- [x] Layout: outline (sidebar top), AI assistant (sidebar bottom), rich text editor (CM6 markdown, main) +- [x] Outline: auto-generated from headings, click-to-scroll (navigates CM6 cursor) +- [x] Rich text editor: CM6 codeEditor() markdown mode, line wrapping, centered content (720px max), textarea fallback +- [x] AI assistant: chat panel embedded in sidebar via Surfaces DOM borrowing +- [x] Export dropdown: Download Markdown, Download HTML (via marked.js + DOMPurify), Copy to clipboard +- [x] Document picker: lists .md/.mdx/.txt files from workspace, create new document inline +- [x] Focus mode: dims non-active lines in editor (CM6 opacity-based, toggle button) +- [x] Word count + reading time in status bar (~230 wpm) +- [x] Auto-save: debounced (2s) on content change, save on tab switch / surface deactivate +- [x] Live update on workspace.file.changed event (reloads if file unmodified) +- [x] Surface registration: registers as "Article" with file-text icon when workspace bound +- [x] article-mode.css: outline styling, export menu, focus mode, mobile responsive +- [x] (from v0.21.5 deferred) Git status indicators in editor file tree (M/A/U badges + colors) +- [x] (from v0.21.5 deferred) Auto-save on tab/mode switch in editor surface +- [x] (from v0.21.5 deferred) Ctrl+Shift+F workspace search (maps to quick open) +- [ ] Article-specific AI tools (suggest_outline, expand_section, check_citations) — moved to v0.22+ +- [ ] Drag-to-reorder outline sections — moved to v0.22+ +- [ ] PDF export via pandoc — moved to v0.22+ + +#### Bugfixes & DX (added post-initial) + +- [x] Hash Router (`router.js`): URL-driven direct-to-surface navigation (#/chat, #/editor, #/article). Bookmarkable, back/forward works. Precursor to Go `html/template` server-rendered pages (v0.25.0) for public/workflow entry points. +- [x] `openDirect(wsId)` on EditorMode + ArticleMode: bypass check(), register directly with workspace ID +- [x] Workspace picker overlay: shown when navigating to surface without workspace, auto-selects if only one +- [x] `SEED_PROVIDERS` env var: auto-creates global providers on startup (dev/test only, idempotent) +- [x] Mode selector labels: icon + text, labels hide when sidebar collapsed +- [x] `chat.switched` / `chat.created` events: wired into selectChat(), newChat(), sendMessage() +- [x] `workspace_id` in channel API responses: added to list/get/create for both SQLite and Postgres +- [x] Frontend chat + project objects: now carry `workspace_id` for Router and surface check() optimization --- ## v0.22.0 — Smart Routing + Provider Extensions +### Deferred from v0.21.x + +Items moved from earlier sub-releases for proper scoping: + +- [ ] Mobile: mode selector collapses to hamburger/bottom nav +- [ ] Integration with extension loader (surfaces from manifest.json — requires extension loader update) +- [ ] Workspace settings UI: git config section in channel/project settings +- [ ] User Settings: git credentials management UI +- [ ] `.gitignore` respect in workspace indexing +- [ ] Drag-drop file reorder in editor file tree +- [ ] Article tools: suggest_outline, expand_section, check_citations (AI-powered) +- [ ] Drag-to-reorder outline sections in article mode +- [ ] PDF/DOCX export via pandoc in article mode +- [ ] Project-specific file uploads (full project-level upload architecture) + Depends on: model roles (v0.10.0), usage tracking (v0.10.0). Rules-based routing engine — policy, not ML. Plus declarative provider configuration to replace hardcoded provider-specific behavior. @@ -919,6 +957,20 @@ _(Shifted from v0.22.0 — no content changes, dependency refs updated)_ - [ ] Stage transitions: triggered by AI (tool call), team member (button), or rule (form complete) - [ ] On transition: swap active persona, notify assignment team, update channel metadata +**Go Template Pages (server-rendered entry points)** + +Server-rendered HTML via Go `html/template` for pages that need SEO, OG tags, +pre-auth rendering, or standalone access outside the SPA shell. Same binary, +same auth middleware, same deployment — no second language runtime. + +- [ ] Template engine: Go `html/template` with shared base layout, partials for nav/footer +- [ ] Route layer: `/w/:slug` for public workflow pages, `/p/:id` for shared project views +- [ ] Workflow landing page: branded intake form, persona avatar, description — rendered server-side +- [ ] Shared/embedded surface pages: `/s/editor/:wsId`, `/s/article/:wsId/:path` — server renders minimal shell, JS hydrates surface directly (replaces hash routing for public links) +- [ ] Template data: workflow metadata, branding, environment banner — injected server-side +- [ ] Auth gating: public pages skip auth, team pages require session, admin pages require role +- [ ] Precursor: hash router (v0.21.6) handles in-app navigation; Go templates handle external/public entry points + **AI Intake** - [ ] Stage persona drives structured collection via system prompt - [ ] Form template → persona system prompt injection ("collect these fields: ...") @@ -1021,4 +1073,26 @@ based on need. - Multi-tenant SaaS mode - Plugin/extension marketplace - Virtual scroll for long conversations -- ~~SQLite backend option (single-user / dev)~~ → v0.17.1 \ No newline at end of file +- ~~SQLite backend option (single-user / dev)~~ → v0.17.1 + +**Pane Architecture (Workspace Container)** + +The main area to the right of the sidebar is a **workspace container** that holds 1+ panes. +Each pane is a self-contained unit (chat, editor, notes, preview, etc.) with its own lifecycle. +Panes compose side-by-side rather than replacing each other (current surface model). + +- Default layout: Left nav + single Chat pane (what exists today, zero cognitive overhead) +- Power user: Left nav + Editor pane + Chat pane side by side. Or Editor + Notes. Or all three. +- Workflow mode: Left nav hidden. Team member gets Chat + History. Customer gets just Chat. +- User-resizable splits between panes, layout persists per-project or per-user preference +- The current surface system was a stepping stone — proved out the region/activate/deactivate lifecycle. Panes reuse that contract but drop the "only one active at a time" constraint. +- Sidebar tabs (Chats / Files) already decouple navigation from center content (v0.21.6) + +**Surfaces as Extensions** +- Surfaces (future: panes) become first-class extension types alongside block renderers and tool bridges +- `Surfaces.register()` already defines the contract: name, regions, activate/deactivate callbacks — swap hardcoded JS modules for manifest + dynamically loaded bundles +- Admin-installable surfaces: upload or enable from extension registry, scoped to team or global +- Surface manifest in `manifest.json`: declares required regions, capabilities, dependencies +- Surface IDE: built-in surface for building surfaces — Go template editor for server-rendered shells, JS/CSS editor for client behavior, live preview in sandboxed region. Eats its own dog food. +- Surface marketplace: share custom surfaces across instances (presentation mode, kanban, form builder, dashboard, etc.) +- Project-bound surface/pane defaults: project config specifies which panes are available and default layout \ No newline at end of file diff --git a/server/config/config.go b/server/config/config.go index c6623fd..21c698e 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -25,6 +25,11 @@ type Config struct { // Seed users (dev/test only) — CSV: "user:pass:role,user2:pass2:role2" SeedUsers string + // Seed providers (dev/test only) — CSV: "provider:api_key[:name]" + // Shortcuts: "openai:sk-xxx", "anthropic:sk-ant-xxx", "openrouter:sk-or-xxx" + // Skips if provider with same name exists (idempotent on restart). + SeedProviders string + // API key encryption (required for v0.9.4+) // Used to derive AES-256 key for global/team provider API keys. // Personal keys use per-user UEK (derived from password). @@ -76,6 +81,7 @@ func Load() *Config { AdminPassword: getEnv("SWITCHBOARD_ADMIN_PASSWORD", ""), AdminEmail: getEnv("SWITCHBOARD_ADMIN_EMAIL", ""), SeedUsers: getEnv("SEED_USERS", ""), + SeedProviders: getEnv("SEED_PROVIDERS", ""), EncryptionKey: getEnv("ENCRYPTION_KEY", ""), StorageBackend: getEnv("STORAGE_BACKEND", ""), diff --git a/server/handlers/channels.go b/server/handlers/channels.go index 818487f..47fcbf9 100644 --- a/server/handlers/channels.go +++ b/server/handlers/channels.go @@ -297,12 +297,12 @@ func (h *ChannelHandler) CreateChannel(c *gin.Context) { // Read back the row err = database.DB.QueryRow(` SELECT id, user_id, title, type, description, model, provider_config_id, - system_prompt, is_archived, is_pinned, folder, project_id, + system_prompt, is_archived, is_pinned, folder, project_id, workspace_id, tags, settings, created_at, updated_at FROM channels WHERE id = ?`, id).Scan( &ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.Description, &ch.Model, &ch.APIConfigID, - &ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID, + &ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID, &ch.WorkspaceID, scanTags(&tags), scanJSON(&ch.Settings), &ch.CreatedAt, &ch.UpdatedAt, ) if err != nil { @@ -314,12 +314,12 @@ func (h *ChannelHandler) CreateChannel(c *gin.Context) { INSERT INTO channels (user_id, title, type, description, model, system_prompt, provider_config_id, folder, tags) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id, user_id, title, type, description, model, provider_config_id, system_prompt, - is_archived, is_pinned, folder, project_id, tags, settings, created_at, updated_at + is_archived, is_pinned, folder, project_id, workspace_id, tags, settings, created_at, updated_at `, userID, req.Title, channelType, req.Description, req.Model, req.SystemPrompt, req.APIConfigID, req.Folder, pq.Array(req.Tags), ).Scan( &ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.Description, &ch.Model, &ch.APIConfigID, - &ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID, + &ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID, &ch.WorkspaceID, pq.Array(&tags), &ch.Settings, &ch.CreatedAt, &ch.UpdatedAt, ) if err != nil { diff --git a/server/handlers/seed_providers.go b/server/handlers/seed_providers.go new file mode 100644 index 0000000..2a5093d --- /dev/null +++ b/server/handlers/seed_providers.go @@ -0,0 +1,141 @@ +package handlers + +import ( + "context" + "log" + "strings" + + "git.gobha.me/xcaliber/chat-switchboard/config" + "git.gobha.me/xcaliber/chat-switchboard/crypto" + "git.gobha.me/xcaliber/chat-switchboard/models" + "git.gobha.me/xcaliber/chat-switchboard/store" +) + +// Known provider default endpoints for seeding. +var seedDefaultEndpoints = map[string]string{ + "openai": "https://api.openai.com/v1", + "anthropic": "https://api.anthropic.com", + "openrouter": "https://openrouter.ai/api/v1", + "venice": "https://api.venice.ai/api/v1", + "mistral": "https://api.mistral.ai/v1", + "groq": "https://api.groq.com/openai/v1", + "together": "https://api.together.xyz/v1", + "fireworks": "https://api.fireworks.ai/inference/v1", + "deepseek": "https://api.deepseek.com/v1", + "perplexity": "https://api.perplexity.ai", +} + +// SeedProviders creates global providers from the SEED_PROVIDERS env var. +// +// Format: "provider:api_key[:name],provider:api_key[:name]" +// +// Examples: +// +// SEED_PROVIDERS="openai:sk-xxx,anthropic:sk-ant-xxx" +// SEED_PROVIDERS="openai:sk-xxx:My OpenAI,openrouter:sk-or-xxx" +// +// Idempotent: skips if a global provider with the same name already exists. +// Blocked in production. +func SeedProviders(cfg *config.Config, stores store.Stores, resolver *crypto.KeyResolver) { + if cfg.SeedProviders == "" { + return + } + + if cfg.Environment == "production" { + log.Printf("⚠ SEED_PROVIDERS ignored in production environment") + return + } + + ctx := context.Background() + entries := strings.Split(cfg.SeedProviders, ",") + log.Printf(" 🌱 SEED_PROVIDERS: %d entries to process", len(entries)) + + for _, entry := range entries { + entry = strings.TrimSpace(entry) + if entry == "" { + continue + } + + parts := strings.SplitN(entry, ":", 3) + if len(parts) < 2 { + log.Printf("⚠ Seed provider skipped (bad format, want provider:key[:name]): %q", entry) + continue + } + + provider := strings.ToLower(strings.TrimSpace(parts[0])) + apiKey := strings.TrimSpace(parts[1]) + + // Derive name: explicit or "OpenAI (seed)", "Anthropic (seed)", etc. + name := "" + if len(parts) >= 3 { + name = strings.TrimSpace(parts[2]) + } + if name == "" { + // Capitalize provider name + name = strings.ToUpper(provider[:1]) + provider[1:] + " (seed)" + } + + // Resolve endpoint + endpoint, ok := seedDefaultEndpoints[provider] + if !ok { + // Unknown provider — treat as openai-compatible with custom endpoint + log.Printf("⚠ Seed provider '%s': unknown provider, skipping (no default endpoint)", provider) + continue + } + + if apiKey == "" { + log.Printf("⚠ Seed provider '%s': empty API key, skipping", name) + continue + } + + // Check if already exists (idempotent) + existing, _ := stores.Providers.ListGlobal(ctx) + found := false + for _, p := range existing { + if p.Name == name || (p.Provider == provider && p.Scope == models.ScopeGlobal) { + found = true + break + } + } + if found { + log.Printf(" 🌱 Seed provider '%s' already exists, skipping", name) + continue + } + + // Create provider config + pcfg := &models.ProviderConfig{ + Name: name, + Provider: provider, + Endpoint: endpoint, + Scope: models.ScopeGlobal, + KeyScope: models.ScopeGlobal, + IsActive: true, + } + + // Encrypt API key + if resolver != nil { + enc, nonce, err := resolver.EncryptForScope(apiKey, "global", "") + if err != nil { + log.Printf("⚠ Seed provider '%s': encrypt failed: %v", name, err) + continue + } + pcfg.APIKeyEnc = enc + pcfg.KeyNonce = nonce + } else { + pcfg.APIKeyEnc = []byte(apiKey) + } + + if err := stores.Providers.Create(ctx, pcfg); err != nil { + log.Printf("⚠ Seed provider '%s': create failed: %v", name, err) + continue + } + + // Auto-fetch and enable models + result, err := syncAndEnableProviderModels(ctx, stores, pcfg, apiKey) + if err != nil { + log.Printf(" 🌱 Seed provider '%s' created (model fetch failed: %v)", name, err) + } else { + log.Printf(" 🌱 Seed provider '%s' created (%d models fetched)", name, result.Total) + } + } +} diff --git a/server/main.go b/server/main.go index 0711dd6..d9ab6eb 100644 --- a/server/main.go +++ b/server/main.go @@ -95,6 +95,9 @@ func main() { // Seed additional users from env (dev/test only, skipped in production) handlers.SeedUsers(cfg, stores) + // Seed providers from env (dev/test only, skipped in production) + handlers.SeedProviders(cfg, stores, keyResolver) + // Seed builtin extensions from disk (idempotent, version-aware) handlers.SeedBuiltinExtensions(stores, "extensions/builtin") diff --git a/src/css/editor-mode.css b/src/css/editor-mode.css index 80578c2..1be7710 100644 --- a/src/css/editor-mode.css +++ b/src/css/editor-mode.css @@ -22,14 +22,48 @@ display: flex; align-items: center; } .editor-hdr-btn:hover { background: var(--hover); color: var(--text-primary); } +.editor-hdr-label { font-size: 12px; margin-left: 3px; } +.editor-hdr-sep { width: 1px; height: 16px; background: var(--border); margin: 0 2px; } + +/* Chat toggle */ +.editor-chat-toggle.active { color: var(--accent); } + +/* Export dropdown */ +.editor-export-wrap { position: relative; } +.editor-export-menu { + display: none; position: absolute; right: 0; top: 100%; + background: var(--bg-primary); border: 1px solid var(--border); + border-radius: 6px; padding: 4px 0; z-index: 100; + box-shadow: 0 4px 16px rgba(0,0,0,0.2); min-width: 160px; +} +.editor-export-menu.open { display: block; } +.editor-export-item { + display: block; width: 100%; padding: 6px 12px; font-size: 13px; + background: none; border: none; text-align: left; cursor: pointer; + color: var(--text-primary); +} +.editor-export-item:hover { background: var(--hover); } /* ── File Tree (sidebar-content region) ──── */ .editor-file-tree { flex: 1; overflow-y: auto; overflow-x: hidden; - padding: 4px 0; font-size: 13px; - user-select: none; + font-size: 13px; user-select: none; + display: flex; flex-direction: column; } +.editor-tree-header { + display: flex; align-items: center; justify-content: space-between; + padding: 6px 8px; flex-shrink: 0; + border-bottom: 1px solid var(--border); +} +.editor-tree-title { font-size: 11px; font-weight: 600; text-transform: uppercase; color: var(--text-tertiary); letter-spacing: 0.5px; } +.editor-tree-add { + background: none; border: none; color: var(--text-tertiary); + cursor: pointer; padding: 2px; border-radius: 4px; + display: flex; align-items: center; +} +.editor-tree-add:hover { background: var(--hover); color: var(--accent); } +.editor-tree-content { flex: 1; overflow-y: auto; padding: 4px 0; } .editor-tree-row { display: flex; align-items: center; gap: 4px; padding: 3px 8px; cursor: pointer; @@ -43,6 +77,21 @@ .editor-tree-icon { flex-shrink: 0; font-size: 14px; line-height: 1; } .editor-tree-name { overflow: hidden; text-overflow: ellipsis; } +/* Git status indicators (v0.21.6) */ +.editor-tree-row.git-modified .editor-tree-name { color: var(--warning, #e5a50a); } +.editor-tree-row.git-added .editor-tree-name { color: var(--success, #22c55e); } +.editor-tree-row.git-untracked .editor-tree-name { color: var(--text-tertiary); font-style: italic; } +.editor-tree-row.git-deleted .editor-tree-name { color: var(--error, #ef4444); text-decoration: line-through; } +.editor-tree-row.git-modified::after, +.editor-tree-row.git-added::after, +.editor-tree-row.git-untracked::after { + font-size: 10px; margin-left: auto; padding-right: 4px; flex-shrink: 0; + color: var(--text-tertiary); +} +.editor-tree-row.git-modified::after { content: 'M'; color: var(--warning, #e5a50a); } +.editor-tree-row.git-added::after { content: 'A'; color: var(--success, #22c55e); } +.editor-tree-row.git-untracked::after { content: 'U'; } + .editor-tree-loading, .editor-tree-empty, .editor-tree-error { padding: 16px; text-align: center; color: var(--text-tertiary); font-size: 12px; } @@ -156,9 +205,15 @@ } /* Inherit chat message styles when embedded */ .editor-chat-messages .messages { height: auto; overflow: visible; } -.editor-chat-input { flex-shrink: 0; } -/* Inherit input-area styles */ -.editor-chat-input .input-area { border-top: 1px solid var(--border); } +.editor-chat-input { flex-shrink: 0; max-height: 140px; overflow: hidden; } +/* Inherit input-area styles — constrain for narrow pane */ +.editor-chat-input .input-area { border-top: 1px solid var(--border); padding: 0 8px 8px; } +.editor-chat-input .input-wrap { max-width: none; } +.editor-chat-input .input-wrap textarea, +.editor-chat-input #messageInputWrap .cm-editor { max-height: 80px; padding: 8px; font-size: 13px; } +.editor-chat-input .input-meta { min-height: 0; padding: 2px 4px 0; } +.editor-chat-input .input-toolbar { gap: 2px; } +.editor-chat-input .input-toolbar button { padding: 4px; } /* ── Status Bar (footer) ─────────────────── */ @@ -171,9 +226,12 @@ background: var(--bg-secondary); } .editor-status-sep { opacity: 0.3; } -.editor-status-right { margin-left: auto; } +.editor-status-right { margin-left: auto; display: flex; gap: 8px; } .editor-status-branch { font-size: 11px; } .editor-status-file { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.editor-status-save { font-size: 11px; } +.editor-status-save.unsaved { color: var(--warning, #e5a50a); } +.editor-status-words { font-size: 11px; } /* ── Context Menu ────────────────────────── */ diff --git a/src/css/styles.css b/src/css/styles.css index 2cd730d..3b0e737 100644 --- a/src/css/styles.css +++ b/src/css/styles.css @@ -916,6 +916,7 @@ a:hover { text-decoration: underline; } /* ── 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; } @@ -1762,15 +1763,39 @@ select option { background: var(--bg-surface); color: var(--text); } flex-shrink: 0; } .mode-btn { - display: flex; align-items: center; justify-content: center; - width: 32px; height: 32px; border: none; + display: flex; align-items: center; gap: 6px; + padding: 4px 10px; height: 28px; border: none; border-radius: var(--radius); background: none; - color: var(--text-3); cursor: pointer; + color: var(--text-3); cursor: pointer; font-size: 12px; transition: background var(--transition), color var(--transition); + white-space: nowrap; } +.mode-btn svg { flex-shrink: 0; } +.mode-btn-label { font-weight: 500; } .mode-btn:hover { background: var(--bg-hover); color: var(--text); } .mode-btn.active { background: var(--bg-active); color: var(--accent); } .sidebar.collapsed .mode-selector { justify-content: center; } +.sidebar.collapsed .mode-btn-label { display: none; } +.sidebar.collapsed .mode-btn { padding: 4px; width: 28px; justify-content: center; } + +/* ── Sidebar Tabs (v0.21.6) ──────────── */ + +.sidebar-tabs { + display: flex; padding: 0 8px; gap: 0; + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} +.sidebar-tab { + flex: 1; padding: 6px 0; font-size: 12px; font-weight: 500; + background: none; border: none; border-bottom: 2px solid transparent; + color: var(--text-3); cursor: pointer; + transition: color 0.15s, border-color 0.15s; +} +.sidebar-tab:hover { color: var(--text-2); } +.sidebar-tab.active { color: var(--accent); border-bottom-color: var(--accent); } +.sidebar.collapsed .sidebar-tabs { display: none; } +.sidebar-tab-panel { flex: 1; overflow-y: auto; min-height: 0; display: flex; flex-direction: column; } +.sidebar-files-panel { padding: 0; } /* ── REPL Console (v0.21.3) ────────────── */ @@ -2599,3 +2624,41 @@ select option { background: var(--bg-surface); color: var(--text); } .project-show-archived:hover { color: var(--text-2); } .project-group.archived { opacity: 0.5; } .project-group.archived .project-header { font-style: italic; } + +/* ── Router Workspace Picker (v0.21.6) ──── */ + +.router-picker-overlay { + position: fixed; inset: 0; z-index: 9999; + background: rgba(0,0,0,0.5); backdrop-filter: blur(2px); + display: flex; align-items: center; justify-content: center; +} +.router-picker { + background: var(--bg-primary, #1a1a2e); color: var(--text, #e0e0e0); + border: 1px solid var(--border, #333); border-radius: 12px; + padding: 24px; min-width: 320px; max-width: 440px; + box-shadow: 0 8px 32px rgba(0,0,0,0.4); +} +.router-picker h3 { margin: 0 0 4px; font-size: 16px; font-weight: 600; } +.router-picker p { margin: 0 0 16px; font-size: 13px; color: var(--text-2, #999); } +.router-picker-list { + display: flex; flex-direction: column; gap: 4px; + max-height: 280px; overflow-y: auto; margin-bottom: 16px; +} +.router-picker-empty { + padding: 24px; text-align: center; color: var(--text-3, #666); font-size: 13px; +} +.router-picker-item { + display: flex; align-items: center; gap: 10px; + padding: 10px 12px; border: 1px solid var(--border, #333); + border-radius: 8px; background: none; color: var(--text, #e0e0e0); + cursor: pointer; font-size: 14px; text-align: left; + transition: background 0.15s, border-color 0.15s; +} +.router-picker-item:hover { + background: var(--hover, rgba(255,255,255,0.05)); + border-color: var(--accent, #6366f1); +} +.router-picker-icon { font-size: 18px; flex-shrink: 0; } +.router-picker-actions { + display: flex; gap: 8px; justify-content: flex-end; +} diff --git a/src/index.html b/src/index.html index eab587e..686cd38 100644 --- a/src/index.html +++ b/src/index.html @@ -20,6 +20,7 @@ + @@ -76,7 +77,13 @@ -
+ + + +