@polderlabs/bizar 5.5.1 → 5.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bizar-dash/dist/assets/{EnvVarsSection-BUQnyVQD.js → EnvVarsSection-DoIzqrlb.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-BUQnyVQD.js.map → EnvVarsSection-DoIzqrlb.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-8FupFgTv.js → MobileChat-CoUCUsbr.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-8FupFgTv.js.map → MobileChat-CoUCUsbr.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-BeO04g52.js +1 -0
- package/bizar-dash/dist/assets/{MobileSettings-BkXtglA6.js.map → MobileSettings-BeO04g52.js.map} +1 -1
- package/bizar-dash/dist/assets/{Toast-CUwkLRms.js → Toast-D9nv5N6m.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-CUwkLRms.js.map → Toast-D9nv5N6m.js.map} +1 -1
- package/bizar-dash/dist/assets/{icons-COpQr35l.js → icons-Btipv2pp.js} +65 -70
- package/bizar-dash/dist/assets/icons-Btipv2pp.js.map +1 -0
- package/bizar-dash/dist/assets/main-DHXPuddY.js +18 -0
- package/bizar-dash/dist/assets/main-DHXPuddY.js.map +1 -0
- package/bizar-dash/dist/assets/{main-Bz_zWFCw.css → main-UG4jKK-F.css} +1 -1
- package/bizar-dash/dist/assets/mobile-H5KH9cTL.js +1 -0
- package/bizar-dash/dist/assets/{mobile-B7dAkmYd.js.map → mobile-H5KH9cTL.js.map} +1 -1
- package/bizar-dash/dist/assets/{mobile-layout-ClC6Qa42.js → mobile-layout-BqHxVb6_.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-layout-ClC6Qa42.js.map → mobile-layout-BqHxVb6_.js.map} +1 -1
- package/bizar-dash/dist/assets/{useSlashCommands-B3MZ1JXa.js → useSlashCommands-C24-O1e7.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-B3MZ1JXa.js.map → useSlashCommands-C24-O1e7.js.map} +1 -1
- package/bizar-dash/dist/index.html +7 -7
- package/bizar-dash/dist/mobile.html +3 -3
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/memory-store.mjs +74 -28
- package/bizar-dash/src/server/routes/memory.mjs +91 -38
- package/bizar-dash/src/web/App.tsx +36 -43
- package/bizar-dash/src/web/components/SettingsNav.tsx +92 -0
- package/bizar-dash/src/web/components/Sidebar.tsx +26 -7
- package/bizar-dash/src/web/components/Topbar.tsx +11 -48
- package/bizar-dash/src/web/styles/main.css +18 -3
- package/bizar-dash/src/web/styles/settings.css +115 -3
- package/bizar-dash/src/web/views/Settings.tsx +184 -205
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +4 -4
- package/bizar-dash/tests/memory-namespace.test.mjs +2 -2
- package/bizar-dash/tests/memory-status-root.test.mjs +144 -0
- package/bizar-dash/tests/memory-store.test.mjs +5 -4
- package/bizar-dash/tests/memory-vault-config-endpoint.test.mjs +173 -0
- package/bizar-dash/tests/settings-layout.test.tsx +129 -0
- package/bizar-dash/tests/settings-mode-wiring.test.tsx +151 -0
- package/bizar-dash/tests/settings-nav.test.tsx +127 -0
- package/bizar-dash/tests/views/Memory.test.tsx +40 -1
- package/bizar-dash/tests/views/topbar.test.tsx +35 -50
- package/cli/memory.mjs +1 -1
- package/package.json +1 -1
- package/bizar-dash/dist/assets/MobileSettings-BkXtglA6.js +0 -1
- package/bizar-dash/dist/assets/icons-COpQr35l.js.map +0 -1
- package/bizar-dash/dist/assets/main-gqVwliHz.js +0 -18
- package/bizar-dash/dist/assets/main-gqVwliHz.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-B7dAkmYd.js +0 -1
- package/bizar-dash/tests/views/Settings.test.tsx +0 -160
package/bizar-dash/dist/assets/{useSlashCommands-B3MZ1JXa.js.map → useSlashCommands-C24-O1e7.js.map}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"mappings":"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","names":["VirtualList","items","renderItem","itemHeight","height","overscan","className","scrollTop","setScrollTop","useState","containerRef","useRef","visibleRange","useMemo","start","end","totalHeight","offsetY","visibleItems","jsx","e","item","i","ChatTopBar","activeProject","sessionCount","sessionsOpen","infoOpen","onToggleSessions","onToggleInfo","onOpenOverview","jsxs","Folder","ChevronDown","LoadingSkeleton","count","_","SuggestionCards","projectName","onPickSuggestion","suggestions","Code","BookOpen","Lightbulb","ImageIcon","s","WelcomeScreen","variant","displayName","MessageBubble","message","pinned","onCopy","onDelete","onTogglePin","onRegenerate","role","ts","formatTime","content","isUser","Copy","RefreshCw","Pin","Trash2","ReactMarkdown","remarkGfm","ChatThread","messages","loading","sessionId","activeSource","innerRef","useEffect","el","p","wrapped","stream","m","AgentChip","agent","setAgent","agents","open","setOpen","ref","handler","current","display","v","Bot","a","useAutoGrowTextarea","value","maxHeight","useLayoutEffect","next","Composer","model","setModel","text","setText","sending","onSend","attachments","setAttachments","onAttach","inputRef","fileInputRef","onKeyDown","base","onFiles","files","names","cur","_a","Paperclip","j","X","enhanced","enhancePrompt","Sparkles","Send","useChat","snapshot","settings","initialTaskId","bizarMessages","setBizarMessages","sessions","setSessions","setSessionId","setLoading","busy","setBusy","opencodeSessions","setOpencodeSessions","opencodeMessages","setOpencodeMessages","setActiveSource","activeOpencodeSessionId","setActiveOpencodeSessionId","opencodeError","setOpencodeError","opencodeSuggestion","setOpencodeSuggestion","opencodeErrorInfo","setOpencodeErrorInfo","sessionStates","setSessionStates","stickToBottom","setStickToBottom","newMessageCount","setNewMessageCount","seenOpencodeMessages","setSeenOpencodeMessages","setPinned","listRef","toastRef","opencodeEsRef","sseReconnectAttemptRef","sseReconnectTimerRef","sseCurrentSessionIdRef","sseAutoReconnectRef","appendOpencodeMessage","useCallback","msg","idx","markSeen","id","setSessionDisplayState","patch","prev","markSessionStreaming","streaming","setToast","toast","loadChat","sid","url","data","api","err","loadTaskChat","taskId","refreshSessions","refreshOpencodeSessions","enriched","openSseForSession","tok","connect","es","onEnvelope","evt","sidForState","payload","_b","_c","_d","parseData","raw","parsed","props","sessionID","messageID","type","inner","innerData","namedTypes","name","nextAttempt","delayMs","closeOpencodeSession","loadOpencodeSession","info","ApiError","d","code","canRetry","selectBizarSession","onCreateSession","b","created","opencodeErr","_e","trimmed","optimisticId","optimistic","response","messageId","deleteMessage","togglePin","copyMessage","handleScroll","atBottom","jumpToLatest","renameSession","title","deleteSession","n","closed","__vitePreload","Ws","BUILTIN_COMMANDS","useSlashCommands","query","setQuery","allCommands","q","c"],"ignoreList":[],"sources":["../../src/web/components/VirtualList.tsx","../../src/web/components/chat/ChatTopBar.tsx","../../src/web/components/chat/LoadingSkeleton.tsx","../../src/web/components/chat/SuggestionCards.tsx","../../src/web/components/chat/WelcomeScreen.tsx","../../src/web/components/chat/MessageBubble.tsx","../../src/web/components/chat/ChatThread.tsx","../../src/web/components/chat/AgentChip.tsx","../../src/web/components/chat/useAutoGrowTextarea.ts","../../src/web/components/chat/Composer.tsx","../../src/web/components/chat/useChat.ts","../../src/web/components/chat/useSlashCommands.ts"],"sourcesContent":["// src/web/components/VirtualList.tsx — hand-rolled virtual scrolling (no external deps).\nimport React, { useState, useRef, useEffect, useMemo } from 'react';\n\nexport function VirtualList<T>({\n items,\n renderItem,\n itemHeight = 80,\n height = 400,\n overscan = 5,\n className,\n}: {\n items: T[];\n renderItem: (item: T, index: number) => React.ReactNode;\n itemHeight?: number;\n height?: number;\n overscan?: number;\n className?: string;\n}) {\n const [scrollTop, setScrollTop] = useState(0);\n const containerRef = useRef<HTMLDivElement>(null);\n\n const visibleRange = useMemo(() => {\n const start = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);\n const end = Math.min(\n items.length,\n Math.ceil((scrollTop + height) / itemHeight) + overscan,\n );\n return { start, end };\n }, [scrollTop, itemHeight, height, overscan, items.length]);\n\n const totalHeight = items.length * itemHeight;\n const offsetY = visibleRange.start * itemHeight;\n const visibleItems = items.slice(visibleRange.start, visibleRange.end);\n\n return (\n <div\n ref={containerRef}\n onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}\n style={{ height, overflowY: 'auto' }}\n className={className}\n >\n <div style={{ height: totalHeight, position: 'relative' }}>\n <div style={{ transform: `translateY(${offsetY}px)` }}>\n {visibleItems.map((item, i) => (\n <div key={visibleRange.start + i} style={{ height: itemHeight }}>\n {renderItem(item, visibleRange.start + i)}\n </div>\n ))}\n </div>\n </div>\n </div>\n );\n}\n","// src/components/chat/ChatTopBar.tsx — 48px topbar with project chip + panel toggles.\n\nimport { Folder, ChevronDown } from 'lucide-react';\n\ninterface Props {\n activeProject: { name: string; path?: string } | null;\n sessionCount: number;\n sessionsOpen: boolean;\n infoOpen: boolean;\n onToggleSessions: () => void;\n onToggleInfo: () => void;\n onOpenOverview: () => void;\n}\n\nexport function ChatTopBar({\n activeProject,\n sessionCount,\n sessionsOpen,\n infoOpen,\n onToggleSessions,\n onToggleInfo,\n onOpenOverview,\n}: Props) {\n return (\n <header className=\"chat-topbar\">\n <button type=\"button\" className=\"chat-topbar-project\" onClick={onOpenOverview}>\n <Folder size={14} />\n <span className=\"chat-topbar-project-name\">{activeProject?.name ?? 'No project'}</span>\n <ChevronDown size={12} />\n </button>\n <div className=\"chat-topbar-spacer\" />\n <button\n type=\"button\"\n className={`chat-topbar-btn ${sessionsOpen ? 'active' : ''}`}\n onClick={onToggleSessions}\n aria-pressed={sessionsOpen}\n >\n Sessions ({sessionCount})\n </button>\n <button\n type=\"button\"\n className={`chat-topbar-btn ${infoOpen ? 'active' : ''}`}\n onClick={onToggleInfo}\n aria-pressed={infoOpen}\n >\n Info\n </button>\n </header>\n );\n}","// src/components/chat/LoadingSkeleton.tsx — pulsing skeleton bubbles while loading.\n\ninterface Props {\n count?: number;\n}\n\nexport function LoadingSkeleton({ count = 3 }: Props) {\n return (\n <div className=\"chat-loading-skeleton\" aria-label=\"Loading messages\">\n {Array.from({ length: count }).map((_, i) => (\n <div key={i} className=\"chat-loading-skeleton-bubble\">\n <div className=\"chat-loading-skeleton-meta\" />\n <div className=\"chat-loading-skeleton-line\" />\n <div className=\"chat-loading-skeleton-line short\" />\n </div>\n ))}\n </div>\n );\n}","// src/components/chat/SuggestionCards.tsx — 4 hardcoded suggestion cards for first-run.\n\nimport { Code, BookOpen, Lightbulb, Image as ImageIcon } from 'lucide-react';\n\ninterface Props {\n projectName: string;\n onPickSuggestion: (text: string) => void;\n}\n\nexport function SuggestionCards({ projectName, onPickSuggestion }: Props) {\n const suggestions = [\n {\n icon: <Code size={16} />,\n title: projectName ? `Explain ${projectName}` : 'Explain this project',\n preview: 'Give me a tour of the codebase, highlight the architecture, and call out any anti-patterns or hotspots.',\n prompt: projectName\n ? `Explain the ${projectName} project. Give me a tour of the codebase, highlight the architecture, and call out any anti-patterns or hotspots.`\n : 'Explain the current project. Give me a tour of the codebase, highlight the architecture, and call out any anti-patterns or hotspots.',\n },\n {\n icon: <BookOpen size={16} />,\n title: 'Summarize recent sessions',\n preview: 'Read my last 5 chat sessions and give me a 3-bullet summary of what I worked on and what got stuck.',\n prompt: 'Read my last 5 chat sessions and give me a 3-bullet summary of what I worked on and what got stuck.',\n },\n {\n icon: <Lightbulb size={16} />,\n title: 'Suggest a refactor',\n preview: 'Look for the largest, most painful files in this project and propose a concrete refactor for the worst one.',\n prompt: 'Look for the largest, most painful files in this project and propose a concrete refactor for the worst one.',\n },\n {\n icon: <ImageIcon size={16} />,\n title: 'Find visual regressions',\n preview: 'Open the dashboard, screenshot the Chat tab, and flag any layout problems or anti-patterns you see.',\n prompt: 'Open the dashboard, screenshot the Chat tab, and flag any layout problems or anti-patterns you see.',\n },\n ];\n\n return (\n <div className=\"chat-suggestions\">\n {suggestions.map((s, i) => (\n <button\n key={i}\n type=\"button\"\n className=\"chat-suggestion\"\n onClick={() => onPickSuggestion(s.prompt)}\n >\n <div className=\"chat-suggestion-title\">{s.icon}<span>{s.title}</span></div>\n <div className=\"chat-suggestion-preview\">{s.preview}</div>\n </button>\n ))}\n </div>\n );\n}","// src/components/chat/WelcomeScreen.tsx — gradient greeting + no-project state.\n\nimport { Folder } from 'lucide-react';\nimport { SuggestionCards } from './SuggestionCards';\n\ninterface Props {\n variant: 'empty' | 'no-project';\n projectName: string;\n onPickSuggestion: (text: string) => void;\n}\n\nexport function WelcomeScreen({ variant, projectName, onPickSuggestion }: Props) {\n if (variant === 'no-project') {\n return (\n <div className=\"chat-welcome\">\n <h1 className=\"chat-welcome-h1\">\n <span className=\"chat-greeting-hello\">No project selected.</span>\n </h1>\n <p className=\"chat-welcome-subtitle\">Pick a project in Overview to scope chat sessions.</p>\n </div>\n );\n }\n\n const displayName = projectName || 'there';\n\n return (\n <div className=\"chat-welcome\">\n <h1 className=\"chat-welcome-h1\">\n <span className=\"chat-greeting-hello\">Hello, </span>\n <span className=\"chat-greeting-name\">{displayName}.</span>\n </h1>\n <p className=\"chat-welcome-subtitle\">How can I help you today?</p>\n <SuggestionCards projectName={projectName} onPickSuggestion={onPickSuggestion} />\n </div>\n );\n}","// src/components/chat/MessageBubble.tsx — user/assistant message with hover actions.\n\nimport ReactMarkdown from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport { Copy, RefreshCw, Pin, Trash2 } from 'lucide-react';\nimport { formatTime } from '../../lib/utils';\n\ninterface Message {\n role?: string;\n content?: string;\n message?: string;\n agent?: string;\n ts?: string | number;\n}\n\ninterface Props {\n message: Message;\n pinned: boolean;\n onCopy: () => void;\n onDelete: () => void;\n onTogglePin: () => void;\n onRegenerate: () => void;\n}\n\nexport function MessageBubble({ message, pinned, onCopy, onDelete, onTogglePin, onRegenerate }: Props) {\n const role = (message.role || 'assistant').toLowerCase();\n const ts = message.ts ? formatTime(message.ts) : '';\n const content = message.content || message.message || '';\n const isUser = role === 'user';\n\n return (\n <article className={`chat-message ${isUser ? 'chat-message-user' : 'chat-message-assistant'} ${pinned ? 'chat-message-pinned' : ''}`}>\n <header className=\"chat-message-meta\">\n <span className=\"chat-message-role\">{role}</span>\n {message.agent && <span className=\"chat-message-agent\">@{message.agent}</span>}\n {ts && <time className=\"chat-message-time\">{ts}</time>}\n {pinned && <span className=\"chat-message-pin\">pinned</span>}\n <div className=\"chat-message-actions\">\n <button type=\"button\" className=\"chat-msg-action-btn\" onClick={onCopy} title=\"Copy\" aria-label=\"Copy\">\n <Copy size={12} />\n </button>\n {!isUser && (\n <button type=\"button\" className=\"chat-msg-action-btn\" onClick={onRegenerate} title=\"Regenerate\" aria-label=\"Regenerate\">\n <RefreshCw size={12} />\n </button>\n )}\n <button type=\"button\" className=\"chat-msg-action-btn\" onClick={onTogglePin} title=\"Pin\" aria-label=\"Pin\">\n <Pin size={12} />\n </button>\n <button type=\"button\" className=\"chat-msg-action-btn chat-msg-action-btn-danger\" onClick={onDelete} title=\"Delete\" aria-label=\"Delete\">\n <Trash2 size={12} />\n </button>\n </div>\n </header>\n <div className=\"chat-message-content markdown-body\">\n <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>\n </div>\n </article>\n );\n}","// src/components/chat/ChatThread.tsx — message list, welcome screen, loading skeleton.\n//\n// v4.2.5 — accepts `activeSource` so the loading skeleton can hint\n// which stream is loading and the welcome variant for \"opencode\"\n// can differ from the local chat welcome.\n\nimport { useEffect, useRef } from 'react';\nimport { LoadingSkeleton } from './LoadingSkeleton';\nimport { WelcomeScreen } from './WelcomeScreen';\nimport { MessageBubble } from './MessageBubble';\nimport { VirtualList } from '../VirtualList';\n\ninterface Message {\n role?: string;\n content?: string;\n message?: string;\n agent?: string;\n ts?: string | number;\n}\n\ninterface Props {\n messages: Message[];\n loading: boolean;\n activeProject: { name: string } | null;\n sessionId: string;\n pinned: Set<number>;\n /** Which stream currently owns the thread — purely cosmetic, lets\n * the empty/loading states be slightly more specific. The type\n * uses `unknown` so we don't pull in a tight coupling; the value\n * is reflected back into the loading skeleton and welcome screen\n * via data attributes so CSS can style accordingly. */\n activeSource?: 'bizar' | 'opencode' | null;\n onPickSuggestion: (text: string) => void;\n onCopy: (m: Message) => void;\n onDelete: (idx: number) => void;\n onTogglePin: (idx: number) => void;\n onRegenerate: (messageId: string) => void;\n}\n\nexport function ChatThread({\n messages,\n loading,\n activeProject,\n sessionId,\n pinned,\n activeSource,\n onPickSuggestion,\n onCopy,\n onDelete,\n onTogglePin,\n onRegenerate,\n}: Props) {\n const innerRef = useRef<HTMLDivElement>(null);\n\n // Only auto-scroll when we own the scroll container (i.e. when\n // rendered directly as the message scroller). When embedded\n // inside Chat.tsx's chat-thread-scroll, the parent handles scroll.\n useEffect(() => {\n const el = innerRef.current;\n if (!el) return;\n let p: HTMLElement | null = el.parentElement;\n let wrapped = false;\n while (p) {\n if (p.classList.contains('chat-thread-scroll')) {\n wrapped = true;\n break;\n }\n p = p.parentElement;\n }\n if (!wrapped) el.scrollTop = el.scrollHeight;\n }, [messages]);\n\n const stream: 'bizar' | 'opencode' = activeSource === 'opencode' ? 'opencode' : 'bizar';\n\n if (loading) {\n return (\n <div className=\"chat-thread legacy\" ref={innerRef} data-stream={stream}>\n <LoadingSkeleton count={3} />\n </div>\n );\n }\n\n if (!activeProject) {\n return (\n <div className=\"chat-thread legacy\" ref={innerRef} data-stream={stream}>\n <WelcomeScreen\n variant=\"no-project\"\n projectName=\"\"\n onPickSuggestion={onPickSuggestion}\n />\n </div>\n );\n }\n\n if (messages.length === 0) {\n return (\n <div className=\"chat-thread legacy\" ref={innerRef} data-stream={stream}>\n <WelcomeScreen\n variant=\"empty\"\n projectName={activeProject.name}\n onPickSuggestion={onPickSuggestion}\n />\n </div>\n );\n }\n\n return (\n <div className=\"chat-thread legacy\" ref={innerRef}>\n <VirtualList\n items={messages}\n itemHeight={120}\n height={800}\n className=\"chat-thread-virtual\"\n renderItem={(m, i) => (\n <MessageBubble\n message={m}\n pinned={pinned.has(i)}\n onCopy={() => onCopy(m)}\n onDelete={() => onDelete(i)}\n onTogglePin={() => onTogglePin(i)}\n onRegenerate={() => onRegenerate(String(m.ts ?? i))}\n />\n )}\n />\n </div>\n );\n}\n","// src/components/chat/AgentChip.tsx — custom popover agent picker, replaces <select>.\n\nimport { useEffect, useRef, useState } from 'react';\nimport { Bot, ChevronDown } from 'lucide-react';\n\ninterface Props {\n agent: string;\n setAgent: (a: string) => void;\n agents: Array<{ name: string }>;\n}\n\nexport function AgentChip({ agent, setAgent, agents }: Props) {\n const [open, setOpen] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (!open) return;\n const handler = (e: MouseEvent) => {\n if (ref.current && !ref.current.contains(e.target as Node)) {\n setOpen(false);\n }\n };\n document.addEventListener('mousedown', handler);\n return () => document.removeEventListener('mousedown', handler);\n }, [open]);\n\n const current = agent || 'no agent';\n const display = current.startsWith('@') ? current.slice(1) : current;\n\n return (\n <div className=\"chat-agent-chip\" ref={ref}>\n <button\n type=\"button\"\n className=\"chat-agent-chip-btn\"\n onClick={() => setOpen((v) => !v)}\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n >\n <Bot size={14} />\n <span>{display}</span>\n <ChevronDown size={12} className={open ? 'rotated' : ''} />\n </button>\n {open && (\n <div className=\"chat-agent-chip-menu\" role=\"listbox\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={!agent}\n onClick={() => { setAgent(''); setOpen(false); }}\n >\n <span>no agent</span>\n </button>\n {agents.map((a) => (\n <button\n key={a.name}\n type=\"button\"\n role=\"option\"\n aria-selected={agent === a.name}\n onClick={() => { setAgent(a.name); setOpen(false); }}\n >\n <span>@{a.name}</span>\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}","// src/components/chat/useAutoGrowTextarea.ts — auto-resize textarea up to maxHeight.\n\nimport { useLayoutEffect } from 'react';\n\nexport function useAutoGrowTextarea(\n ref: React.RefObject<HTMLTextAreaElement | null>,\n value: string,\n maxHeight = 240,\n): void {\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el) return;\n el.style.height = 'auto';\n const next = Math.min(el.scrollHeight, maxHeight);\n el.style.height = `${next}px`;\n el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden';\n }, [ref, value, maxHeight]);\n}","// src/components/chat/Composer.tsx — textarea + toolbar, used inside FloatingComposer.\n\nimport { useRef } from 'react';\nimport { Send, Paperclip, X, Sparkles } from 'lucide-react';\nimport { enhancePrompt } from '../../lib/api';\nimport { AgentChip } from './AgentChip';\nimport { useAutoGrowTextarea } from './useAutoGrowTextarea';\n\ninterface Props {\n agent: string;\n setAgent: (a: string) => void;\n model: string;\n setModel: (m: string) => void;\n text: string;\n setText: (t: string) => void;\n sending: boolean;\n onSend: () => void;\n attachments: string[];\n setAttachments: (a: string[] | ((prev: string[]) => string[])) => void;\n suggestions: Array<{ cmd: string; desc: string }>;\n onPickSuggestion: (cmd: string) => void;\n agents: Array<{ name: string }>;\n onAttach: () => void;\n}\n\nexport function Composer({\n agent,\n setAgent,\n model,\n setModel,\n text,\n setText,\n sending,\n onSend,\n attachments,\n setAttachments,\n suggestions,\n onPickSuggestion,\n agents,\n onAttach,\n}: Props) {\n const inputRef = useRef<HTMLTextAreaElement>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n useAutoGrowTextarea(inputRef, text, 160);\n\n const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n onSend();\n } else if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n onSend();\n } else if (e.key === 'Tab' && suggestions.length) {\n e.preventDefault();\n const base = suggestions[0].cmd.split(' ')[0];\n setText(`${base} `);\n }\n };\n\n const onFiles = (e: React.ChangeEvent<HTMLInputElement>) => {\n const files = e.target.files;\n if (!files) return;\n const names: string[] = [];\n for (let i = 0; i < files.length; i++) names.push(files[i].name);\n setAttachments((cur) => [...cur, ...names]);\n e.target.value = '';\n };\n\n return (\n <div className=\"chat-composer\">\n {suggestions.length > 0 && (\n <div className=\"chat-composer-suggestions\">\n {suggestions.map((s) => (\n <button\n key={s.cmd}\n type=\"button\"\n className=\"chat-composer-suggestion\"\n onClick={() => {\n const base = s.cmd.split(' ')[0];\n setText(`${base} `);\n inputRef.current?.focus();\n }}\n >\n <span className=\"mono\">{s.cmd}</span>\n <span>{s.desc}</span>\n </button>\n ))}\n </div>\n )}\n\n {attachments.length > 0 && (\n <div className=\"chat-composer-attachments\">\n {attachments.map((a, i) => (\n <span key={i} className=\"chat-composer-attachment-tag\">\n <Paperclip size={10} /> {a}\n <button\n type=\"button\"\n className=\"chat-composer-attachment-remove\"\n onClick={() => setAttachments((cur) => cur.filter((_, j) => j !== i))}\n aria-label={`Remove ${a}`}\n >\n <X size={10} />\n </button>\n </span>\n ))}\n </div>\n )}\n\n <div className=\"chat-composer-input\">\n <AgentChip agent={agent} setAgent={setAgent} agents={agents} />\n <textarea\n ref={inputRef}\n className=\"chat-composer-textarea\"\n placeholder={sending ? 'Sending…' : 'Send a message…'}\n rows={1}\n value={text}\n onChange={(e) => setText(e.target.value)}\n onKeyDown={onKeyDown}\n disabled={sending}\n aria-label=\"Message\"\n />\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n style={{ display: 'none' }}\n onChange={onFiles}\n aria-label=\"Attach files\"\n tabIndex={-1}\n />\n <button\n type=\"button\"\n className=\"chat-attach-btn\"\n onClick={onAttach}\n title=\"Attach files\"\n aria-label=\"Attach files\"\n >\n <Paperclip size={14} />\n </button>\n <button\n type=\"button\"\n className=\"chat-attach-btn\"\n onClick={async () => {\n if (!text.trim()) return;\n const enhanced = await enhancePrompt(text);\n if (enhanced !== text) setText(enhanced);\n }}\n title=\"Enhance prompt with AI\"\n aria-label=\"Enhance prompt\"\n >\n <Sparkles size={14} />\n </button>\n <button\n type=\"button\"\n className=\"chat-send-btn\"\n onClick={onSend}\n disabled={sending || !text.trim()}\n title=\"Send (Enter)\"\n aria-label=\"Send message\"\n >\n <Send size={16} />\n </button>\n </div>\n </div>\n );\n}","// src/components/chat/useChat.ts — shared chat state: messages, sessions,\n// send, polling, SSE, session-state tracking.\n//\n// v4.2.5 — Chat overhaul. Supports two parallel message streams:\n// - bizar (GET /api/chat)\n// - opencode (GET /api/opencode-sessions/:id/messages + SSE)\n//\n// v5.0.0 — bug #4 fix: `loadOpencodeSession` failure path now extracts\n// structured `code`, `cause`, `status`, and `suggestion` from the\n// server's ApiError.data envelope and exposes them via a new\n// `opencodeErrorInfo` field. `ChatInfoPanel` consumes that field to\n// render a structured \"Couldn't load session\" panel with the server's\n// `suggestion` verbatim, plus a Retry button.\n//\n// Changes from v3.22:\n// * SSE has automatic reconnect-with-backoff (1s → 30s, doubled on\n// each error up to a cap). Closes cleanly on unmount and on\n// session-switch (no leaked EventSource).\n// * Optimistic send uses a stable `msg_*` ID and is deduped when\n// opencode echoes the message back via SSE `message.updated`/`message.user.*`.\n// * `opencodeMessages` keeps server-provided IDs distinct from\n// optimistic IDs so the merge is unambiguous (server echo\n// replaces optimistic by matching messageID; otherwise both stick\n// around briefly until the user/session state catches up).\n// * `onSend` routes to `/opencode-sessions/:id/send` when an\n// opencode session is active, `/chat` otherwise. Errors surface\n// via toast + we always remove the optimistic placeholder.\n// * Sessions are managed by source:\n// - `bizarSessions` → local .jsonl store, POST /chat/sessions\n// - `opencodeSessions` → opencode serve, GET /opencode-sessions\n// The rail shows both side-by-side with a source indicator and\n// uses separate rename/delete endpoints per source.\n// * All async actions surface a single `busy: { create, send, rename, delete }`\n// so the rail can disable buttons properly.\n// * No `any` leaks — all SSE event shapes are typed via\n// `OpencodeSseEnvelope`.\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport type {\n ChatMessage,\n ChatResponse,\n ChatSession,\n Settings,\n Snapshot,\n} from '../../lib/types';\nimport { api, ApiError } from '../../lib/api';\nimport type { SessionState } from './ChatRail';\nimport type { AgentTreeNode } from './AgentNode';\n\nexport interface SessionDisplayState {\n /** idle / streaming / awaiting — derived from the latest SSE event. */\n state: SessionState;\n /** Number of unread (i.e. arrived while the user wasn't viewing) messages. */\n unread: number;\n /** Pinned? (cosmetic for now; pinned state lives in the parent.) */\n pinned: boolean;\n /** Sub-agent tree, if any. */\n tree?: { root: AgentTreeNode };\n}\n\n/** Per-source action busy flags. Used by rail/info-panel to disable buttons. */\nexport interface ChatBusyState {\n create: boolean;\n send: boolean;\n rename: boolean;\n delete: boolean;\n}\n\n/**\n * Structured error envelope surfaced by `loadOpencodeSession` when\n * the upstream `/api/opencode-sessions/:id/messages` call fails.\n *\n * - `code` mirrors the server's `error` field (`plugin_offline`,\n * `directory_unknown`, `opencode_error`, `bad_request`).\n * - `cause` is the server's structured cause (`network`, `timeout`,\n * `unknown`, or a raw ECONNREFUSED-style string).\n * - `suggestion` is the operator-friendly hint from the server\n * (e.g. \"Run `bizar doctor` for diagnostics\").\n * - `canRetry` is `true` whenever the failure is transient (network\n * error, upstream 502) — i.e. the user can fix it by trying again\n * after starting the plugin / waiting for opencode to come back.\n * False for hard failures (bad_request).\n */\nexport interface OpencodeErrorInfo {\n type: 'session_load_failed';\n code: string;\n message: string;\n cause?: string;\n status?: number;\n suggestion?: string | null;\n canRetry: boolean;\n}\n\n/** Shape of one opencode SSE envelope after unwrapping. */\ninterface OpencodeSseEnvelope {\n type: string;\n sessionID?: string;\n messageID?: string;\n /** Raw upstream payload (kept for forwarders that want full fidelity). */\n data?: unknown;\n}\n\n/**\n * The chat hook returns a stable bag of state + actions used by\n * both Chat.tsx (desktop) and MobileChat.tsx (mobile).\n *\n * The hook owns the lifecycle of the opencode SSE stream. Callers\n * should never instantiate their own EventSource for the same\n * session — they should always go through this hook.\n */\nexport function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?: string | null) {\n // ── Bizar stream ────────────────────────────────────────────────────────────\n const [bizarMessages, setBizarMessages] = useState<ChatMessage[]>([]);\n const [sessions, setSessions] = useState<ChatSession[]>([]);\n const [sessionId, setSessionId] = useState('');\n const [loading, setLoading] = useState(true);\n const [busy, setBusy] = useState<ChatBusyState>({\n create: false,\n send: false,\n rename: false,\n delete: false,\n });\n\n // ── Opencode stream ─────────────────────────────────────────────────────────\n const [opencodeSessions, setOpencodeSessions] = useState<ChatSession[]>([]);\n const [opencodeMessages, setOpencodeMessages] = useState<ChatMessage[]>([]);\n const [activeSource, setActiveSource] = useState<'bizar' | 'opencode' | null>(null);\n const [activeOpencodeSessionId, setActiveOpencodeSessionId] = useState<string | null>(null);\n const [opencodeError, setOpencodeError] = useState<string | null>(null);\n const [opencodeSuggestion, setOpencodeSuggestion] = useState<string | null>(null);\n /**\n * Structured error info for the opencode session load path. New in\n * v5.0.0 — `ChatInfoPanel` consumes this to render its own error\n * section (in addition to the chat-thread-header error already\n * rendered by Chat.tsx). `null` when no error.\n */\n const [opencodeErrorInfo, setOpencodeErrorInfo] = useState<OpencodeErrorInfo | null>(null);\n\n // ── v3.22 — session-state, unread, tree per session ────────────────────────\n const [sessionStates, setSessionStates] = useState<Record<string, SessionDisplayState>>({});\n const [stickToBottom, setStickToBottom] = useState(true);\n const [newMessageCount, setNewMessageCount] = useState(0);\n\n // Set of messageIDs the user has already accepted (or that arrived\n // via SSE) — used by the opencode path to dedupe optimistic + echo.\n const [seenOpencodeMessages, setSeenOpencodeMessages] = useState<Set<string>>(new Set());\n\n // Backwards-compat alias (chat.sendSession delete etc. used `pinned` historically).\n const [pinned, setPinned] = useState<Set<number>>(new Set());\n\n const listRef = useRef<HTMLDivElement>(null);\n const toastRef = useRef<\n | {\n error: (msg: string) => void;\n success: (msg: string) => void;\n info: (msg: string) => void;\n warning: (msg: string) => void;\n }\n | null\n >(null);\n\n // ── SSE lifecycle refs ──────────────────────────────────────────────────────\n const opencodeEsRef = useRef<EventSource | null>(null);\n const sseReconnectAttemptRef = useRef(0);\n const sseReconnectTimerRef = useRef<number | null>(null);\n // The id of the session we're CURRENTLY streaming. When the user\n // switches sessions, this id changes and the SSE handler short-\n // circuits any in-flight appends.\n const sseCurrentSessionIdRef = useRef<string | null>(null);\n // Whether the SSE connection should be auto-reconnected on error.\n // Set to false on explicit disconnect (session switch / unmount).\n const sseAutoReconnectRef = useRef(false);\n\n /** Append `msg` to `opencodeMessages`, replacing any prior\n * optimistic entry whose content matches or whose ID matches. */\n const appendOpencodeMessage = useCallback((msg: ChatMessage) => {\n if (!msg) return;\n setOpencodeMessages((cur) => {\n // Existing exact-ID match: skip.\n if (msg.id && cur.some((m) => m.id === msg.id)) return cur;\n // Optimistic placeholder (starts with msg_) with the same role\n // and content: replace it. This is how opencode's echo of our\n // own message gets deduplicated against the optimistic copy.\n const idx = cur.findIndex(\n (m) =>\n typeof m.id === 'string' &&\n m.id.startsWith('msg_') &&\n m.role === msg.role &&\n (m.content || '') === (msg.content || ''),\n );\n if (idx >= 0) {\n const next = cur.slice();\n next[idx] = { ...msg, id: msg.id ?? cur[idx].id };\n return next;\n }\n return [...cur, msg];\n });\n }, []);\n\n /** Mark a messageID as seen — used to dedupe echo rounds. */\n const markSeen = useCallback((id: string | undefined) => {\n if (!id) return;\n setSeenOpencodeMessages((cur) => {\n if (cur.has(id)) return cur;\n const next = new Set(cur);\n next.add(id);\n return next;\n });\n }, []);\n\n // ── Helpers to mutate per-session state ────────────────────────────────────\n const setSessionDisplayState = useCallback(\n (id: string, patch: Partial<SessionDisplayState>) => {\n setSessionStates((cur) => {\n const prev: SessionDisplayState =\n cur[id] ?? { state: 'idle', unread: 0, pinned: false };\n return { ...cur, [id]: { ...prev, ...patch } };\n });\n },\n [],\n );\n\n const markSessionStreaming = useCallback(\n (id: string, streaming: boolean) => {\n setSessionDisplayState(id, { state: streaming ? 'streaming' : 'idle' });\n },\n [setSessionDisplayState],\n );\n\n /** ─── Toast binding (set by the consuming view via `setToast`) ──── */\n const setToast = useCallback(\n (toast: typeof toastRef.current) => {\n toastRef.current = toast;\n },\n [],\n );\n\n // ── Load bizar chat ─────────────────────────────────────────────────────────\n const loadChat = useCallback(\n async (sid?: string) => {\n try {\n const url = sid\n ? `/chat?session=${encodeURIComponent(sid)}`\n : '/chat?limit=200';\n const data = await api.get<ChatResponse>(url);\n setBizarMessages(data.messages || []);\n setSessions(data.sessions || []);\n } catch (err) {\n toastRef.current?.error(\n `Chat load failed: ${(err as Error).message}`,\n );\n } finally {\n setLoading(false);\n }\n },\n [],\n );\n\n const loadTaskChat = useCallback(async (taskId: string) => {\n setLoading(true);\n try {\n const data = await api.get<{\n sessionId?: string;\n messages: ChatMessage[];\n }>(`/tasks/${encodeURIComponent(taskId)}/chat`);\n setBizarMessages(data.messages || []);\n setSessionId(data.sessionId || taskId);\n } catch (err) {\n toastRef.current?.error(\n `Task chat load failed: ${(err as Error).message}`,\n );\n } finally {\n setLoading(false);\n }\n }, []);\n\n const refreshSessions = useCallback(async () => {\n try {\n const data = await api.get<{ sessions: ChatSession[] }>('/chat/sessions');\n setSessions(data.sessions || []);\n } catch {\n /* best-effort */\n }\n }, []);\n\n const refreshOpencodeSessions = useCallback(async () => {\n try {\n const data = await api.get<{ sessions: ChatSession[] }>(\n '/opencode-sessions',\n );\n // Enrich with source marker.\n const enriched: ChatSession[] = (data.sessions || []).map((s) => ({\n ...s,\n source: 'opencode' as const,\n title: s.title || s.id,\n }));\n setOpencodeSessions(enriched);\n } catch {\n /* best-effort */\n }\n }, []);\n\n // ── Opencode SSE connection ─────────────────────────────────────────────────\n //\n // Single source of truth for the EventSource lifecycle. Callers:\n // - `loadOpencodeSession(id)` — close any current, open a new one.\n // - `closeOpencodeSession()` — close and stop auto-reconnect.\n // - unmount — close and stop.\n //\n // On error / network drop, the EventSource triggers `onerror`. We\n // close the connection and schedule a reconnect (with exponential\n // backoff: 1s, 2s, 4s, …, capped at 30s) UNLESS `sseAutoReconnectRef`\n // is false (explicit disconnect).\n //\n const openSseForSession = useCallback(\n (id: string) => {\n // Close any existing connection first.\n if (opencodeEsRef.current) {\n try {\n opencodeEsRef.current.close();\n } catch {\n /* noop */\n }\n opencodeEsRef.current = null;\n }\n if (sseReconnectTimerRef.current !== null) {\n clearTimeout(sseReconnectTimerRef.current);\n sseReconnectTimerRef.current = null;\n }\n\n // Build the SSE URL. EventSource can't set headers, so we\n // pass the auth token as ?token= when set (loopback makes this\n // optional, but harmless).\n const tok = api.getToken();\n const url = tok\n ? `/api/opencode-sessions/${encodeURIComponent(id)}/stream?token=${encodeURIComponent(tok)}`\n : `/api/opencode-sessions/${encodeURIComponent(id)}/stream`;\n\n sseCurrentSessionIdRef.current = id;\n sseAutoReconnectRef.current = true;\n sseReconnectAttemptRef.current = 0;\n setOpencodeError(null);\n setOpencodeSuggestion(null);\n setOpencodeErrorInfo(null);\n\n const connect = () => {\n if (!sseAutoReconnectRef.current) return;\n if (sseCurrentSessionIdRef.current !== id) return;\n const es = new EventSource(url);\n opencodeEsRef.current = es;\n\n // Helper: dispatch one parsed envelope to local state.\n const onEnvelope = (evt: OpencodeSseEnvelope | null) => {\n if (!evt || !evt.type) return;\n // Drop events for OTHER sessions even though the proxy\n // already filters — defense in depth.\n if (evt.sessionID && evt.sessionID !== id) return;\n\n const ts = new Date().toISOString();\n const sidForState = id;\n\n if (\n evt.type === 'message.user.created' ||\n evt.type === 'message.user.updated'\n ) {\n const payload = evt.data as\n | { messageID?: string; data?: { content?: string } }\n | undefined;\n appendOpencodeMessage({\n id: payload?.messageID ?? evt.messageID,\n role: 'user',\n content: payload?.data?.content ?? '',\n ts,\n });\n markSeen(payload?.messageID ?? evt.messageID);\n return;\n }\n if (\n evt.type === 'message.assistant.created' ||\n evt.type === 'message.assistant.updated'\n ) {\n const payload = evt.data as\n | { messageID?: string; data?: { content?: string } }\n | undefined;\n appendOpencodeMessage({\n id: payload?.messageID ?? evt.messageID,\n role: 'assistant',\n content: payload?.data?.content ?? '',\n ts,\n });\n markSeen(payload?.messageID ?? evt.messageID);\n markSessionStreaming(sidForState, true);\n return;\n }\n if (evt.type === 'message.part.updated') {\n const payload = evt.data as\n | {\n part?: { type?: string; text?: string };\n messageID?: string;\n id?: string;\n }\n | undefined;\n const text = payload?.part?.text ?? '';\n if (!text) return;\n appendOpencodeMessage({\n id: payload?.id ?? payload?.messageID ?? evt.messageID,\n role: 'assistant',\n content: text,\n ts,\n });\n markSeen(payload?.id ?? payload?.messageID ?? evt.messageID);\n markSessionStreaming(sidForState, true);\n return;\n }\n if (\n evt.type === 'session.idle' ||\n evt.type === 'message.assistant.completed' ||\n evt.type === 'session.completed'\n ) {\n markSessionStreaming(sidForState, false);\n return;\n }\n if (\n evt.type === 'session.awaiting' ||\n evt.type === 'session.awaiting_input' ||\n evt.type === 'message.assistant.awaiting'\n ) {\n setSessionDisplayState(sidForState, { state: 'awaiting' });\n return;\n }\n if (evt.type === 'session.error') {\n setOpencodeError(\n (evt.data as { message?: string } | undefined)?.message ||\n 'opencode session error',\n );\n return;\n }\n // Other event types are intentionally ignored — the proxy\n // is canonical, we don't need to project every upstream\n // event into local state.\n };\n\n // Stable parser — the server may send both `{type: \"x\"}` and\n // `sync` envelopes with a `.<n>` version suffix. We accept both.\n const parseData = (raw: string | null | undefined):\n | OpencodeSseEnvelope\n | null => {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n // Direct shape: {type, properties: {...}}\n if (typeof parsed.type === 'string') {\n const props = parsed.properties as\n | Record<string, unknown>\n | undefined;\n const data = (parsed.data as Record<string, unknown> | undefined) ?? props;\n const sessionID = (\n typeof parsed.sessionID === 'string'\n ? parsed.sessionID\n : typeof props?.sessionID === 'string'\n ? (props.sessionID as string)\n : typeof data?.sessionID === 'string'\n ? (data.sessionID as string)\n : undefined\n );\n const messageID = (\n typeof parsed.messageID === 'string'\n ? parsed.messageID\n : typeof props?.messageID === 'string'\n ? (props.messageID as string)\n : typeof data?.messageID === 'string'\n ? (data.messageID as string)\n : undefined\n );\n let type = parsed.type as string;\n const m = /\\.\\d+$/.exec(type);\n if (m) type = type.slice(0, m.index);\n return { type, sessionID, messageID, data: parsed };\n }\n // Sync envelope: {type: \"sync\", syncEvent: {...}}\n if (\n parsed.type === 'sync' &&\n parsed.syncEvent &&\n typeof parsed.syncEvent === 'object'\n ) {\n const inner = parsed.syncEvent as Record<string, unknown>;\n let type = (inner.type as string) || '';\n const m = /\\.\\d+$/.exec(type);\n if (m) type = type.slice(0, m.index);\n const innerData = (inner.data as Record<string, unknown>) || {};\n return {\n type,\n sessionID:\n typeof innerData.sessionID === 'string'\n ? innerData.sessionID\n : undefined,\n messageID:\n typeof innerData.messageID === 'string'\n ? innerData.messageID\n : undefined,\n data: inner,\n };\n }\n return null;\n } catch {\n return null;\n }\n };\n\n // register handlers per named event type\n const namedTypes = [\n 'message.user.created',\n 'message.user.updated',\n 'message.assistant.created',\n 'message.assistant.updated',\n 'message.part.updated',\n 'session.idle',\n 'message.assistant.completed',\n 'session.completed',\n 'session.awaiting',\n 'session.awaiting_input',\n 'message.assistant.awaiting',\n 'session.error',\n ];\n for (const name of namedTypes) {\n es.addEventListener(name, (e: MessageEvent) => {\n onEnvelope(parseData((e as MessageEvent).data));\n });\n }\n\n es.onerror = () => {\n if (sseCurrentSessionIdRef.current !== id) return;\n if (!sseAutoReconnectRef.current) {\n try {\n es.close();\n } catch {\n /* noop */\n }\n opencodeEsRef.current = null;\n return;\n }\n try {\n es.close();\n } catch {\n /* noop */\n }\n opencodeEsRef.current = null;\n\n const nextAttempt = sseReconnectAttemptRef.current + 1;\n sseReconnectAttemptRef.current = nextAttempt;\n // Backoff: 1s, 2s, 4s, 8s, … capped at 30s.\n const delayMs = Math.min(30_000, 1000 * 2 ** (nextAttempt - 1));\n\n if (sseReconnectTimerRef.current !== null) {\n clearTimeout(sseReconnectTimerRef.current);\n }\n sseReconnectTimerRef.current = window.setTimeout(() => {\n sseReconnectTimerRef.current = null;\n if (\n sseAutoReconnectRef.current &&\n sseCurrentSessionIdRef.current === id\n ) {\n connect();\n }\n }, delayMs);\n };\n\n es.onopen = () => {\n // Successful (re)connect — reset backoff.\n sseReconnectAttemptRef.current = 0;\n setOpencodeError(null);\n setOpencodeSuggestion(null);\n setOpencodeErrorInfo(null);\n };\n };\n\n connect();\n },\n [appendOpencodeMessage, markSeen, markSessionStreaming, setSessionDisplayState],\n );\n\n const closeOpencodeSession = useCallback(() => {\n // Disable auto-reconnect first so the onerror handler short-circuits.\n sseAutoReconnectRef.current = false;\n sseCurrentSessionIdRef.current = null;\n if (sseReconnectTimerRef.current !== null) {\n clearTimeout(sseReconnectTimerRef.current);\n sseReconnectTimerRef.current = null;\n }\n if (opencodeEsRef.current) {\n try {\n opencodeEsRef.current.close();\n } catch {\n /* noop */\n }\n opencodeEsRef.current = null;\n }\n setOpencodeMessages([]);\n setSeenOpencodeMessages(new Set());\n setOpencodeError(null);\n setOpencodeSuggestion(null);\n setOpencodeErrorInfo(null);\n setActiveOpencodeSessionId(null);\n setActiveSource('bizar');\n }, []);\n\n const loadOpencodeSession = useCallback(\n async (id: string) => {\n // Close any previous opencode session cleanly.\n sseAutoReconnectRef.current = false;\n if (opencodeEsRef.current) {\n try {\n opencodeEsRef.current.close();\n } catch {\n /* noop */\n }\n opencodeEsRef.current = null;\n }\n if (sseReconnectTimerRef.current !== null) {\n clearTimeout(sseReconnectTimerRef.current);\n sseReconnectTimerRef.current = null;\n }\n\n setOpencodeError(null);\n setOpencodeSuggestion(null);\n setOpencodeErrorInfo(null);\n setLoading(true);\n\n try {\n const data = await api.get<{ messages: ChatMessage[] }>(\n `/opencode-sessions/${encodeURIComponent(id)}/messages`,\n );\n setOpencodeMessages(data.messages || []);\n } catch (err) {\n const msg = (err as Error).message;\n // v5.0.0 — bug #4: surface the server's structured error\n // envelope so the UI can render a useful suggestion instead\n // of the raw upstream error string. The server returns\n // `{error, message, cause?, status?, suggestion?}` for every\n // non-2xx response (see serve-info.mjs + routes/opencode-\n // session-detail.mjs).\n let info: OpencodeErrorInfo | null = null;\n if (err instanceof ApiError && err.data && typeof err.data === 'object') {\n const d = err.data as {\n error?: string;\n message?: string;\n cause?: string;\n status?: number;\n suggestion?: string;\n };\n // canRetry: 503/502 are transient (plugin offline /\n // upstream hiccup); the operator can fix the plugin and\n // try again. 400 is a hard bad_request — retrying with the\n // same payload won't help.\n const code = d.error || 'unknown';\n const canRetry =\n code !== 'bad_request' &&\n code !== 'too_many_subscribers' &&\n code !== 'unauthorized';\n info = {\n type: 'session_load_failed',\n code,\n message: d.message || msg,\n cause: d.cause,\n status: d.status,\n suggestion: d.suggestion ?? null,\n canRetry,\n };\n } else if (err instanceof ApiError) {\n info = {\n type: 'session_load_failed',\n code: 'unknown',\n message: msg,\n suggestion: null,\n canRetry: true,\n };\n }\n // Legacy fields — kept so the existing Chat.tsx thread-header\n // error display continues to work unchanged.\n setOpencodeError(\n info\n ? `Opencode session load failed (${info.code}): ${info.message}`\n : `Opencode session load failed: ${msg}`,\n );\n setOpencodeSuggestion(info?.suggestion ?? null);\n setOpencodeErrorInfo(info);\n toastRef.current?.error(\n info\n ? `Opencode session load failed: ${info.message}`\n : `Opencode session load failed: ${msg}`,\n );\n setOpencodeMessages([]);\n } finally {\n setLoading(false);\n }\n\n setActiveOpencodeSessionId(id);\n setActiveSource('opencode');\n markSessionStreaming(id, false);\n openSseForSession(id);\n },\n [markSessionStreaming, openSseForSession],\n );\n\n // ── Select a bizar session ──────────────────────────────────────────────────\n const selectBizarSession = useCallback(\n (sid: string) => {\n closeOpencodeSession();\n setSessionId(sid);\n setActiveSource('bizar');\n loadChat(sid);\n // Reset unread for the session being opened.\n setSessionDisplayState(sid, { unread: 0 });\n },\n [closeOpencodeSession, loadChat, setSessionDisplayState],\n );\n\n // ── Create session (opencode primary; bizar fallback) ──────────────────────\n const onCreateSession = useCallback(async (): Promise<{\n ok: boolean;\n source?: 'bizar' | 'opencode';\n id?: string;\n }> => {\n if (busy.create) return { ok: false };\n const agent = settings.defaultAgent || 'odin';\n setBusy((b) => ({ ...b, create: true }));\n try {\n // Try to create a real opencode session first. This is the\n // primary create path: it gives the user a full conversation\n // roundtrip with SSE streaming, abort, etc.\n try {\n const created = await api.post<{\n id: string;\n title: string;\n agent: string;\n directory: string;\n createdAt: number;\n }>('/opencode-sessions/new', { agent });\n await refreshOpencodeSessions();\n // Open the newly created session right away so the user is\n // dropped into a fresh empty thread, not stuck on the\n // previous one.\n await loadOpencodeSession(created.id);\n toastRef.current?.success(`Session ${created.id} created.`);\n return { ok: true, source: 'opencode', id: created.id };\n } catch (opencodeErr) {\n // Opencode plugin offline (503) — fall back to the local\n // per-project .jsonl session. Otherwise, surface the error.\n if (!(opencodeErr instanceof ApiError) || opencodeErr.status !== 503) {\n toastRef.current?.error(\n `Create failed: ${(opencodeErr as Error).message}`,\n );\n return { ok: false };\n }\n }\n\n // Fallback: local jsonl session.\n if (!snapshot.activeProject) {\n toastRef.current?.error(\n 'Pick a project in Overview to scope chat sessions.',\n );\n return { ok: false };\n }\n const created = await api.post<ChatSession>('/chat/sessions', {});\n setSessionId(created.id);\n setBizarMessages([]);\n setPinned(new Set());\n setSessionDisplayState(created.id, { state: 'idle', unread: 0 });\n await loadChat(created.id);\n refreshSessions().catch(() => {});\n toastRef.current?.success(`Session ${created.id} created.`);\n return { ok: true, source: 'bizar', id: created.id };\n } catch (err) {\n toastRef.current?.error(`Create failed: ${(err as Error).message}`);\n return { ok: false };\n } finally {\n setBusy((b) => ({ ...b, create: false }));\n }\n }, [\n busy.create,\n settings.defaultAgent,\n snapshot.activeProject,\n refreshOpencodeSessions,\n loadOpencodeSession,\n loadChat,\n refreshSessions,\n setSessionDisplayState,\n ]);\n\n // ── Send message ────────────────────────────────────────────────────────────\n const onSend = useCallback(\n async (\n message: string,\n agent: string,\n model: string,\n attachments?: string[],\n ): Promise<{ ok: boolean }> => {\n if (busy.send) return { ok: false };\n const trimmed = message.trim();\n if (!trimmed) return { ok: false };\n\n if (activeSource === 'opencode' && activeOpencodeSessionId) {\n const sid = activeOpencodeSessionId;\n const optimisticId = `msg_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;\n const optimistic: ChatMessage = {\n id: optimisticId,\n role: 'user',\n content: trimmed,\n agent,\n ts: new Date().toISOString(),\n };\n appendOpencodeMessage(optimistic);\n markSeen(optimisticId);\n setBusy((b) => ({ ...b, send: true }));\n try {\n await api.post<{ ok: boolean; messageId: string }>(\n `/opencode-sessions/${encodeURIComponent(sid)}/send`,\n {\n message: trimmed,\n agent,\n },\n );\n markSessionStreaming(sid, true);\n return { ok: true };\n } catch (err) {\n // Remove optimistic on failure.\n setOpencodeMessages((cur) => cur.filter((m) => m.id !== optimisticId));\n toastRef.current?.error(\n `Send failed: ${(err as Error).message}`,\n );\n return { ok: false };\n } finally {\n setBusy((b) => ({ ...b, send: false }));\n }\n }\n\n // Bizar fallback.\n const optimistic: ChatMessage = {\n role: 'user',\n content: trimmed,\n agent,\n ts: new Date().toISOString(),\n };\n setBizarMessages((cur) => [...cur, optimistic]);\n setBusy((b) => ({ ...b, send: true }));\n markSessionStreaming(sessionId, true);\n try {\n const response = await api.post<{ messages?: ChatMessage[] }>(\n '/chat',\n {\n message: trimmed,\n agent,\n model,\n attachments: attachments || [],\n },\n );\n if (response?.messages) {\n for (const msg of response.messages) {\n if (msg.role === 'assistant') {\n setBizarMessages((cur) => {\n const exists = cur.some((m) => m.ts === msg.ts);\n return exists ? cur : [...cur, msg];\n });\n }\n }\n } else {\n toastRef.current?.info(\n 'Still processing… the response will appear shortly.',\n );\n }\n return { ok: true };\n } catch (err) {\n setBizarMessages((cur) => cur.filter((m) => m !== optimistic));\n toastRef.current?.error(\n `Send failed: ${(err as Error).message}`,\n );\n return { ok: false };\n } finally {\n setBusy((b) => ({ ...b, send: false }));\n markSessionStreaming(sessionId, false);\n }\n },\n [\n activeSource,\n activeOpencodeSessionId,\n appendOpencodeMessage,\n markSeen,\n markSessionStreaming,\n sessionId,\n busy.send,\n ],\n );\n\n const onRegenerate = useCallback(\n async (messageId: string) => {\n if (!sessionId) {\n toastRef.current?.error('No active session to regenerate.');\n return;\n }\n try {\n await api.post('/chat/regenerate', { sessionId, messageId });\n await loadChat(sessionId);\n } catch (err) {\n toastRef.current?.error(\n `Regenerate failed: ${(err as Error).message}`,\n );\n }\n },\n [sessionId, loadChat],\n );\n\n const deleteMessage = useCallback(\n (idx: number) => {\n if (activeSource === 'opencode') {\n setOpencodeMessages((cur) => cur.filter((_, i) => i !== idx));\n } else {\n setBizarMessages((cur) => cur.filter((_, i) => i !== idx));\n }\n },\n [activeSource],\n );\n\n const togglePin = useCallback((idx: number) => {\n setPinned((cur) => {\n const next = new Set(cur);\n if (next.has(idx)) next.delete(idx);\n else next.add(idx);\n return next;\n });\n }, []);\n\n const copyMessage = useCallback((m: ChatMessage) => {\n const text = m.content || m.message || '';\n try {\n navigator.clipboard?.writeText(text).then(\n () => toastRef.current?.success('Copied.'),\n () => toastRef.current?.error('Copy failed.'),\n );\n } catch {\n toastRef.current?.error('Copy failed.');\n }\n }, []);\n\n // ── Jump-to-latest ──────────────────────────────────────────────────────────\n const handleScroll = useCallback(() => {\n const el = listRef.current;\n if (!el) return;\n const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;\n const atBottom = distFromBottom < 32;\n setStickToBottom(atBottom);\n if (atBottom) setNewMessageCount(0);\n }, []);\n\n const jumpToLatest = useCallback(() => {\n const el = listRef.current;\n if (!el) return;\n el.scrollTop = el.scrollHeight;\n setStickToBottom(true);\n setNewMessageCount(0);\n }, []);\n\n // ── Session rename / delete (rail row menu + info panel actions) ────────────\n const renameSession = useCallback(\n async (id: string, title: string): Promise<boolean> => {\n if (busy.rename) return false;\n setBusy((b) => ({ ...b, rename: true }));\n try {\n const trimmed = title.trim();\n if (!trimmed) {\n toastRef.current?.error('Title cannot be empty.');\n return false;\n }\n // Try the opencode path first if this id is in the opencode\n // session list, otherwise the local path. We dispatch by\n // inspecting the in-memory set (avoids a round trip).\n const isOpencode = opencodeSessions.some((s) => s.id === id);\n if (isOpencode) {\n await api.patch<{ id: string; title: string }>(\n `/opencode-sessions/${encodeURIComponent(id)}`,\n { title: trimmed },\n );\n setOpencodeSessions((cur) =>\n cur.map((s) => (s.id === id ? { ...s, title: trimmed } : s)),\n );\n } else {\n await api.post(\n `/chat/sessions/${encodeURIComponent(id)}/rename`,\n { title: trimmed },\n );\n setSessions((cur) =>\n cur.map((s) => (s.id === id ? { ...s, title: trimmed } : s)),\n );\n }\n toastRef.current?.success('Renamed.');\n return true;\n } catch (err) {\n toastRef.current?.error(\n `Rename failed: ${(err as Error).message}`,\n );\n return false;\n } finally {\n setBusy((b) => ({ ...b, rename: false }));\n }\n },\n [busy.rename, opencodeSessions],\n );\n\n const deleteSession = useCallback(\n async (id: string): Promise<boolean> => {\n if (busy.delete) return false;\n setBusy((b) => ({ ...b, delete: true }));\n try {\n const isOpencode = opencodeSessions.some((s) => s.id === id);\n if (isOpencode) {\n await api.del(`/opencode-sessions/${encodeURIComponent(id)}`);\n setOpencodeSessions((cur) => cur.filter((s) => s.id !== id));\n if (activeOpencodeSessionId === id) {\n closeOpencodeSession();\n }\n } else {\n await api.del(`/chat/sessions/${encodeURIComponent(id)}`);\n setSessions((cur) => cur.filter((s) => s.id !== id));\n if (sessionId === id) {\n setSessionId('');\n setBizarMessages([]);\n }\n }\n setSessionStates((cur) => {\n const next = { ...cur };\n delete next[id];\n return next;\n });\n toastRef.current?.success('Session deleted.');\n return true;\n } catch (err) {\n toastRef.current?.error(\n `Delete failed: ${(err as Error).message}`,\n );\n return false;\n } finally {\n setBusy((b) => ({ ...b, delete: false }));\n }\n },\n [\n busy.delete,\n opencodeSessions,\n activeOpencodeSessionId,\n closeOpencodeSession,\n sessionId,\n ],\n );\n\n // ── Auto-scroll on new messages ─────────────────────────────────────────────\n useEffect(() => {\n if (listRef.current) {\n if (stickToBottom) {\n listRef.current.scrollTop = listRef.current.scrollHeight;\n } else {\n setNewMessageCount((n) => n + 1);\n }\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [bizarMessages, opencodeMessages]);\n\n // ── Cleanup SSE on unmount ──────────────────────────────────────────────────\n useEffect(() => {\n return () => {\n sseAutoReconnectRef.current = false;\n sseCurrentSessionIdRef.current = null;\n if (sseReconnectTimerRef.current !== null) {\n clearTimeout(sseReconnectTimerRef.current);\n sseReconnectTimerRef.current = null;\n }\n if (opencodeEsRef.current) {\n try {\n opencodeEsRef.current.close();\n } catch {\n /* noop */\n }\n opencodeEsRef.current = null;\n }\n };\n }, []);\n\n // ── Initial load ────────────────────────────────────────────────────────────\n useEffect(() => {\n if (initialTaskId) {\n loadTaskChat(initialTaskId);\n } else {\n loadChat();\n }\n refreshOpencodeSessions();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // ── Listen for bizar:setChatTask events ────────────────────────────────────\n useEffect(() => {\n const handler = (e: Event) => {\n const taskId = (e as CustomEvent<{ taskId: string }>).detail?.taskId;\n if (taskId) loadTaskChat(taskId);\n };\n window.addEventListener('bizar:setChatTask', handler);\n return () => window.removeEventListener('bizar:setChatTask', handler);\n }, [loadTaskChat]);\n\n // ── WebSocket for live bizar updates ────────────────────────────────────────\n useEffect(() => {\n let closed = false;\n import('../../lib/ws').then(({ Ws }) => {\n if (closed) return;\n const ws = new Ws();\n ws.on((msg: { type: string; message?: ChatMessage }) => {\n if (msg.type !== 'chat:message') return;\n const next = msg.message;\n if (!next) return;\n setBizarMessages((cur) => {\n const exists = cur.some((m) => m.ts === next.ts);\n return exists ? cur : [...cur, next];\n });\n });\n });\n return () => {\n closed = true;\n };\n }, []);\n\n return {\n // ── State ────────────────────────────────────────────────────────────────\n messages:\n activeSource === 'opencode' ? opencodeMessages : bizarMessages,\n bizarMessages,\n opencodeMessages,\n sessions,\n opencodeSessions,\n sessionId,\n setSessionId,\n loading,\n sending: busy.send,\n pinned,\n listRef,\n // ── Opencode state ──────────────────────────────────────────────────────\n activeSource,\n activeOpencodeSessionId,\n opencodeError,\n opencodeSuggestion,\n /**\n * v5.0.0 — bug #4: structured error info consumed by\n * `ChatInfoPanel`. `null` when no error is active.\n */\n opencodeErrorInfo,\n /**\n * v5.0.0 — bug #4: retry helper exposed for `ChatInfoPanel`'s\n * Retry button. Calls `loadOpencodeSession` on the currently\n * active opencode session (or no-op if none is active).\n */\n retryOpencodeSession: () => {\n if (activeOpencodeSessionId) void loadOpencodeSession(activeOpencodeSessionId);\n },\n // ── v3.22 — per-session display state ──────────────────────────────────\n sessionStates,\n busy,\n getSessionDisplay: (s: ChatSession) => ({\n ...s,\n state: sessionStates[s.id]?.state ?? 'idle',\n unread: sessionStates[s.id]?.unread ?? 0,\n pinned: sessionStates[s.id]?.pinned ?? false,\n tree: sessionStates[s.id]?.tree,\n }),\n // ── Jump-to-latest ──────────────────────────────────────────────────────\n stickToBottom,\n newMessageCount,\n handleScroll,\n jumpToLatest,\n // ── Toast integration ──────────────────────────────────────────────────\n setToast,\n // ── Actions ─────────────────────────────────────────────────────────────\n loadChat,\n loadTaskChat,\n refreshSessions,\n refreshOpencodeSessions,\n loadOpencodeSession,\n closeOpencodeSession,\n selectBizarSession,\n onCreateSession,\n onSend,\n onRegenerate,\n deleteMessage,\n togglePin,\n copyMessage,\n renameSession,\n deleteSession,\n seenOpencodeMessages,\n };\n}\n","// src/components/chat/useSlashCommands.ts — filter slash-command suggestions from mods.\n\nimport { useMemo, useState } from 'react';\nimport type { Mod, Snapshot } from '../../lib/types';\n\nexport type SlashCommand = { cmd: string; desc: string; mod?: string };\n\nconst BUILTIN_COMMANDS: SlashCommand[] = [\n { cmd: '/visual-plan [on|off|status]', desc: 'Toggle or view visual plan mode' },\n { cmd: '/plan new <slug> [template]', desc: 'Create a new plan' },\n { cmd: '/plan list', desc: 'List all plans' },\n { cmd: '/plan open <slug>', desc: 'Open a plan in the viewer' },\n { cmd: '/plan status <slug> <status>', desc: 'Set plan status' },\n { cmd: '/plan get <slug>', desc: 'Fetch plan canvas' },\n { cmd: '/plan add <slug> --title T --type kind', desc: 'Add element to a plan' },\n { cmd: '/plan update <slug> <id>', desc: 'Update a plan element' },\n { cmd: '/plan delete <slug> <id>', desc: 'Delete a plan element' },\n { cmd: '/plan comment <slug> [id] \"text\"', desc: 'Add a comment' },\n { cmd: '/plan comments <slug> [id]', desc: 'Read plan comments' },\n { cmd: '/bizar', desc: 'Launch Bizar dashboard' },\n { cmd: '/bizar <args>', desc: 'Route via Bizar menu' },\n { cmd: '/audit', desc: 'Run security audit' },\n { cmd: '/explain <q>', desc: 'Read-only code Q&A' },\n { cmd: '/init', desc: 'Initialize .bizar/ in this project' },\n { cmd: '/learn', desc: 'Extract patterns from session' },\n { cmd: '/pr-review', desc: 'PR review' },\n { cmd: '/help | /commands', desc: 'Show all Bizar commands' },\n];\n\nexport function useSlashCommands(snapshot: Snapshot) {\n const [query, setQuery] = useState('');\n\n const allCommands = useMemo<SlashCommand[]>(() => [\n ...BUILTIN_COMMANDS,\n ...(snapshot.mods || []).flatMap((m: Mod) =>\n m.entry?.command ? [{ cmd: `/${m.id}`, desc: m.description || m.name, mod: m.id }] : [],\n ),\n ], [snapshot.mods]);\n\n const suggestions = useMemo(() => {\n if (!query.startsWith('/') || query.includes(' ')) return [];\n const q = query.toLowerCase();\n return allCommands.filter((c) => c.cmd.toLowerCase().startsWith(q)).slice(0, 6);\n }, [query, allCommands]);\n\n return { allCommands, suggestions, setQuery };\n}"],"file":"assets/useSlashCommands-B3MZ1JXa.js"}
|
|
1
|
+
{"version":3,"mappings":"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","names":["VirtualList","items","renderItem","itemHeight","height","overscan","className","scrollTop","setScrollTop","useState","containerRef","useRef","visibleRange","useMemo","start","end","totalHeight","offsetY","visibleItems","jsx","e","item","i","ChatTopBar","activeProject","sessionCount","sessionsOpen","infoOpen","onToggleSessions","onToggleInfo","onOpenOverview","jsxs","Folder","ChevronDown","LoadingSkeleton","count","_","SuggestionCards","projectName","onPickSuggestion","suggestions","Code","BookOpen","Lightbulb","ImageIcon","s","WelcomeScreen","variant","displayName","MessageBubble","message","pinned","onCopy","onDelete","onTogglePin","onRegenerate","role","ts","formatTime","content","isUser","Copy","RefreshCw","Pin","Trash2","ReactMarkdown","remarkGfm","ChatThread","messages","loading","sessionId","activeSource","innerRef","useEffect","el","p","wrapped","stream","m","AgentChip","agent","setAgent","agents","open","setOpen","ref","handler","current","display","v","Bot","a","useAutoGrowTextarea","value","maxHeight","useLayoutEffect","next","Composer","model","setModel","text","setText","sending","onSend","attachments","setAttachments","onAttach","inputRef","fileInputRef","onKeyDown","base","onFiles","files","names","cur","_a","Paperclip","j","X","enhanced","enhancePrompt","Sparkles","Send","useChat","snapshot","settings","initialTaskId","bizarMessages","setBizarMessages","sessions","setSessions","setSessionId","setLoading","busy","setBusy","opencodeSessions","setOpencodeSessions","opencodeMessages","setOpencodeMessages","setActiveSource","activeOpencodeSessionId","setActiveOpencodeSessionId","opencodeError","setOpencodeError","opencodeSuggestion","setOpencodeSuggestion","opencodeErrorInfo","setOpencodeErrorInfo","sessionStates","setSessionStates","stickToBottom","setStickToBottom","newMessageCount","setNewMessageCount","seenOpencodeMessages","setSeenOpencodeMessages","setPinned","listRef","toastRef","opencodeEsRef","sseReconnectAttemptRef","sseReconnectTimerRef","sseCurrentSessionIdRef","sseAutoReconnectRef","appendOpencodeMessage","useCallback","msg","idx","markSeen","id","setSessionDisplayState","patch","prev","markSessionStreaming","streaming","setToast","toast","loadChat","sid","url","data","api","err","loadTaskChat","taskId","refreshSessions","refreshOpencodeSessions","enriched","openSseForSession","tok","connect","es","onEnvelope","evt","sidForState","payload","_b","_c","_d","parseData","raw","parsed","props","sessionID","messageID","type","inner","innerData","namedTypes","name","nextAttempt","delayMs","closeOpencodeSession","loadOpencodeSession","info","ApiError","d","code","canRetry","selectBizarSession","onCreateSession","b","created","opencodeErr","_e","trimmed","optimisticId","optimistic","response","messageId","deleteMessage","togglePin","copyMessage","handleScroll","atBottom","jumpToLatest","renameSession","title","deleteSession","n","closed","__vitePreload","Ws","BUILTIN_COMMANDS","useSlashCommands","query","setQuery","allCommands","q","c"],"ignoreList":[],"sources":["../../src/web/components/VirtualList.tsx","../../src/web/components/chat/ChatTopBar.tsx","../../src/web/components/chat/LoadingSkeleton.tsx","../../src/web/components/chat/SuggestionCards.tsx","../../src/web/components/chat/WelcomeScreen.tsx","../../src/web/components/chat/MessageBubble.tsx","../../src/web/components/chat/ChatThread.tsx","../../src/web/components/chat/AgentChip.tsx","../../src/web/components/chat/useAutoGrowTextarea.ts","../../src/web/components/chat/Composer.tsx","../../src/web/components/chat/useChat.ts","../../src/web/components/chat/useSlashCommands.ts"],"sourcesContent":["// src/web/components/VirtualList.tsx — hand-rolled virtual scrolling (no external deps).\nimport React, { useState, useRef, useEffect, useMemo } from 'react';\n\nexport function VirtualList<T>({\n items,\n renderItem,\n itemHeight = 80,\n height = 400,\n overscan = 5,\n className,\n}: {\n items: T[];\n renderItem: (item: T, index: number) => React.ReactNode;\n itemHeight?: number;\n height?: number;\n overscan?: number;\n className?: string;\n}) {\n const [scrollTop, setScrollTop] = useState(0);\n const containerRef = useRef<HTMLDivElement>(null);\n\n const visibleRange = useMemo(() => {\n const start = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);\n const end = Math.min(\n items.length,\n Math.ceil((scrollTop + height) / itemHeight) + overscan,\n );\n return { start, end };\n }, [scrollTop, itemHeight, height, overscan, items.length]);\n\n const totalHeight = items.length * itemHeight;\n const offsetY = visibleRange.start * itemHeight;\n const visibleItems = items.slice(visibleRange.start, visibleRange.end);\n\n return (\n <div\n ref={containerRef}\n onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}\n style={{ height, overflowY: 'auto' }}\n className={className}\n >\n <div style={{ height: totalHeight, position: 'relative' }}>\n <div style={{ transform: `translateY(${offsetY}px)` }}>\n {visibleItems.map((item, i) => (\n <div key={visibleRange.start + i} style={{ height: itemHeight }}>\n {renderItem(item, visibleRange.start + i)}\n </div>\n ))}\n </div>\n </div>\n </div>\n );\n}\n","// src/components/chat/ChatTopBar.tsx — 48px topbar with project chip + panel toggles.\n\nimport { Folder, ChevronDown } from 'lucide-react';\n\ninterface Props {\n activeProject: { name: string; path?: string } | null;\n sessionCount: number;\n sessionsOpen: boolean;\n infoOpen: boolean;\n onToggleSessions: () => void;\n onToggleInfo: () => void;\n onOpenOverview: () => void;\n}\n\nexport function ChatTopBar({\n activeProject,\n sessionCount,\n sessionsOpen,\n infoOpen,\n onToggleSessions,\n onToggleInfo,\n onOpenOverview,\n}: Props) {\n return (\n <header className=\"chat-topbar\">\n <button type=\"button\" className=\"chat-topbar-project\" onClick={onOpenOverview}>\n <Folder size={14} />\n <span className=\"chat-topbar-project-name\">{activeProject?.name ?? 'No project'}</span>\n <ChevronDown size={12} />\n </button>\n <div className=\"chat-topbar-spacer\" />\n <button\n type=\"button\"\n className={`chat-topbar-btn ${sessionsOpen ? 'active' : ''}`}\n onClick={onToggleSessions}\n aria-pressed={sessionsOpen}\n >\n Sessions ({sessionCount})\n </button>\n <button\n type=\"button\"\n className={`chat-topbar-btn ${infoOpen ? 'active' : ''}`}\n onClick={onToggleInfo}\n aria-pressed={infoOpen}\n >\n Info\n </button>\n </header>\n );\n}","// src/components/chat/LoadingSkeleton.tsx — pulsing skeleton bubbles while loading.\n\ninterface Props {\n count?: number;\n}\n\nexport function LoadingSkeleton({ count = 3 }: Props) {\n return (\n <div className=\"chat-loading-skeleton\" aria-label=\"Loading messages\">\n {Array.from({ length: count }).map((_, i) => (\n <div key={i} className=\"chat-loading-skeleton-bubble\">\n <div className=\"chat-loading-skeleton-meta\" />\n <div className=\"chat-loading-skeleton-line\" />\n <div className=\"chat-loading-skeleton-line short\" />\n </div>\n ))}\n </div>\n );\n}","// src/components/chat/SuggestionCards.tsx — 4 hardcoded suggestion cards for first-run.\n\nimport { Code, BookOpen, Lightbulb, Image as ImageIcon } from 'lucide-react';\n\ninterface Props {\n projectName: string;\n onPickSuggestion: (text: string) => void;\n}\n\nexport function SuggestionCards({ projectName, onPickSuggestion }: Props) {\n const suggestions = [\n {\n icon: <Code size={16} />,\n title: projectName ? `Explain ${projectName}` : 'Explain this project',\n preview: 'Give me a tour of the codebase, highlight the architecture, and call out any anti-patterns or hotspots.',\n prompt: projectName\n ? `Explain the ${projectName} project. Give me a tour of the codebase, highlight the architecture, and call out any anti-patterns or hotspots.`\n : 'Explain the current project. Give me a tour of the codebase, highlight the architecture, and call out any anti-patterns or hotspots.',\n },\n {\n icon: <BookOpen size={16} />,\n title: 'Summarize recent sessions',\n preview: 'Read my last 5 chat sessions and give me a 3-bullet summary of what I worked on and what got stuck.',\n prompt: 'Read my last 5 chat sessions and give me a 3-bullet summary of what I worked on and what got stuck.',\n },\n {\n icon: <Lightbulb size={16} />,\n title: 'Suggest a refactor',\n preview: 'Look for the largest, most painful files in this project and propose a concrete refactor for the worst one.',\n prompt: 'Look for the largest, most painful files in this project and propose a concrete refactor for the worst one.',\n },\n {\n icon: <ImageIcon size={16} />,\n title: 'Find visual regressions',\n preview: 'Open the dashboard, screenshot the Chat tab, and flag any layout problems or anti-patterns you see.',\n prompt: 'Open the dashboard, screenshot the Chat tab, and flag any layout problems or anti-patterns you see.',\n },\n ];\n\n return (\n <div className=\"chat-suggestions\">\n {suggestions.map((s, i) => (\n <button\n key={i}\n type=\"button\"\n className=\"chat-suggestion\"\n onClick={() => onPickSuggestion(s.prompt)}\n >\n <div className=\"chat-suggestion-title\">{s.icon}<span>{s.title}</span></div>\n <div className=\"chat-suggestion-preview\">{s.preview}</div>\n </button>\n ))}\n </div>\n );\n}","// src/components/chat/WelcomeScreen.tsx — gradient greeting + no-project state.\n\nimport { Folder } from 'lucide-react';\nimport { SuggestionCards } from './SuggestionCards';\n\ninterface Props {\n variant: 'empty' | 'no-project';\n projectName: string;\n onPickSuggestion: (text: string) => void;\n}\n\nexport function WelcomeScreen({ variant, projectName, onPickSuggestion }: Props) {\n if (variant === 'no-project') {\n return (\n <div className=\"chat-welcome\">\n <h1 className=\"chat-welcome-h1\">\n <span className=\"chat-greeting-hello\">No project selected.</span>\n </h1>\n <p className=\"chat-welcome-subtitle\">Pick a project in Overview to scope chat sessions.</p>\n </div>\n );\n }\n\n const displayName = projectName || 'there';\n\n return (\n <div className=\"chat-welcome\">\n <h1 className=\"chat-welcome-h1\">\n <span className=\"chat-greeting-hello\">Hello, </span>\n <span className=\"chat-greeting-name\">{displayName}.</span>\n </h1>\n <p className=\"chat-welcome-subtitle\">How can I help you today?</p>\n <SuggestionCards projectName={projectName} onPickSuggestion={onPickSuggestion} />\n </div>\n );\n}","// src/components/chat/MessageBubble.tsx — user/assistant message with hover actions.\n\nimport ReactMarkdown from 'react-markdown';\nimport remarkGfm from 'remark-gfm';\nimport { Copy, RefreshCw, Pin, Trash2 } from 'lucide-react';\nimport { formatTime } from '../../lib/utils';\n\ninterface Message {\n role?: string;\n content?: string;\n message?: string;\n agent?: string;\n ts?: string | number;\n}\n\ninterface Props {\n message: Message;\n pinned: boolean;\n onCopy: () => void;\n onDelete: () => void;\n onTogglePin: () => void;\n onRegenerate: () => void;\n}\n\nexport function MessageBubble({ message, pinned, onCopy, onDelete, onTogglePin, onRegenerate }: Props) {\n const role = (message.role || 'assistant').toLowerCase();\n const ts = message.ts ? formatTime(message.ts) : '';\n const content = message.content || message.message || '';\n const isUser = role === 'user';\n\n return (\n <article className={`chat-message ${isUser ? 'chat-message-user' : 'chat-message-assistant'} ${pinned ? 'chat-message-pinned' : ''}`}>\n <header className=\"chat-message-meta\">\n <span className=\"chat-message-role\">{role}</span>\n {message.agent && <span className=\"chat-message-agent\">@{message.agent}</span>}\n {ts && <time className=\"chat-message-time\">{ts}</time>}\n {pinned && <span className=\"chat-message-pin\">pinned</span>}\n <div className=\"chat-message-actions\">\n <button type=\"button\" className=\"chat-msg-action-btn\" onClick={onCopy} title=\"Copy\" aria-label=\"Copy\">\n <Copy size={12} />\n </button>\n {!isUser && (\n <button type=\"button\" className=\"chat-msg-action-btn\" onClick={onRegenerate} title=\"Regenerate\" aria-label=\"Regenerate\">\n <RefreshCw size={12} />\n </button>\n )}\n <button type=\"button\" className=\"chat-msg-action-btn\" onClick={onTogglePin} title=\"Pin\" aria-label=\"Pin\">\n <Pin size={12} />\n </button>\n <button type=\"button\" className=\"chat-msg-action-btn chat-msg-action-btn-danger\" onClick={onDelete} title=\"Delete\" aria-label=\"Delete\">\n <Trash2 size={12} />\n </button>\n </div>\n </header>\n <div className=\"chat-message-content markdown-body\">\n <ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>\n </div>\n </article>\n );\n}","// src/components/chat/ChatThread.tsx — message list, welcome screen, loading skeleton.\n//\n// v4.2.5 — accepts `activeSource` so the loading skeleton can hint\n// which stream is loading and the welcome variant for \"opencode\"\n// can differ from the local chat welcome.\n\nimport { useEffect, useRef } from 'react';\nimport { LoadingSkeleton } from './LoadingSkeleton';\nimport { WelcomeScreen } from './WelcomeScreen';\nimport { MessageBubble } from './MessageBubble';\nimport { VirtualList } from '../VirtualList';\n\ninterface Message {\n role?: string;\n content?: string;\n message?: string;\n agent?: string;\n ts?: string | number;\n}\n\ninterface Props {\n messages: Message[];\n loading: boolean;\n activeProject: { name: string } | null;\n sessionId: string;\n pinned: Set<number>;\n /** Which stream currently owns the thread — purely cosmetic, lets\n * the empty/loading states be slightly more specific. The type\n * uses `unknown` so we don't pull in a tight coupling; the value\n * is reflected back into the loading skeleton and welcome screen\n * via data attributes so CSS can style accordingly. */\n activeSource?: 'bizar' | 'opencode' | null;\n onPickSuggestion: (text: string) => void;\n onCopy: (m: Message) => void;\n onDelete: (idx: number) => void;\n onTogglePin: (idx: number) => void;\n onRegenerate: (messageId: string) => void;\n}\n\nexport function ChatThread({\n messages,\n loading,\n activeProject,\n sessionId,\n pinned,\n activeSource,\n onPickSuggestion,\n onCopy,\n onDelete,\n onTogglePin,\n onRegenerate,\n}: Props) {\n const innerRef = useRef<HTMLDivElement>(null);\n\n // Only auto-scroll when we own the scroll container (i.e. when\n // rendered directly as the message scroller). When embedded\n // inside Chat.tsx's chat-thread-scroll, the parent handles scroll.\n useEffect(() => {\n const el = innerRef.current;\n if (!el) return;\n let p: HTMLElement | null = el.parentElement;\n let wrapped = false;\n while (p) {\n if (p.classList.contains('chat-thread-scroll')) {\n wrapped = true;\n break;\n }\n p = p.parentElement;\n }\n if (!wrapped) el.scrollTop = el.scrollHeight;\n }, [messages]);\n\n const stream: 'bizar' | 'opencode' = activeSource === 'opencode' ? 'opencode' : 'bizar';\n\n if (loading) {\n return (\n <div className=\"chat-thread legacy\" ref={innerRef} data-stream={stream}>\n <LoadingSkeleton count={3} />\n </div>\n );\n }\n\n if (!activeProject) {\n return (\n <div className=\"chat-thread legacy\" ref={innerRef} data-stream={stream}>\n <WelcomeScreen\n variant=\"no-project\"\n projectName=\"\"\n onPickSuggestion={onPickSuggestion}\n />\n </div>\n );\n }\n\n if (messages.length === 0) {\n return (\n <div className=\"chat-thread legacy\" ref={innerRef} data-stream={stream}>\n <WelcomeScreen\n variant=\"empty\"\n projectName={activeProject.name}\n onPickSuggestion={onPickSuggestion}\n />\n </div>\n );\n }\n\n return (\n <div className=\"chat-thread legacy\" ref={innerRef}>\n <VirtualList\n items={messages}\n itemHeight={120}\n height={800}\n className=\"chat-thread-virtual\"\n renderItem={(m, i) => (\n <MessageBubble\n message={m}\n pinned={pinned.has(i)}\n onCopy={() => onCopy(m)}\n onDelete={() => onDelete(i)}\n onTogglePin={() => onTogglePin(i)}\n onRegenerate={() => onRegenerate(String(m.ts ?? i))}\n />\n )}\n />\n </div>\n );\n}\n","// src/components/chat/AgentChip.tsx — custom popover agent picker, replaces <select>.\n\nimport { useEffect, useRef, useState } from 'react';\nimport { Bot, ChevronDown } from 'lucide-react';\n\ninterface Props {\n agent: string;\n setAgent: (a: string) => void;\n agents: Array<{ name: string }>;\n}\n\nexport function AgentChip({ agent, setAgent, agents }: Props) {\n const [open, setOpen] = useState(false);\n const ref = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n if (!open) return;\n const handler = (e: MouseEvent) => {\n if (ref.current && !ref.current.contains(e.target as Node)) {\n setOpen(false);\n }\n };\n document.addEventListener('mousedown', handler);\n return () => document.removeEventListener('mousedown', handler);\n }, [open]);\n\n const current = agent || 'no agent';\n const display = current.startsWith('@') ? current.slice(1) : current;\n\n return (\n <div className=\"chat-agent-chip\" ref={ref}>\n <button\n type=\"button\"\n className=\"chat-agent-chip-btn\"\n onClick={() => setOpen((v) => !v)}\n aria-haspopup=\"listbox\"\n aria-expanded={open}\n >\n <Bot size={14} />\n <span>{display}</span>\n <ChevronDown size={12} className={open ? 'rotated' : ''} />\n </button>\n {open && (\n <div className=\"chat-agent-chip-menu\" role=\"listbox\">\n <button\n type=\"button\"\n role=\"option\"\n aria-selected={!agent}\n onClick={() => { setAgent(''); setOpen(false); }}\n >\n <span>no agent</span>\n </button>\n {agents.map((a) => (\n <button\n key={a.name}\n type=\"button\"\n role=\"option\"\n aria-selected={agent === a.name}\n onClick={() => { setAgent(a.name); setOpen(false); }}\n >\n <span>@{a.name}</span>\n </button>\n ))}\n </div>\n )}\n </div>\n );\n}","// src/components/chat/useAutoGrowTextarea.ts — auto-resize textarea up to maxHeight.\n\nimport { useLayoutEffect } from 'react';\n\nexport function useAutoGrowTextarea(\n ref: React.RefObject<HTMLTextAreaElement | null>,\n value: string,\n maxHeight = 240,\n): void {\n useLayoutEffect(() => {\n const el = ref.current;\n if (!el) return;\n el.style.height = 'auto';\n const next = Math.min(el.scrollHeight, maxHeight);\n el.style.height = `${next}px`;\n el.style.overflowY = el.scrollHeight > maxHeight ? 'auto' : 'hidden';\n }, [ref, value, maxHeight]);\n}","// src/components/chat/Composer.tsx — textarea + toolbar, used inside FloatingComposer.\n\nimport { useRef } from 'react';\nimport { Send, Paperclip, X, Sparkles } from 'lucide-react';\nimport { enhancePrompt } from '../../lib/api';\nimport { AgentChip } from './AgentChip';\nimport { useAutoGrowTextarea } from './useAutoGrowTextarea';\n\ninterface Props {\n agent: string;\n setAgent: (a: string) => void;\n model: string;\n setModel: (m: string) => void;\n text: string;\n setText: (t: string) => void;\n sending: boolean;\n onSend: () => void;\n attachments: string[];\n setAttachments: (a: string[] | ((prev: string[]) => string[])) => void;\n suggestions: Array<{ cmd: string; desc: string }>;\n onPickSuggestion: (cmd: string) => void;\n agents: Array<{ name: string }>;\n onAttach: () => void;\n}\n\nexport function Composer({\n agent,\n setAgent,\n model,\n setModel,\n text,\n setText,\n sending,\n onSend,\n attachments,\n setAttachments,\n suggestions,\n onPickSuggestion,\n agents,\n onAttach,\n}: Props) {\n const inputRef = useRef<HTMLTextAreaElement>(null);\n const fileInputRef = useRef<HTMLInputElement>(null);\n useAutoGrowTextarea(inputRef, text, 160);\n\n const onKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n onSend();\n } else if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n onSend();\n } else if (e.key === 'Tab' && suggestions.length) {\n e.preventDefault();\n const base = suggestions[0].cmd.split(' ')[0];\n setText(`${base} `);\n }\n };\n\n const onFiles = (e: React.ChangeEvent<HTMLInputElement>) => {\n const files = e.target.files;\n if (!files) return;\n const names: string[] = [];\n for (let i = 0; i < files.length; i++) names.push(files[i].name);\n setAttachments((cur) => [...cur, ...names]);\n e.target.value = '';\n };\n\n return (\n <div className=\"chat-composer\">\n {suggestions.length > 0 && (\n <div className=\"chat-composer-suggestions\">\n {suggestions.map((s) => (\n <button\n key={s.cmd}\n type=\"button\"\n className=\"chat-composer-suggestion\"\n onClick={() => {\n const base = s.cmd.split(' ')[0];\n setText(`${base} `);\n inputRef.current?.focus();\n }}\n >\n <span className=\"mono\">{s.cmd}</span>\n <span>{s.desc}</span>\n </button>\n ))}\n </div>\n )}\n\n {attachments.length > 0 && (\n <div className=\"chat-composer-attachments\">\n {attachments.map((a, i) => (\n <span key={i} className=\"chat-composer-attachment-tag\">\n <Paperclip size={10} /> {a}\n <button\n type=\"button\"\n className=\"chat-composer-attachment-remove\"\n onClick={() => setAttachments((cur) => cur.filter((_, j) => j !== i))}\n aria-label={`Remove ${a}`}\n >\n <X size={10} />\n </button>\n </span>\n ))}\n </div>\n )}\n\n <div className=\"chat-composer-input\">\n <AgentChip agent={agent} setAgent={setAgent} agents={agents} />\n <textarea\n ref={inputRef}\n className=\"chat-composer-textarea\"\n placeholder={sending ? 'Sending…' : 'Send a message…'}\n rows={1}\n value={text}\n onChange={(e) => setText(e.target.value)}\n onKeyDown={onKeyDown}\n disabled={sending}\n aria-label=\"Message\"\n />\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n style={{ display: 'none' }}\n onChange={onFiles}\n aria-label=\"Attach files\"\n tabIndex={-1}\n />\n <button\n type=\"button\"\n className=\"chat-attach-btn\"\n onClick={onAttach}\n title=\"Attach files\"\n aria-label=\"Attach files\"\n >\n <Paperclip size={14} />\n </button>\n <button\n type=\"button\"\n className=\"chat-attach-btn\"\n onClick={async () => {\n if (!text.trim()) return;\n const enhanced = await enhancePrompt(text);\n if (enhanced !== text) setText(enhanced);\n }}\n title=\"Enhance prompt with AI\"\n aria-label=\"Enhance prompt\"\n >\n <Sparkles size={14} />\n </button>\n <button\n type=\"button\"\n className=\"chat-send-btn\"\n onClick={onSend}\n disabled={sending || !text.trim()}\n title=\"Send (Enter)\"\n aria-label=\"Send message\"\n >\n <Send size={16} />\n </button>\n </div>\n </div>\n );\n}","// src/components/chat/useChat.ts — shared chat state: messages, sessions,\n// send, polling, SSE, session-state tracking.\n//\n// v4.2.5 — Chat overhaul. Supports two parallel message streams:\n// - bizar (GET /api/chat)\n// - opencode (GET /api/opencode-sessions/:id/messages + SSE)\n//\n// v5.0.0 — bug #4 fix: `loadOpencodeSession` failure path now extracts\n// structured `code`, `cause`, `status`, and `suggestion` from the\n// server's ApiError.data envelope and exposes them via a new\n// `opencodeErrorInfo` field. `ChatInfoPanel` consumes that field to\n// render a structured \"Couldn't load session\" panel with the server's\n// `suggestion` verbatim, plus a Retry button.\n//\n// Changes from v3.22:\n// * SSE has automatic reconnect-with-backoff (1s → 30s, doubled on\n// each error up to a cap). Closes cleanly on unmount and on\n// session-switch (no leaked EventSource).\n// * Optimistic send uses a stable `msg_*` ID and is deduped when\n// opencode echoes the message back via SSE `message.updated`/`message.user.*`.\n// * `opencodeMessages` keeps server-provided IDs distinct from\n// optimistic IDs so the merge is unambiguous (server echo\n// replaces optimistic by matching messageID; otherwise both stick\n// around briefly until the user/session state catches up).\n// * `onSend` routes to `/opencode-sessions/:id/send` when an\n// opencode session is active, `/chat` otherwise. Errors surface\n// via toast + we always remove the optimistic placeholder.\n// * Sessions are managed by source:\n// - `bizarSessions` → local .jsonl store, POST /chat/sessions\n// - `opencodeSessions` → opencode serve, GET /opencode-sessions\n// The rail shows both side-by-side with a source indicator and\n// uses separate rename/delete endpoints per source.\n// * All async actions surface a single `busy: { create, send, rename, delete }`\n// so the rail can disable buttons properly.\n// * No `any` leaks — all SSE event shapes are typed via\n// `OpencodeSseEnvelope`.\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport type {\n ChatMessage,\n ChatResponse,\n ChatSession,\n Settings,\n Snapshot,\n} from '../../lib/types';\nimport { api, ApiError } from '../../lib/api';\nimport type { SessionState } from './ChatRail';\nimport type { AgentTreeNode } from './AgentNode';\n\nexport interface SessionDisplayState {\n /** idle / streaming / awaiting — derived from the latest SSE event. */\n state: SessionState;\n /** Number of unread (i.e. arrived while the user wasn't viewing) messages. */\n unread: number;\n /** Pinned? (cosmetic for now; pinned state lives in the parent.) */\n pinned: boolean;\n /** Sub-agent tree, if any. */\n tree?: { root: AgentTreeNode };\n}\n\n/** Per-source action busy flags. Used by rail/info-panel to disable buttons. */\nexport interface ChatBusyState {\n create: boolean;\n send: boolean;\n rename: boolean;\n delete: boolean;\n}\n\n/**\n * Structured error envelope surfaced by `loadOpencodeSession` when\n * the upstream `/api/opencode-sessions/:id/messages` call fails.\n *\n * - `code` mirrors the server's `error` field (`plugin_offline`,\n * `directory_unknown`, `opencode_error`, `bad_request`).\n * - `cause` is the server's structured cause (`network`, `timeout`,\n * `unknown`, or a raw ECONNREFUSED-style string).\n * - `suggestion` is the operator-friendly hint from the server\n * (e.g. \"Run `bizar doctor` for diagnostics\").\n * - `canRetry` is `true` whenever the failure is transient (network\n * error, upstream 502) — i.e. the user can fix it by trying again\n * after starting the plugin / waiting for opencode to come back.\n * False for hard failures (bad_request).\n */\nexport interface OpencodeErrorInfo {\n type: 'session_load_failed';\n code: string;\n message: string;\n cause?: string;\n status?: number;\n suggestion?: string | null;\n canRetry: boolean;\n}\n\n/** Shape of one opencode SSE envelope after unwrapping. */\ninterface OpencodeSseEnvelope {\n type: string;\n sessionID?: string;\n messageID?: string;\n /** Raw upstream payload (kept for forwarders that want full fidelity). */\n data?: unknown;\n}\n\n/**\n * The chat hook returns a stable bag of state + actions used by\n * both Chat.tsx (desktop) and MobileChat.tsx (mobile).\n *\n * The hook owns the lifecycle of the opencode SSE stream. Callers\n * should never instantiate their own EventSource for the same\n * session — they should always go through this hook.\n */\nexport function useChat(snapshot: Snapshot, settings: Settings, initialTaskId?: string | null) {\n // ── Bizar stream ────────────────────────────────────────────────────────────\n const [bizarMessages, setBizarMessages] = useState<ChatMessage[]>([]);\n const [sessions, setSessions] = useState<ChatSession[]>([]);\n const [sessionId, setSessionId] = useState('');\n const [loading, setLoading] = useState(true);\n const [busy, setBusy] = useState<ChatBusyState>({\n create: false,\n send: false,\n rename: false,\n delete: false,\n });\n\n // ── Opencode stream ─────────────────────────────────────────────────────────\n const [opencodeSessions, setOpencodeSessions] = useState<ChatSession[]>([]);\n const [opencodeMessages, setOpencodeMessages] = useState<ChatMessage[]>([]);\n const [activeSource, setActiveSource] = useState<'bizar' | 'opencode' | null>(null);\n const [activeOpencodeSessionId, setActiveOpencodeSessionId] = useState<string | null>(null);\n const [opencodeError, setOpencodeError] = useState<string | null>(null);\n const [opencodeSuggestion, setOpencodeSuggestion] = useState<string | null>(null);\n /**\n * Structured error info for the opencode session load path. New in\n * v5.0.0 — `ChatInfoPanel` consumes this to render its own error\n * section (in addition to the chat-thread-header error already\n * rendered by Chat.tsx). `null` when no error.\n */\n const [opencodeErrorInfo, setOpencodeErrorInfo] = useState<OpencodeErrorInfo | null>(null);\n\n // ── v3.22 — session-state, unread, tree per session ────────────────────────\n const [sessionStates, setSessionStates] = useState<Record<string, SessionDisplayState>>({});\n const [stickToBottom, setStickToBottom] = useState(true);\n const [newMessageCount, setNewMessageCount] = useState(0);\n\n // Set of messageIDs the user has already accepted (or that arrived\n // via SSE) — used by the opencode path to dedupe optimistic + echo.\n const [seenOpencodeMessages, setSeenOpencodeMessages] = useState<Set<string>>(new Set());\n\n // Backwards-compat alias (chat.sendSession delete etc. used `pinned` historically).\n const [pinned, setPinned] = useState<Set<number>>(new Set());\n\n const listRef = useRef<HTMLDivElement>(null);\n const toastRef = useRef<\n | {\n error: (msg: string) => void;\n success: (msg: string) => void;\n info: (msg: string) => void;\n warning: (msg: string) => void;\n }\n | null\n >(null);\n\n // ── SSE lifecycle refs ──────────────────────────────────────────────────────\n const opencodeEsRef = useRef<EventSource | null>(null);\n const sseReconnectAttemptRef = useRef(0);\n const sseReconnectTimerRef = useRef<number | null>(null);\n // The id of the session we're CURRENTLY streaming. When the user\n // switches sessions, this id changes and the SSE handler short-\n // circuits any in-flight appends.\n const sseCurrentSessionIdRef = useRef<string | null>(null);\n // Whether the SSE connection should be auto-reconnected on error.\n // Set to false on explicit disconnect (session switch / unmount).\n const sseAutoReconnectRef = useRef(false);\n\n /** Append `msg` to `opencodeMessages`, replacing any prior\n * optimistic entry whose content matches or whose ID matches. */\n const appendOpencodeMessage = useCallback((msg: ChatMessage) => {\n if (!msg) return;\n setOpencodeMessages((cur) => {\n // Existing exact-ID match: skip.\n if (msg.id && cur.some((m) => m.id === msg.id)) return cur;\n // Optimistic placeholder (starts with msg_) with the same role\n // and content: replace it. This is how opencode's echo of our\n // own message gets deduplicated against the optimistic copy.\n const idx = cur.findIndex(\n (m) =>\n typeof m.id === 'string' &&\n m.id.startsWith('msg_') &&\n m.role === msg.role &&\n (m.content || '') === (msg.content || ''),\n );\n if (idx >= 0) {\n const next = cur.slice();\n next[idx] = { ...msg, id: msg.id ?? cur[idx].id };\n return next;\n }\n return [...cur, msg];\n });\n }, []);\n\n /** Mark a messageID as seen — used to dedupe echo rounds. */\n const markSeen = useCallback((id: string | undefined) => {\n if (!id) return;\n setSeenOpencodeMessages((cur) => {\n if (cur.has(id)) return cur;\n const next = new Set(cur);\n next.add(id);\n return next;\n });\n }, []);\n\n // ── Helpers to mutate per-session state ────────────────────────────────────\n const setSessionDisplayState = useCallback(\n (id: string, patch: Partial<SessionDisplayState>) => {\n setSessionStates((cur) => {\n const prev: SessionDisplayState =\n cur[id] ?? { state: 'idle', unread: 0, pinned: false };\n return { ...cur, [id]: { ...prev, ...patch } };\n });\n },\n [],\n );\n\n const markSessionStreaming = useCallback(\n (id: string, streaming: boolean) => {\n setSessionDisplayState(id, { state: streaming ? 'streaming' : 'idle' });\n },\n [setSessionDisplayState],\n );\n\n /** ─── Toast binding (set by the consuming view via `setToast`) ──── */\n const setToast = useCallback(\n (toast: typeof toastRef.current) => {\n toastRef.current = toast;\n },\n [],\n );\n\n // ── Load bizar chat ─────────────────────────────────────────────────────────\n const loadChat = useCallback(\n async (sid?: string) => {\n try {\n const url = sid\n ? `/chat?session=${encodeURIComponent(sid)}`\n : '/chat?limit=200';\n const data = await api.get<ChatResponse>(url);\n setBizarMessages(data.messages || []);\n setSessions(data.sessions || []);\n } catch (err) {\n toastRef.current?.error(\n `Chat load failed: ${(err as Error).message}`,\n );\n } finally {\n setLoading(false);\n }\n },\n [],\n );\n\n const loadTaskChat = useCallback(async (taskId: string) => {\n setLoading(true);\n try {\n const data = await api.get<{\n sessionId?: string;\n messages: ChatMessage[];\n }>(`/tasks/${encodeURIComponent(taskId)}/chat`);\n setBizarMessages(data.messages || []);\n setSessionId(data.sessionId || taskId);\n } catch (err) {\n toastRef.current?.error(\n `Task chat load failed: ${(err as Error).message}`,\n );\n } finally {\n setLoading(false);\n }\n }, []);\n\n const refreshSessions = useCallback(async () => {\n try {\n const data = await api.get<{ sessions: ChatSession[] }>('/chat/sessions');\n setSessions(data.sessions || []);\n } catch {\n /* best-effort */\n }\n }, []);\n\n const refreshOpencodeSessions = useCallback(async () => {\n try {\n const data = await api.get<{ sessions: ChatSession[] }>(\n '/opencode-sessions',\n );\n // Enrich with source marker.\n const enriched: ChatSession[] = (data.sessions || []).map((s) => ({\n ...s,\n source: 'opencode' as const,\n title: s.title || s.id,\n }));\n setOpencodeSessions(enriched);\n } catch {\n /* best-effort */\n }\n }, []);\n\n // ── Opencode SSE connection ─────────────────────────────────────────────────\n //\n // Single source of truth for the EventSource lifecycle. Callers:\n // - `loadOpencodeSession(id)` — close any current, open a new one.\n // - `closeOpencodeSession()` — close and stop auto-reconnect.\n // - unmount — close and stop.\n //\n // On error / network drop, the EventSource triggers `onerror`. We\n // close the connection and schedule a reconnect (with exponential\n // backoff: 1s, 2s, 4s, …, capped at 30s) UNLESS `sseAutoReconnectRef`\n // is false (explicit disconnect).\n //\n const openSseForSession = useCallback(\n (id: string) => {\n // Close any existing connection first.\n if (opencodeEsRef.current) {\n try {\n opencodeEsRef.current.close();\n } catch {\n /* noop */\n }\n opencodeEsRef.current = null;\n }\n if (sseReconnectTimerRef.current !== null) {\n clearTimeout(sseReconnectTimerRef.current);\n sseReconnectTimerRef.current = null;\n }\n\n // Build the SSE URL. EventSource can't set headers, so we\n // pass the auth token as ?token= when set (loopback makes this\n // optional, but harmless).\n const tok = api.getToken();\n const url = tok\n ? `/api/opencode-sessions/${encodeURIComponent(id)}/stream?token=${encodeURIComponent(tok)}`\n : `/api/opencode-sessions/${encodeURIComponent(id)}/stream`;\n\n sseCurrentSessionIdRef.current = id;\n sseAutoReconnectRef.current = true;\n sseReconnectAttemptRef.current = 0;\n setOpencodeError(null);\n setOpencodeSuggestion(null);\n setOpencodeErrorInfo(null);\n\n const connect = () => {\n if (!sseAutoReconnectRef.current) return;\n if (sseCurrentSessionIdRef.current !== id) return;\n const es = new EventSource(url);\n opencodeEsRef.current = es;\n\n // Helper: dispatch one parsed envelope to local state.\n const onEnvelope = (evt: OpencodeSseEnvelope | null) => {\n if (!evt || !evt.type) return;\n // Drop events for OTHER sessions even though the proxy\n // already filters — defense in depth.\n if (evt.sessionID && evt.sessionID !== id) return;\n\n const ts = new Date().toISOString();\n const sidForState = id;\n\n if (\n evt.type === 'message.user.created' ||\n evt.type === 'message.user.updated'\n ) {\n const payload = evt.data as\n | { messageID?: string; data?: { content?: string } }\n | undefined;\n appendOpencodeMessage({\n id: payload?.messageID ?? evt.messageID,\n role: 'user',\n content: payload?.data?.content ?? '',\n ts,\n });\n markSeen(payload?.messageID ?? evt.messageID);\n return;\n }\n if (\n evt.type === 'message.assistant.created' ||\n evt.type === 'message.assistant.updated'\n ) {\n const payload = evt.data as\n | { messageID?: string; data?: { content?: string } }\n | undefined;\n appendOpencodeMessage({\n id: payload?.messageID ?? evt.messageID,\n role: 'assistant',\n content: payload?.data?.content ?? '',\n ts,\n });\n markSeen(payload?.messageID ?? evt.messageID);\n markSessionStreaming(sidForState, true);\n return;\n }\n if (evt.type === 'message.part.updated') {\n const payload = evt.data as\n | {\n part?: { type?: string; text?: string };\n messageID?: string;\n id?: string;\n }\n | undefined;\n const text = payload?.part?.text ?? '';\n if (!text) return;\n appendOpencodeMessage({\n id: payload?.id ?? payload?.messageID ?? evt.messageID,\n role: 'assistant',\n content: text,\n ts,\n });\n markSeen(payload?.id ?? payload?.messageID ?? evt.messageID);\n markSessionStreaming(sidForState, true);\n return;\n }\n if (\n evt.type === 'session.idle' ||\n evt.type === 'message.assistant.completed' ||\n evt.type === 'session.completed'\n ) {\n markSessionStreaming(sidForState, false);\n return;\n }\n if (\n evt.type === 'session.awaiting' ||\n evt.type === 'session.awaiting_input' ||\n evt.type === 'message.assistant.awaiting'\n ) {\n setSessionDisplayState(sidForState, { state: 'awaiting' });\n return;\n }\n if (evt.type === 'session.error') {\n setOpencodeError(\n (evt.data as { message?: string } | undefined)?.message ||\n 'opencode session error',\n );\n return;\n }\n // Other event types are intentionally ignored — the proxy\n // is canonical, we don't need to project every upstream\n // event into local state.\n };\n\n // Stable parser — the server may send both `{type: \"x\"}` and\n // `sync` envelopes with a `.<n>` version suffix. We accept both.\n const parseData = (raw: string | null | undefined):\n | OpencodeSseEnvelope\n | null => {\n if (!raw) return null;\n try {\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n // Direct shape: {type, properties: {...}}\n if (typeof parsed.type === 'string') {\n const props = parsed.properties as\n | Record<string, unknown>\n | undefined;\n const data = (parsed.data as Record<string, unknown> | undefined) ?? props;\n const sessionID = (\n typeof parsed.sessionID === 'string'\n ? parsed.sessionID\n : typeof props?.sessionID === 'string'\n ? (props.sessionID as string)\n : typeof data?.sessionID === 'string'\n ? (data.sessionID as string)\n : undefined\n );\n const messageID = (\n typeof parsed.messageID === 'string'\n ? parsed.messageID\n : typeof props?.messageID === 'string'\n ? (props.messageID as string)\n : typeof data?.messageID === 'string'\n ? (data.messageID as string)\n : undefined\n );\n let type = parsed.type as string;\n const m = /\\.\\d+$/.exec(type);\n if (m) type = type.slice(0, m.index);\n return { type, sessionID, messageID, data: parsed };\n }\n // Sync envelope: {type: \"sync\", syncEvent: {...}}\n if (\n parsed.type === 'sync' &&\n parsed.syncEvent &&\n typeof parsed.syncEvent === 'object'\n ) {\n const inner = parsed.syncEvent as Record<string, unknown>;\n let type = (inner.type as string) || '';\n const m = /\\.\\d+$/.exec(type);\n if (m) type = type.slice(0, m.index);\n const innerData = (inner.data as Record<string, unknown>) || {};\n return {\n type,\n sessionID:\n typeof innerData.sessionID === 'string'\n ? innerData.sessionID\n : undefined,\n messageID:\n typeof innerData.messageID === 'string'\n ? innerData.messageID\n : undefined,\n data: inner,\n };\n }\n return null;\n } catch {\n return null;\n }\n };\n\n // register handlers per named event type\n const namedTypes = [\n 'message.user.created',\n 'message.user.updated',\n 'message.assistant.created',\n 'message.assistant.updated',\n 'message.part.updated',\n 'session.idle',\n 'message.assistant.completed',\n 'session.completed',\n 'session.awaiting',\n 'session.awaiting_input',\n 'message.assistant.awaiting',\n 'session.error',\n ];\n for (const name of namedTypes) {\n es.addEventListener(name, (e: MessageEvent) => {\n onEnvelope(parseData((e as MessageEvent).data));\n });\n }\n\n es.onerror = () => {\n if (sseCurrentSessionIdRef.current !== id) return;\n if (!sseAutoReconnectRef.current) {\n try {\n es.close();\n } catch {\n /* noop */\n }\n opencodeEsRef.current = null;\n return;\n }\n try {\n es.close();\n } catch {\n /* noop */\n }\n opencodeEsRef.current = null;\n\n const nextAttempt = sseReconnectAttemptRef.current + 1;\n sseReconnectAttemptRef.current = nextAttempt;\n // Backoff: 1s, 2s, 4s, 8s, … capped at 30s.\n const delayMs = Math.min(30_000, 1000 * 2 ** (nextAttempt - 1));\n\n if (sseReconnectTimerRef.current !== null) {\n clearTimeout(sseReconnectTimerRef.current);\n }\n sseReconnectTimerRef.current = window.setTimeout(() => {\n sseReconnectTimerRef.current = null;\n if (\n sseAutoReconnectRef.current &&\n sseCurrentSessionIdRef.current === id\n ) {\n connect();\n }\n }, delayMs);\n };\n\n es.onopen = () => {\n // Successful (re)connect — reset backoff.\n sseReconnectAttemptRef.current = 0;\n setOpencodeError(null);\n setOpencodeSuggestion(null);\n setOpencodeErrorInfo(null);\n };\n };\n\n connect();\n },\n [appendOpencodeMessage, markSeen, markSessionStreaming, setSessionDisplayState],\n );\n\n const closeOpencodeSession = useCallback(() => {\n // Disable auto-reconnect first so the onerror handler short-circuits.\n sseAutoReconnectRef.current = false;\n sseCurrentSessionIdRef.current = null;\n if (sseReconnectTimerRef.current !== null) {\n clearTimeout(sseReconnectTimerRef.current);\n sseReconnectTimerRef.current = null;\n }\n if (opencodeEsRef.current) {\n try {\n opencodeEsRef.current.close();\n } catch {\n /* noop */\n }\n opencodeEsRef.current = null;\n }\n setOpencodeMessages([]);\n setSeenOpencodeMessages(new Set());\n setOpencodeError(null);\n setOpencodeSuggestion(null);\n setOpencodeErrorInfo(null);\n setActiveOpencodeSessionId(null);\n setActiveSource('bizar');\n }, []);\n\n const loadOpencodeSession = useCallback(\n async (id: string) => {\n // Close any previous opencode session cleanly.\n sseAutoReconnectRef.current = false;\n if (opencodeEsRef.current) {\n try {\n opencodeEsRef.current.close();\n } catch {\n /* noop */\n }\n opencodeEsRef.current = null;\n }\n if (sseReconnectTimerRef.current !== null) {\n clearTimeout(sseReconnectTimerRef.current);\n sseReconnectTimerRef.current = null;\n }\n\n setOpencodeError(null);\n setOpencodeSuggestion(null);\n setOpencodeErrorInfo(null);\n setLoading(true);\n\n try {\n const data = await api.get<{ messages: ChatMessage[] }>(\n `/opencode-sessions/${encodeURIComponent(id)}/messages`,\n );\n setOpencodeMessages(data.messages || []);\n } catch (err) {\n const msg = (err as Error).message;\n // v5.0.0 — bug #4: surface the server's structured error\n // envelope so the UI can render a useful suggestion instead\n // of the raw upstream error string. The server returns\n // `{error, message, cause?, status?, suggestion?}` for every\n // non-2xx response (see serve-info.mjs + routes/opencode-\n // session-detail.mjs).\n let info: OpencodeErrorInfo | null = null;\n if (err instanceof ApiError && err.data && typeof err.data === 'object') {\n const d = err.data as {\n error?: string;\n message?: string;\n cause?: string;\n status?: number;\n suggestion?: string;\n };\n // canRetry: 503/502 are transient (plugin offline /\n // upstream hiccup); the operator can fix the plugin and\n // try again. 400 is a hard bad_request — retrying with the\n // same payload won't help.\n const code = d.error || 'unknown';\n const canRetry =\n code !== 'bad_request' &&\n code !== 'too_many_subscribers' &&\n code !== 'unauthorized';\n info = {\n type: 'session_load_failed',\n code,\n message: d.message || msg,\n cause: d.cause,\n status: d.status,\n suggestion: d.suggestion ?? null,\n canRetry,\n };\n } else if (err instanceof ApiError) {\n info = {\n type: 'session_load_failed',\n code: 'unknown',\n message: msg,\n suggestion: null,\n canRetry: true,\n };\n }\n // Legacy fields — kept so the existing Chat.tsx thread-header\n // error display continues to work unchanged.\n setOpencodeError(\n info\n ? `Opencode session load failed (${info.code}): ${info.message}`\n : `Opencode session load failed: ${msg}`,\n );\n setOpencodeSuggestion(info?.suggestion ?? null);\n setOpencodeErrorInfo(info);\n toastRef.current?.error(\n info\n ? `Opencode session load failed: ${info.message}`\n : `Opencode session load failed: ${msg}`,\n );\n setOpencodeMessages([]);\n } finally {\n setLoading(false);\n }\n\n setActiveOpencodeSessionId(id);\n setActiveSource('opencode');\n markSessionStreaming(id, false);\n openSseForSession(id);\n },\n [markSessionStreaming, openSseForSession],\n );\n\n // ── Select a bizar session ──────────────────────────────────────────────────\n const selectBizarSession = useCallback(\n (sid: string) => {\n closeOpencodeSession();\n setSessionId(sid);\n setActiveSource('bizar');\n loadChat(sid);\n // Reset unread for the session being opened.\n setSessionDisplayState(sid, { unread: 0 });\n },\n [closeOpencodeSession, loadChat, setSessionDisplayState],\n );\n\n // ── Create session (opencode primary; bizar fallback) ──────────────────────\n const onCreateSession = useCallback(async (): Promise<{\n ok: boolean;\n source?: 'bizar' | 'opencode';\n id?: string;\n }> => {\n if (busy.create) return { ok: false };\n const agent = settings.defaultAgent || 'odin';\n setBusy((b) => ({ ...b, create: true }));\n try {\n // Try to create a real opencode session first. This is the\n // primary create path: it gives the user a full conversation\n // roundtrip with SSE streaming, abort, etc.\n try {\n const created = await api.post<{\n id: string;\n title: string;\n agent: string;\n directory: string;\n createdAt: number;\n }>('/opencode-sessions/new', { agent });\n await refreshOpencodeSessions();\n // Open the newly created session right away so the user is\n // dropped into a fresh empty thread, not stuck on the\n // previous one.\n await loadOpencodeSession(created.id);\n toastRef.current?.success(`Session ${created.id} created.`);\n return { ok: true, source: 'opencode', id: created.id };\n } catch (opencodeErr) {\n // Opencode plugin offline (503) — fall back to the local\n // per-project .jsonl session. Otherwise, surface the error.\n if (!(opencodeErr instanceof ApiError) || opencodeErr.status !== 503) {\n toastRef.current?.error(\n `Create failed: ${(opencodeErr as Error).message}`,\n );\n return { ok: false };\n }\n }\n\n // Fallback: local jsonl session.\n if (!snapshot.activeProject) {\n toastRef.current?.error(\n 'Pick a project in Overview to scope chat sessions.',\n );\n return { ok: false };\n }\n const created = await api.post<ChatSession>('/chat/sessions', {});\n setSessionId(created.id);\n setBizarMessages([]);\n setPinned(new Set());\n setSessionDisplayState(created.id, { state: 'idle', unread: 0 });\n await loadChat(created.id);\n refreshSessions().catch(() => {});\n toastRef.current?.success(`Session ${created.id} created.`);\n return { ok: true, source: 'bizar', id: created.id };\n } catch (err) {\n toastRef.current?.error(`Create failed: ${(err as Error).message}`);\n return { ok: false };\n } finally {\n setBusy((b) => ({ ...b, create: false }));\n }\n }, [\n busy.create,\n settings.defaultAgent,\n snapshot.activeProject,\n refreshOpencodeSessions,\n loadOpencodeSession,\n loadChat,\n refreshSessions,\n setSessionDisplayState,\n ]);\n\n // ── Send message ────────────────────────────────────────────────────────────\n const onSend = useCallback(\n async (\n message: string,\n agent: string,\n model: string,\n attachments?: string[],\n ): Promise<{ ok: boolean }> => {\n if (busy.send) return { ok: false };\n const trimmed = message.trim();\n if (!trimmed) return { ok: false };\n\n if (activeSource === 'opencode' && activeOpencodeSessionId) {\n const sid = activeOpencodeSessionId;\n const optimisticId = `msg_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;\n const optimistic: ChatMessage = {\n id: optimisticId,\n role: 'user',\n content: trimmed,\n agent,\n ts: new Date().toISOString(),\n };\n appendOpencodeMessage(optimistic);\n markSeen(optimisticId);\n setBusy((b) => ({ ...b, send: true }));\n try {\n await api.post<{ ok: boolean; messageId: string }>(\n `/opencode-sessions/${encodeURIComponent(sid)}/send`,\n {\n message: trimmed,\n agent,\n },\n );\n markSessionStreaming(sid, true);\n return { ok: true };\n } catch (err) {\n // Remove optimistic on failure.\n setOpencodeMessages((cur) => cur.filter((m) => m.id !== optimisticId));\n toastRef.current?.error(\n `Send failed: ${(err as Error).message}`,\n );\n return { ok: false };\n } finally {\n setBusy((b) => ({ ...b, send: false }));\n }\n }\n\n // Bizar fallback.\n const optimistic: ChatMessage = {\n role: 'user',\n content: trimmed,\n agent,\n ts: new Date().toISOString(),\n };\n setBizarMessages((cur) => [...cur, optimistic]);\n setBusy((b) => ({ ...b, send: true }));\n markSessionStreaming(sessionId, true);\n try {\n const response = await api.post<{ messages?: ChatMessage[] }>(\n '/chat',\n {\n message: trimmed,\n agent,\n model,\n attachments: attachments || [],\n },\n );\n if (response?.messages) {\n for (const msg of response.messages) {\n if (msg.role === 'assistant') {\n setBizarMessages((cur) => {\n const exists = cur.some((m) => m.ts === msg.ts);\n return exists ? cur : [...cur, msg];\n });\n }\n }\n } else {\n toastRef.current?.info(\n 'Still processing… the response will appear shortly.',\n );\n }\n return { ok: true };\n } catch (err) {\n setBizarMessages((cur) => cur.filter((m) => m !== optimistic));\n toastRef.current?.error(\n `Send failed: ${(err as Error).message}`,\n );\n return { ok: false };\n } finally {\n setBusy((b) => ({ ...b, send: false }));\n markSessionStreaming(sessionId, false);\n }\n },\n [\n activeSource,\n activeOpencodeSessionId,\n appendOpencodeMessage,\n markSeen,\n markSessionStreaming,\n sessionId,\n busy.send,\n ],\n );\n\n const onRegenerate = useCallback(\n async (messageId: string) => {\n if (!sessionId) {\n toastRef.current?.error('No active session to regenerate.');\n return;\n }\n try {\n await api.post('/chat/regenerate', { sessionId, messageId });\n await loadChat(sessionId);\n } catch (err) {\n toastRef.current?.error(\n `Regenerate failed: ${(err as Error).message}`,\n );\n }\n },\n [sessionId, loadChat],\n );\n\n const deleteMessage = useCallback(\n (idx: number) => {\n if (activeSource === 'opencode') {\n setOpencodeMessages((cur) => cur.filter((_, i) => i !== idx));\n } else {\n setBizarMessages((cur) => cur.filter((_, i) => i !== idx));\n }\n },\n [activeSource],\n );\n\n const togglePin = useCallback((idx: number) => {\n setPinned((cur) => {\n const next = new Set(cur);\n if (next.has(idx)) next.delete(idx);\n else next.add(idx);\n return next;\n });\n }, []);\n\n const copyMessage = useCallback((m: ChatMessage) => {\n const text = m.content || m.message || '';\n try {\n navigator.clipboard?.writeText(text).then(\n () => toastRef.current?.success('Copied.'),\n () => toastRef.current?.error('Copy failed.'),\n );\n } catch {\n toastRef.current?.error('Copy failed.');\n }\n }, []);\n\n // ── Jump-to-latest ──────────────────────────────────────────────────────────\n const handleScroll = useCallback(() => {\n const el = listRef.current;\n if (!el) return;\n const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;\n const atBottom = distFromBottom < 32;\n setStickToBottom(atBottom);\n if (atBottom) setNewMessageCount(0);\n }, []);\n\n const jumpToLatest = useCallback(() => {\n const el = listRef.current;\n if (!el) return;\n el.scrollTop = el.scrollHeight;\n setStickToBottom(true);\n setNewMessageCount(0);\n }, []);\n\n // ── Session rename / delete (rail row menu + info panel actions) ────────────\n const renameSession = useCallback(\n async (id: string, title: string): Promise<boolean> => {\n if (busy.rename) return false;\n setBusy((b) => ({ ...b, rename: true }));\n try {\n const trimmed = title.trim();\n if (!trimmed) {\n toastRef.current?.error('Title cannot be empty.');\n return false;\n }\n // Try the opencode path first if this id is in the opencode\n // session list, otherwise the local path. We dispatch by\n // inspecting the in-memory set (avoids a round trip).\n const isOpencode = opencodeSessions.some((s) => s.id === id);\n if (isOpencode) {\n await api.patch<{ id: string; title: string }>(\n `/opencode-sessions/${encodeURIComponent(id)}`,\n { title: trimmed },\n );\n setOpencodeSessions((cur) =>\n cur.map((s) => (s.id === id ? { ...s, title: trimmed } : s)),\n );\n } else {\n await api.post(\n `/chat/sessions/${encodeURIComponent(id)}/rename`,\n { title: trimmed },\n );\n setSessions((cur) =>\n cur.map((s) => (s.id === id ? { ...s, title: trimmed } : s)),\n );\n }\n toastRef.current?.success('Renamed.');\n return true;\n } catch (err) {\n toastRef.current?.error(\n `Rename failed: ${(err as Error).message}`,\n );\n return false;\n } finally {\n setBusy((b) => ({ ...b, rename: false }));\n }\n },\n [busy.rename, opencodeSessions],\n );\n\n const deleteSession = useCallback(\n async (id: string): Promise<boolean> => {\n if (busy.delete) return false;\n setBusy((b) => ({ ...b, delete: true }));\n try {\n const isOpencode = opencodeSessions.some((s) => s.id === id);\n if (isOpencode) {\n await api.del(`/opencode-sessions/${encodeURIComponent(id)}`);\n setOpencodeSessions((cur) => cur.filter((s) => s.id !== id));\n if (activeOpencodeSessionId === id) {\n closeOpencodeSession();\n }\n } else {\n await api.del(`/chat/sessions/${encodeURIComponent(id)}`);\n setSessions((cur) => cur.filter((s) => s.id !== id));\n if (sessionId === id) {\n setSessionId('');\n setBizarMessages([]);\n }\n }\n setSessionStates((cur) => {\n const next = { ...cur };\n delete next[id];\n return next;\n });\n toastRef.current?.success('Session deleted.');\n return true;\n } catch (err) {\n toastRef.current?.error(\n `Delete failed: ${(err as Error).message}`,\n );\n return false;\n } finally {\n setBusy((b) => ({ ...b, delete: false }));\n }\n },\n [\n busy.delete,\n opencodeSessions,\n activeOpencodeSessionId,\n closeOpencodeSession,\n sessionId,\n ],\n );\n\n // ── Auto-scroll on new messages ─────────────────────────────────────────────\n useEffect(() => {\n if (listRef.current) {\n if (stickToBottom) {\n listRef.current.scrollTop = listRef.current.scrollHeight;\n } else {\n setNewMessageCount((n) => n + 1);\n }\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [bizarMessages, opencodeMessages]);\n\n // ── Cleanup SSE on unmount ──────────────────────────────────────────────────\n useEffect(() => {\n return () => {\n sseAutoReconnectRef.current = false;\n sseCurrentSessionIdRef.current = null;\n if (sseReconnectTimerRef.current !== null) {\n clearTimeout(sseReconnectTimerRef.current);\n sseReconnectTimerRef.current = null;\n }\n if (opencodeEsRef.current) {\n try {\n opencodeEsRef.current.close();\n } catch {\n /* noop */\n }\n opencodeEsRef.current = null;\n }\n };\n }, []);\n\n // ── Initial load ────────────────────────────────────────────────────────────\n useEffect(() => {\n if (initialTaskId) {\n loadTaskChat(initialTaskId);\n } else {\n loadChat();\n }\n refreshOpencodeSessions();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // ── Listen for bizar:setChatTask events ────────────────────────────────────\n useEffect(() => {\n const handler = (e: Event) => {\n const taskId = (e as CustomEvent<{ taskId: string }>).detail?.taskId;\n if (taskId) loadTaskChat(taskId);\n };\n window.addEventListener('bizar:setChatTask', handler);\n return () => window.removeEventListener('bizar:setChatTask', handler);\n }, [loadTaskChat]);\n\n // ── WebSocket for live bizar updates ────────────────────────────────────────\n useEffect(() => {\n let closed = false;\n import('../../lib/ws').then(({ Ws }) => {\n if (closed) return;\n const ws = new Ws();\n ws.on((msg: { type: string; message?: ChatMessage }) => {\n if (msg.type !== 'chat:message') return;\n const next = msg.message;\n if (!next) return;\n setBizarMessages((cur) => {\n const exists = cur.some((m) => m.ts === next.ts);\n return exists ? cur : [...cur, next];\n });\n });\n });\n return () => {\n closed = true;\n };\n }, []);\n\n return {\n // ── State ────────────────────────────────────────────────────────────────\n messages:\n activeSource === 'opencode' ? opencodeMessages : bizarMessages,\n bizarMessages,\n opencodeMessages,\n sessions,\n opencodeSessions,\n sessionId,\n setSessionId,\n loading,\n sending: busy.send,\n pinned,\n listRef,\n // ── Opencode state ──────────────────────────────────────────────────────\n activeSource,\n activeOpencodeSessionId,\n opencodeError,\n opencodeSuggestion,\n /**\n * v5.0.0 — bug #4: structured error info consumed by\n * `ChatInfoPanel`. `null` when no error is active.\n */\n opencodeErrorInfo,\n /**\n * v5.0.0 — bug #4: retry helper exposed for `ChatInfoPanel`'s\n * Retry button. Calls `loadOpencodeSession` on the currently\n * active opencode session (or no-op if none is active).\n */\n retryOpencodeSession: () => {\n if (activeOpencodeSessionId) void loadOpencodeSession(activeOpencodeSessionId);\n },\n // ── v3.22 — per-session display state ──────────────────────────────────\n sessionStates,\n busy,\n getSessionDisplay: (s: ChatSession) => ({\n ...s,\n state: sessionStates[s.id]?.state ?? 'idle',\n unread: sessionStates[s.id]?.unread ?? 0,\n pinned: sessionStates[s.id]?.pinned ?? false,\n tree: sessionStates[s.id]?.tree,\n }),\n // ── Jump-to-latest ──────────────────────────────────────────────────────\n stickToBottom,\n newMessageCount,\n handleScroll,\n jumpToLatest,\n // ── Toast integration ──────────────────────────────────────────────────\n setToast,\n // ── Actions ─────────────────────────────────────────────────────────────\n loadChat,\n loadTaskChat,\n refreshSessions,\n refreshOpencodeSessions,\n loadOpencodeSession,\n closeOpencodeSession,\n selectBizarSession,\n onCreateSession,\n onSend,\n onRegenerate,\n deleteMessage,\n togglePin,\n copyMessage,\n renameSession,\n deleteSession,\n seenOpencodeMessages,\n };\n}\n","// src/components/chat/useSlashCommands.ts — filter slash-command suggestions from mods.\n\nimport { useMemo, useState } from 'react';\nimport type { Mod, Snapshot } from '../../lib/types';\n\nexport type SlashCommand = { cmd: string; desc: string; mod?: string };\n\nconst BUILTIN_COMMANDS: SlashCommand[] = [\n { cmd: '/visual-plan [on|off|status]', desc: 'Toggle or view visual plan mode' },\n { cmd: '/plan new <slug> [template]', desc: 'Create a new plan' },\n { cmd: '/plan list', desc: 'List all plans' },\n { cmd: '/plan open <slug>', desc: 'Open a plan in the viewer' },\n { cmd: '/plan status <slug> <status>', desc: 'Set plan status' },\n { cmd: '/plan get <slug>', desc: 'Fetch plan canvas' },\n { cmd: '/plan add <slug> --title T --type kind', desc: 'Add element to a plan' },\n { cmd: '/plan update <slug> <id>', desc: 'Update a plan element' },\n { cmd: '/plan delete <slug> <id>', desc: 'Delete a plan element' },\n { cmd: '/plan comment <slug> [id] \"text\"', desc: 'Add a comment' },\n { cmd: '/plan comments <slug> [id]', desc: 'Read plan comments' },\n { cmd: '/bizar', desc: 'Launch Bizar dashboard' },\n { cmd: '/bizar <args>', desc: 'Route via Bizar menu' },\n { cmd: '/audit', desc: 'Run security audit' },\n { cmd: '/explain <q>', desc: 'Read-only code Q&A' },\n { cmd: '/init', desc: 'Initialize .bizar/ in this project' },\n { cmd: '/learn', desc: 'Extract patterns from session' },\n { cmd: '/pr-review', desc: 'PR review' },\n { cmd: '/help | /commands', desc: 'Show all Bizar commands' },\n];\n\nexport function useSlashCommands(snapshot: Snapshot) {\n const [query, setQuery] = useState('');\n\n const allCommands = useMemo<SlashCommand[]>(() => [\n ...BUILTIN_COMMANDS,\n ...(snapshot.mods || []).flatMap((m: Mod) =>\n m.entry?.command ? [{ cmd: `/${m.id}`, desc: m.description || m.name, mod: m.id }] : [],\n ),\n ], [snapshot.mods]);\n\n const suggestions = useMemo(() => {\n if (!query.startsWith('/') || query.includes(' ')) return [];\n const q = query.toLowerCase();\n return allCommands.filter((c) => c.cmd.toLowerCase().startsWith(q)).slice(0, 6);\n }, [query, allCommands]);\n\n return { allCommands, suggestions, setQuery };\n}"],"file":"assets/useSlashCommands-C24-O1e7.js"}
|
|
@@ -29,17 +29,17 @@
|
|
|
29
29
|
}
|
|
30
30
|
})();
|
|
31
31
|
</script>
|
|
32
|
-
<script type="module" crossorigin src="/assets/main-
|
|
32
|
+
<script type="module" crossorigin src="/assets/main-DHXPuddY.js"></script>
|
|
33
33
|
<link rel="modulepreload" crossorigin href="/assets/react-vendor-Dn4wqh4Z.js">
|
|
34
|
-
<link rel="modulepreload" crossorigin href="/assets/icons-
|
|
35
|
-
<link rel="modulepreload" crossorigin href="/assets/mobile-layout-
|
|
36
|
-
<link rel="modulepreload" crossorigin href="/assets/Toast-
|
|
37
|
-
<link rel="modulepreload" crossorigin href="/assets/EnvVarsSection-
|
|
34
|
+
<link rel="modulepreload" crossorigin href="/assets/icons-Btipv2pp.js">
|
|
35
|
+
<link rel="modulepreload" crossorigin href="/assets/mobile-layout-BqHxVb6_.js">
|
|
36
|
+
<link rel="modulepreload" crossorigin href="/assets/Toast-D9nv5N6m.js">
|
|
37
|
+
<link rel="modulepreload" crossorigin href="/assets/EnvVarsSection-DoIzqrlb.js">
|
|
38
38
|
<link rel="modulepreload" crossorigin href="/assets/vendor-CeHGtduv.js">
|
|
39
39
|
<link rel="modulepreload" crossorigin href="/assets/markdown-C6mXtQxD.js">
|
|
40
|
-
<link rel="modulepreload" crossorigin href="/assets/useSlashCommands-
|
|
40
|
+
<link rel="modulepreload" crossorigin href="/assets/useSlashCommands-C24-O1e7.js">
|
|
41
41
|
<link rel="stylesheet" crossorigin href="/assets/mobile-layout-CJnZNLy3.css">
|
|
42
|
-
<link rel="stylesheet" crossorigin href="/assets/main-
|
|
42
|
+
<link rel="stylesheet" crossorigin href="/assets/main-UG4jKK-F.css">
|
|
43
43
|
</head>
|
|
44
44
|
<body>
|
|
45
45
|
<div id="root"></div>
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
13
13
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
14
14
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
|
15
|
-
<script type="module" crossorigin src="/assets/mobile-
|
|
15
|
+
<script type="module" crossorigin src="/assets/mobile-H5KH9cTL.js"></script>
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/assets/react-vendor-Dn4wqh4Z.js">
|
|
17
|
-
<link rel="modulepreload" crossorigin href="/assets/icons-
|
|
18
|
-
<link rel="modulepreload" crossorigin href="/assets/mobile-layout-
|
|
17
|
+
<link rel="modulepreload" crossorigin href="/assets/icons-Btipv2pp.js">
|
|
18
|
+
<link rel="modulepreload" crossorigin href="/assets/mobile-layout-BqHxVb6_.js">
|
|
19
19
|
<link rel="stylesheet" crossorigin href="/assets/mobile-layout-CJnZNLy3.css">
|
|
20
20
|
</head>
|
|
21
21
|
<body>
|
package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"4.1.9","results":[[":tests/lib/utils.test.ts",{"duration":
|
|
1
|
+
{"version":"4.1.9","results":[[":tests/lib/utils.test.ts",{"duration":20.025207000000023,"failed":false}],[":tests/components/Button.test.tsx",{"duration":127.51585299999999,"failed":false}],[":tests/components/Card.test.tsx",{"duration":28.72670699999992,"failed":false}],[":tests/hooks/useModal.test.tsx",{"duration":148.1413090000001,"failed":false}],[":tests/components/Toast.test.tsx",{"duration":171.57751199999996,"failed":false}],[":tests/components/Modal.test.tsx",{"duration":185.125987,"failed":false}],[":tests/lib/i18n.test.ts",{"duration":2.943632999999977,"failed":false}],[":tests/components/StatusBadge.test.tsx",{"duration":45.27787599999999,"failed":false}],[":tests/hooks/useToast.test.tsx",{"duration":91.46383000000003,"failed":false}],[":tests/components/Spinner.test.tsx",{"duration":71.959069,"failed":false}],[":tests/backup-restore.test.tsx",{"duration":218.97713900000008,"failed":false}],[":tests/a11y.test.tsx",{"duration":1595.1776809999997,"failed":false}],[":tests/lib/search-fuzzy.test.ts",{"duration":5.1588409999999385,"failed":false}],[":tests/components/settings-search.test.tsx",{"duration":802.290593,"failed":false}],[":tests/memory-graph-view.test.tsx",{"duration":60.55693100000008,"failed":false}],[":tests/components/workspace-selector.test.tsx",{"duration":148.12282299999993,"failed":false}],[":tests/components/screenshot-ocr.test.tsx",{"duration":34.32638099999997,"failed":false}],[":tests/voice-recorder.test.tsx",{"duration":140.43329000000006,"failed":false}],[":tests/autosave.test.tsx",{"duration":296.6411939999999,"failed":false}],[":tests/settings-layout.test.tsx",{"duration":248.8849059999999,"failed":false}],[":tests/settings-nav.test.tsx",{"duration":329.73130000000003,"failed":false}],[":tests/components/doctor-panel.test.tsx",{"duration":71.16086100000007,"failed":false}],[":tests/chat-composer.test.tsx",{"duration":326.8492749999998,"failed":false}],[":tests/settings-mode-wiring.test.tsx",{"duration":366.22139200000015,"failed":false}],[":tests/minimax-bar.test.ts",{"duration":0,"failed":true}],[":tests/minimax-bar.test.tsx",{"duration":34.713302,"failed":false}],[":tests/components/plugin-permissions.test.tsx",{"duration":104.11512800000003,"failed":false}],[":tests/eval-web-ui.test.tsx",{"duration":156.84043499999996,"failed":false}],[":tests/components/marketplace-plugin-card.test.tsx",{"duration":248.873744,"failed":false}],[":tests/a11y/components.test.tsx",{"duration":155.483838,"failed":false}],[":tests/a11y/forms.test.tsx",{"duration":108.19375200000013,"failed":true}],[":tests/a11y/navigation.test.tsx",{"duration":235.64667499999973,"failed":true}],[":tests/mobile-settings.test.tsx",{"duration":287.317644,"failed":false}],[":tests/mobile-overview.test.tsx",{"duration":300.5304759999999,"failed":false}],[":tests/mobile-layout.test.tsx",{"duration":358.853343,"failed":false}],[":tests/mobile-tasks.test.tsx",{"duration":266.082682,"failed":false}],[":tests/mobile-chat.test.tsx",{"duration":46.805959999999914,"failed":false}],[":tests/mobile-misc.test.tsx",{"duration":388.0453399999999,"failed":false}],[":tests/BackgroundAgents.test.tsx",{"duration":447.60030400000005,"failed":false}],[":tests/views/topbar.test.tsx",{"duration":640.7248260000001,"failed":false}],[":tests/views/Settings.test.tsx",{"duration":276.8023210000001,"failed":false}],[":tests/views/Marketplace.test.tsx",{"duration":336.0940639999999,"failed":false}],[":tests/views/sidebar.test.tsx",{"duration":170.189707,"failed":false}],[":tests/views/Memory.test.tsx",{"duration":528.6620620000001,"failed":false}]]}
|