From f5d1abe978172d0ce2479c69d3a07138fd22f77c Mon Sep 17 00:00:00 2001 From: Chris Scott <99081550+chriswritescode-dev@users.noreply.github.com> Date: Thu, 23 Apr 2026 00:51:07 -0400 Subject: [PATCH 1/3] loop: simple-chat-mode completed after 1 iterations --- frontend/src/api/types/settings.ts | 5 +- .../src/components/message/FileToolRender.tsx | 6 +- .../components/message/MessagePart.test.tsx | 300 ++++++++++++++++++ .../src/components/message/MessagePart.tsx | 9 + .../components/settings/GeneralSettings.tsx | 90 +++--- shared/src/schemas/settings.ts | 10 +- 6 files changed, 376 insertions(+), 44 deletions(-) diff --git a/frontend/src/api/types/settings.ts b/frontend/src/api/types/settings.ts index c3dc0cd6..7d35ab3d 100644 --- a/frontend/src/api/types/settings.ts +++ b/frontend/src/api/types/settings.ts @@ -48,9 +48,10 @@ export interface UserPreferences { defaultModel?: string defaultAgent?: string autoScroll: boolean - showReasoning: boolean - expandToolCalls: boolean expandDiffs: boolean + expandToolCalls: boolean + showReasoning: boolean + simpleChatMode: boolean leaderKey?: string directShortcuts?: string[] keyboardShortcuts: Record diff --git a/frontend/src/components/message/FileToolRender.tsx b/frontend/src/components/message/FileToolRender.tsx index b77c7072..ea684803 100644 --- a/frontend/src/components/message/FileToolRender.tsx +++ b/frontend/src/components/message/FileToolRender.tsx @@ -66,10 +66,12 @@ export function FileToolRender({ part, filediff, filePath, content, toolName, on const { preferences } = useSettings() const isReadTool = toolName === 'Read' const isEditTool = toolName === 'Edit' + const isWriteTool = toolName === 'Write' const hasExpandableContent = !isReadTool && (filediff || content) - const defaultExpanded = isEditTool - ? (preferences?.expandDiffs ?? true) + const isFileMutatingTool = isEditTool || isWriteTool + const defaultExpanded = isFileMutatingTool + ? (preferences?.expandDiffs ?? true) : (preferences?.expandToolCalls ?? false) const [expanded, setExpanded] = useState(defaultExpanded) diff --git a/frontend/src/components/message/MessagePart.test.tsx b/frontend/src/components/message/MessagePart.test.tsx index 99f5b5d5..acce39e7 100644 --- a/frontend/src/components/message/MessagePart.test.tsx +++ b/frontend/src/components/message/MessagePart.test.tsx @@ -5,12 +5,17 @@ import type { MessagePart as MessagePartType } from '@/api/types' const mocks = vi.hoisted(() => ({ useTTS: vi.fn(), + useSettings: vi.fn(), })) vi.mock('@/hooks/useTTS', () => ({ useTTS: mocks.useTTS, })) +vi.mock('@/hooks/useSettings', () => ({ + useSettings: mocks.useSettings, +})) + interface MockTTSReturn { speakMessage: ReturnType stop: ReturnType @@ -20,6 +25,21 @@ interface MockTTSReturn { isEnabled: boolean } +interface MockSettingsReturn { + preferences: { + simpleChatMode: boolean + showReasoning: boolean + expandToolCalls: boolean + expandDiffs: boolean + autoScroll: boolean + theme: 'dark' | 'light' | 'system' + mode: 'plan' | 'build' + } | undefined + isLoading: boolean + updateSettings: ReturnType + isUpdating: boolean +} + describe('MessagePart', () => { const mockSpeakMessage = vi.fn() const mockStop = vi.fn() @@ -27,6 +47,20 @@ describe('MessagePart', () => { beforeEach(() => { mockSpeakMessage.mockClear() mockStop.mockClear() + mocks.useSettings.mockReturnValue({ + preferences: { + simpleChatMode: false, + showReasoning: false, + expandToolCalls: false, + expandDiffs: true, + autoScroll: true, + theme: 'dark' as const, + mode: 'build' as const, + }, + isLoading: false, + updateSettings: vi.fn(), + isUpdating: false, + }) }) const setup = (options: { @@ -47,6 +81,15 @@ describe('MessagePart', () => { mocks.useTTS.mockReturnValue(mockTTS) } + const setupSettings = (preferences: MockSettingsReturn['preferences']) => { + mocks.useSettings.mockReturnValue({ + preferences, + isLoading: false, + updateSettings: vi.fn(), + isUpdating: false, + }) + } + const createStepFinishPart = (messageID: string): MessagePartType => ({ type: 'step-finish', messageID, @@ -223,4 +266,261 @@ describe('MessagePart', () => { expect(screen.getByRole('button')).not.toHaveClass('bg-red-500/20') }) + + describe('simpleChatMode', () => { + const createToolPart = (): MessagePartType => ({ + type: 'tool', + tool: 'edit', + sessionID: 'test-session', + state: { + status: 'completed', + input: { filePath: '/test/file.txt' }, + time: { start: Date.now(), end: Date.now() + 100 }, + }, + }) + + const createPatchPart = (): MessagePartType => ({ + type: 'patch', + hash: 'abc123', + files: ['/test/file.txt'], + sessionID: 'test-session', + }) + + const createReasoningPart = (): MessagePartType => ({ + type: 'reasoning', + text: 'This is the reasoning text', + sessionID: 'test-session', + }) + + const createSnapshotPart = (): MessagePartType => ({ + type: 'snapshot', + snapshot: 'snapshot-data', + sessionID: 'test-session', + }) + + const createAgentPart = (): MessagePartType => ({ + type: 'agent', + name: 'test-agent', + sessionID: 'test-session', + }) + + const createTextPart = (): MessagePartType => ({ + type: 'text', + text: 'Hello, this is a text message', + sessionID: 'test-session', + }) + + it('renders null for tool part when simpleChatMode is true', () => { + setupSettings({ + simpleChatMode: true, + showReasoning: false, + expandToolCalls: false, + expandDiffs: true, + autoScroll: true, + theme: 'dark', + mode: 'build', + }) + + const part = createToolPart() + const { container } = render() + + expect(container.firstChild).toBeNull() + }) + + it('renders null for patch part when simpleChatMode is true', () => { + setupSettings({ + simpleChatMode: true, + showReasoning: false, + expandToolCalls: false, + expandDiffs: true, + autoScroll: true, + theme: 'dark', + mode: 'build', + }) + + const part = createPatchPart() + const { container } = render() + + expect(container.firstChild).toBeNull() + }) + + it('renders null for reasoning part when simpleChatMode is true', () => { + setupSettings({ + simpleChatMode: true, + showReasoning: true, + expandToolCalls: false, + expandDiffs: true, + autoScroll: true, + theme: 'dark', + mode: 'build', + }) + + const part = createReasoningPart() + const { container } = render() + + expect(container.firstChild).toBeNull() + }) + + it('renders null for snapshot part when simpleChatMode is true', () => { + setupSettings({ + simpleChatMode: true, + showReasoning: false, + expandToolCalls: false, + expandDiffs: true, + autoScroll: true, + theme: 'dark', + mode: 'build', + }) + + const part = createSnapshotPart() + const { container } = render() + + expect(container.firstChild).toBeNull() + }) + + it('renders null for agent part when simpleChatMode is true', () => { + setupSettings({ + simpleChatMode: true, + showReasoning: false, + expandToolCalls: false, + expandDiffs: true, + autoScroll: true, + theme: 'dark', + mode: 'build', + }) + + const part = createAgentPart() + const { container } = render() + + expect(container.firstChild).toBeNull() + }) + + it('renders text part when simpleChatMode is true', () => { + setupSettings({ + simpleChatMode: true, + showReasoning: false, + expandToolCalls: false, + expandDiffs: true, + autoScroll: true, + theme: 'dark', + mode: 'build', + }) + + const part = createTextPart() + render() + + expect(screen.getByText('Hello, this is a text message')).toBeInTheDocument() + }) + + it('renders tool part when simpleChatMode is false', () => { + setupSettings({ + simpleChatMode: false, + showReasoning: false, + expandToolCalls: false, + expandDiffs: true, + autoScroll: true, + theme: 'dark', + mode: 'build', + }) + + const part = createToolPart() + const { container } = render() + + expect(container.firstChild).not.toBeNull() + }) + + it('renders patch part when simpleChatMode is false', () => { + setupSettings({ + simpleChatMode: false, + showReasoning: false, + expandToolCalls: false, + expandDiffs: true, + autoScroll: true, + theme: 'dark', + mode: 'build', + }) + + const part = createPatchPart() + const { container } = render() + + expect(container.firstChild).not.toBeNull() + }) + + it('renders snapshot part when simpleChatMode is false', () => { + setupSettings({ + simpleChatMode: false, + showReasoning: false, + expandToolCalls: false, + expandDiffs: true, + autoScroll: true, + theme: 'dark', + mode: 'build', + }) + + const part = createSnapshotPart() + const { container } = render() + + expect(container.firstChild).not.toBeNull() + }) + + it('renders agent part when simpleChatMode is false', () => { + setupSettings({ + simpleChatMode: false, + showReasoning: false, + expandToolCalls: false, + expandDiffs: true, + autoScroll: true, + theme: 'dark', + mode: 'build', + }) + + const part = createAgentPart() + const { container } = render() + + expect(container.firstChild).not.toBeNull() + }) + }) + + describe('showReasoning', () => { + const createReasoningPart = (): MessagePartType => ({ + type: 'reasoning', + text: 'This is the reasoning text', + sessionID: 'test-session', + }) + + it('renders null for reasoning part when showReasoning is false', () => { + setupSettings({ + simpleChatMode: false, + showReasoning: false, + expandToolCalls: false, + expandDiffs: true, + autoScroll: true, + theme: 'dark', + mode: 'build', + }) + + const part = createReasoningPart() + const { container } = render() + + expect(container.firstChild).toBeNull() + }) + + it('renders reasoning part when showReasoning is true', () => { + setupSettings({ + simpleChatMode: false, + showReasoning: true, + expandToolCalls: false, + expandDiffs: true, + autoScroll: true, + theme: 'dark', + mode: 'build', + }) + + const part = createReasoningPart() + render() + + expect(screen.getByText('This is the reasoning text')).toBeInTheDocument() + expect(screen.getByText('Reasoning')).toBeInTheDocument() + }) + }) }) diff --git a/frontend/src/components/message/MessagePart.tsx b/frontend/src/components/message/MessagePart.tsx index 25be72b6..a5600d39 100644 --- a/frontend/src/components/message/MessagePart.tsx +++ b/frontend/src/components/message/MessagePart.tsx @@ -7,6 +7,7 @@ import { ToolCallPart } from './ToolCallPart' import { RetryPart } from './RetryPart' import { useTTS } from '@/hooks/useTTS' import { useMobile } from '@/hooks/useMobile' +import { useSettings } from '@/hooks/useSettings' import { CopyButton } from '@/components/ui/copy-button' type RetryPartType = components['schemas']['RetryPart'] @@ -100,6 +101,9 @@ function TTSButton({ messageId, content, className = "" }: TTSButtonProps) { } export const MessagePart = memo(function MessagePart({ part, role, allParts, partIndex, onFileClick, onChildSessionClick, messageTextContent }: MessagePartProps) { + const { preferences } = useSettings() + const simpleChatMode = preferences?.simpleChatMode ?? false + const showReasoning = preferences?.showReasoning ?? false const copyableContent = getCopyableContent(part, allParts) const isMobile = useMobile() @@ -113,10 +117,13 @@ export const MessagePart = memo(function MessagePart({ part, role, allParts, par } return case 'patch': + if (simpleChatMode) return null return case 'tool': + if (simpleChatMode) return null return case 'reasoning': + if (simpleChatMode || !showReasoning) return null return (
@@ -128,12 +135,14 @@ export const MessagePart = memo(function MessagePart({ part, role, allParts, par
) case 'snapshot': + if (simpleChatMode) return null return (
Snapshot: {part.snapshot}
) case 'agent': + if (simpleChatMode) return null return (
Agent: {part.name}
diff --git a/frontend/src/components/settings/GeneralSettings.tsx b/frontend/src/components/settings/GeneralSettings.tsx index cbc0678c..69e22b78 100644 --- a/frontend/src/components/settings/GeneralSettings.tsx +++ b/frontend/src/components/settings/GeneralSettings.tsx @@ -69,59 +69,77 @@ export function GeneralSettings() {
- +

- Automatically scroll to bottom when new messages arrive + Show only your messages and the assistant's replies. Hides tool calls, reasoning, diffs, and agent details.

updateSettings({ autoScroll: checked })} + id="simpleChatMode" + checked={preferences?.simpleChatMode ?? false} + onCheckedChange={(checked) => updateSettings({ simpleChatMode: checked })} />
- +

- Display model reasoning and thought process + Automatically scroll to bottom when new messages arrive

updateSettings({ showReasoning: checked })} + id="autoScroll" + checked={preferences?.autoScroll ?? true} + onCheckedChange={(checked) => updateSettings({ autoScroll: checked })} />
-
-
- -

- Automatically expand tool call details by default -

-
- updateSettings({ expandToolCalls: checked })} - /> -
+ {!preferences?.simpleChatMode && ( + <> +
+
+ +

+ Display model reasoning and thought process +

+
+ updateSettings({ showReasoning: checked })} + /> +
-
-
- -

- Show file diffs expanded by default for edit operations -

-
- updateSettings({ expandDiffs: checked })} - /> -
+
+
+ +

+ Automatically expand tool call details by default +

+
+ updateSettings({ expandToolCalls: checked })} + /> +
+ +
+
+ +

+ Show file diffs expanded by default for edit operations +

+
+ updateSettings({ expandDiffs: checked })} + /> +
+ + )} diff --git a/shared/src/schemas/settings.ts b/shared/src/schemas/settings.ts index caff07a5..efbb4d3f 100644 --- a/shared/src/schemas/settings.ts +++ b/shared/src/schemas/settings.ts @@ -114,9 +114,10 @@ export const UserPreferencesSchema = z.object({ defaultModel: z.string().optional(), defaultAgent: z.string().optional(), autoScroll: z.boolean(), - showReasoning: z.boolean(), - expandToolCalls: z.boolean(), expandDiffs: z.boolean(), + expandToolCalls: z.boolean(), + showReasoning: z.boolean(), + simpleChatMode: z.boolean(), leaderKey: z.string().optional(), directShortcuts: z.array(z.string()).optional(), keyboardShortcuts: z.record(z.string(), z.string()), @@ -161,9 +162,10 @@ export const DEFAULT_USER_PREFERENCES = { theme: "dark" as const, mode: "build" as const, autoScroll: true, - showReasoning: false, - expandToolCalls: false, expandDiffs: true, + expandToolCalls: false, + showReasoning: false, + simpleChatMode: false, leaderKey: DEFAULT_LEADER_KEY, directShortcuts: ['submit', 'abort'], keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, From cb05e211becaa3ff7485da5d9eea58f0d29c0c74 Mon Sep 17 00:00:00 2001 From: Chris Scott <99081550+chriswritescode-dev@users.noreply.github.com> Date: Thu, 23 Apr 2026 08:23:00 -0400 Subject: [PATCH 2/3] feat(frontend): add simple chat mode for user messages and clean up redundant optimistic filtering --- frontend/src/components/message/MessageThread.tsx | 12 ++++++++++++ frontend/src/hooks/useOpenCode.ts | 13 ++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/message/MessageThread.tsx b/frontend/src/components/message/MessageThread.tsx index 52092f21..e5ffe90d 100644 --- a/frontend/src/components/message/MessageThread.tsx +++ b/frontend/src/components/message/MessageThread.tsx @@ -7,6 +7,7 @@ import { MessageError } from './MessageError' import type { Message, Part, MessageWithParts } from '@/api/types' import { useSessionStatusForSession } from '@/stores/sessionStatusStore' import { useSessionTodos } from '@/stores/sessionTodosStore' +import { useSettings } from '@/hooks/useSettings' import type { components } from '@/api/opencode-types' import type { Todo } from '@/components/message/SessionTodoDisplay' import type { OpenCodeError } from '@/lib/opencode-errors' @@ -78,6 +79,7 @@ interface MessageRowProps { handleStartEditUserMessage: (userMessageId: string, assistantMessageId: string) => void handleCancelEdit: () => void model?: string + simpleChatMode: boolean } const MessageRow = memo(function MessageRow({ @@ -98,6 +100,7 @@ const MessageRow = memo(function MessageRow({ handleStartEditUserMessage, handleCancelEdit, model, + simpleChatMode, }: MessageRowProps) { const msg = msgWithParts.info const parts = msgWithParts.parts @@ -185,6 +188,12 @@ const MessageRow = memo(function MessageRow({ onClick={() => handleStartEditUserMessage(msg.id, nextAssistantMsg.id)} isEditable={false} /> + ) : msg.role === 'user' && simpleChatMode ? ( + {}} + isEditable={false} + /> ) : parts.length > 0 ? ( parts.map((part: Part, partIndex: number) => (
@@ -222,6 +231,8 @@ export const MessageThread = memo(function MessageThread({ const [editingUserMessageId, setEditingUserMessageId] = useState(null) const [editingForAssistantId, setEditingForAssistantId] = useState(null) const sessionStatus = useSessionStatusForSession(sessionID) + const { preferences } = useSettings() + const simpleChatMode = preferences?.simpleChatMode ?? false const pendingAssistantId = useMemo(() => { if (!messages) return undefined @@ -316,6 +327,7 @@ export const MessageThread = memo(function MessageThread({ handleStartEditUserMessage={handleStartEditUserMessage} handleCancelEdit={handleCancelEdit} model={model} + simpleChatMode={simpleChatMode} /> ))}
diff --git a/frontend/src/hooks/useOpenCode.ts b/frontend/src/hooks/useOpenCode.ts index 3566138d..3a410345 100644 --- a/frontend/src/hooks/useOpenCode.ts +++ b/frontend/src/hooks/useOpenCode.ts @@ -331,23 +331,22 @@ export const useSendPrompt = (opcodeUrl: string | null | undefined, directory?: }, onSuccess: async (data, variables) => { const { sessionID } = variables; - const { optimisticUserID, response } = data; + const { response } = data; const messagesQueryKey = ["opencode", "messages", opcodeUrl, sessionID, directory]; queryClient.setQueryData( messagesQueryKey, (old) => { if (!old) return old; - const withoutOptimistic = old.filter((msgWithParts) => msgWithParts.info.id !== optimisticUserID); - - const existingIdx = withoutOptimistic.findIndex(m => m.info.id === response.info.id); + + const existingIdx = old.findIndex(m => m.info.id === response.info.id); if (existingIdx >= 0) { - const updated = [...withoutOptimistic]; + const updated = [...old]; updated[existingIdx] = { info: response.info, parts: response.parts }; return updated; } - - return [...withoutOptimistic, { info: response.info, parts: response.parts }]; + + return [...old, { info: response.info, parts: response.parts }]; }, ); From 87b9553389523dd232bb8fd0f7fa3f836a1a9f47 Mon Sep 17 00:00:00 2001 From: Chris Scott <99081550+chriswritescode-dev@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:08:12 -0400 Subject: [PATCH 3/3] refactor(frontend): consolidate status indicators and simplify simple chat mode - Merge MiniScanner and CompactStatusIndicator into SessionStatusIndicator with size and showLabel props - Hide step-finish cost/token display in simpleChatMode - Update SessionCard and PromptInput to use unified SessionStatusIndicator - Add tests for step-finish rendering in simpleChatMode --- .../components/message/MessagePart.test.tsx | 50 +++++++++++ .../src/components/message/MessagePart.tsx | 26 +++--- .../src/components/message/PromptInput.tsx | 2 +- .../src/components/session/SessionCard.tsx | 26 +++--- frontend/src/components/ui/mini-scanner.tsx | 89 ------------------- .../ui/session-status-indicator.tsx | 70 ++++----------- 6 files changed, 96 insertions(+), 167 deletions(-) delete mode 100644 frontend/src/components/ui/mini-scanner.tsx diff --git a/frontend/src/components/message/MessagePart.test.tsx b/frontend/src/components/message/MessagePart.test.tsx index acce39e7..85ad69f1 100644 --- a/frontend/src/components/message/MessagePart.test.tsx +++ b/frontend/src/components/message/MessagePart.test.tsx @@ -304,6 +304,22 @@ describe('MessagePart', () => { sessionID: 'test-session', }) + const createStepFinishPart = (): MessagePartType => ({ + type: 'step-finish', + messageID: 'test-message', + sessionID: 'test-session', + cost: 0.01, + tokens: { + input: 100, + output: 50, + cache: { read: 0, write: 0 }, + }, + time: { + start: Date.now(), + end: Date.now() + 100, + }, + }) + const createTextPart = (): MessagePartType => ({ type: 'text', text: 'Hello, this is a text message', @@ -479,6 +495,40 @@ describe('MessagePart', () => { expect(container.firstChild).not.toBeNull() }) + + it('renders null for step-finish part when simpleChatMode is true', () => { + setupSettings({ + simpleChatMode: true, + showReasoning: false, + expandToolCalls: false, + expandDiffs: true, + autoScroll: true, + theme: 'dark', + mode: 'build', + }) + + const part = createStepFinishPart() + const { container } = render() + + expect(container.firstChild).toBeNull() + }) + + it('renders step-finish part when simpleChatMode is false', () => { + setupSettings({ + simpleChatMode: false, + showReasoning: false, + expandToolCalls: false, + expandDiffs: true, + autoScroll: true, + theme: 'dark', + mode: 'build', + }) + + const part = createStepFinishPart() + const { container } = render() + + expect(container.firstChild).not.toBeNull() + }) }) describe('showReasoning', () => { diff --git a/frontend/src/components/message/MessagePart.tsx b/frontend/src/components/message/MessagePart.tsx index a5600d39..db3eb491 100644 --- a/frontend/src/components/message/MessagePart.tsx +++ b/frontend/src/components/message/MessagePart.tsx @@ -148,18 +148,20 @@ export const MessagePart = memo(function MessagePart({ part, role, allParts, par
Agent: {part.name}
) - case 'step-finish': { - const isFree = part.cost === 0 - const totalTokens = part.tokens.input + part.tokens.output + (part.tokens.cache?.read || 0) - const costText = isMobile && isFree ? null : ${part.cost.toFixed(4)} • {totalTokens} tokens - return ( -
- {costText} - - {messageTextContent && part.messageID && } -
- ) - } + case 'step-finish': + if (simpleChatMode) return null + { + const isFree = part.cost === 0 + const totalTokens = part.tokens.input + part.tokens.output + (part.tokens.cache?.read || 0) + const costText = isMobile && isFree ? null : ${part.cost.toFixed(4)} • {totalTokens} tokens + return ( +
+ {costText} + + {messageTextContent && part.messageID && } +
+ ) + } case 'file': return ( diff --git a/frontend/src/components/message/PromptInput.tsx b/frontend/src/components/message/PromptInput.tsx index f463c643..04f93845 100644 --- a/frontend/src/components/message/PromptInput.tsx +++ b/frontend/src/components/message/PromptInput.tsx @@ -1065,7 +1065,7 @@ return ( /> {hasActiveStream ? (
- +
) : ( !hideSecondaryButtons && ( diff --git a/frontend/src/components/session/SessionCard.tsx b/frontend/src/components/session/SessionCard.tsx index 09732f43..28d6438e 100644 --- a/frontend/src/components/session/SessionCard.tsx +++ b/frontend/src/components/session/SessionCard.tsx @@ -1,7 +1,7 @@ import { useRef, useEffect } from "react"; import { Card } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; -import { MiniScanner } from "@/components/ui/mini-scanner"; +import { SessionStatusIndicator } from "@/components/ui/session-status-indicator"; import { Trash2, Clock } from "lucide-react"; import { formatDistanceToNow } from "date-fns"; import type { Session } from "@/api/types"; @@ -82,17 +82,17 @@ export const SessionCard = ({ {manageMode ? (
- { - onToggleSelection(checked === true); - }} - onClick={(e) => { - e.stopPropagation(); - }} - className="w-5 h-5 flex-shrink-0" - /> - + { + onToggleSelection(checked === true); + }} + onClick={(e) => { + e.stopPropagation(); + }} + className="w-5 h-5 flex-shrink-0" + /> +
@@ -122,7 +122,7 @@ export const SessionCard = ({ addSuffix: true, })} - +
)} diff --git a/frontend/src/components/ui/mini-scanner.tsx b/frontend/src/components/ui/mini-scanner.tsx deleted file mode 100644 index ed0c23a3..00000000 --- a/frontend/src/components/ui/mini-scanner.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { memo, useEffect, useState } from 'react' -import { useSessionStatusForSession, type SessionStatusType } from '@/stores/sessionStatusStore' - -interface MiniScannerProps { - sessionID: string - className?: string -} - -const SCANNER_WIDTH = 6 -const SCANNER_SEGMENTS = 12 - -export const MiniScanner = memo(function MiniScanner({ - sessionID, - className = '' -}: MiniScannerProps) { - const status = useSessionStatusForSession(sessionID) - const [position, setPosition] = useState(0) - const [direction, setDirection] = useState(1) - - useEffect(() => { - if (status.type !== 'busy' && status.type !== 'retry' && status.type !== 'compact') { - return - } - - const interval = setInterval(() => { - setPosition(prev => { - const next = prev + direction - if (next >= SCANNER_SEGMENTS - SCANNER_WIDTH) { - setDirection(-1) - return SCANNER_SEGMENTS - SCANNER_WIDTH - } - if (next <= 0) { - setDirection(1) - return 0 - } - return next - }) - }, 60) - - return () => clearInterval(interval) - }, [status.type, direction]) - - const getSegmentColor = (index: number, statusType: SessionStatusType['type']) => { - if (statusType === 'idle') { - return 'bg-transparent' - } - - const distance = Math.abs(index - (position + SCANNER_WIDTH / 2)) - const maxDistance = SCANNER_WIDTH / 2 - - if (distance > maxDistance + 1) { - return 'bg-muted/20' - } - - const intensity = Math.max(0, 1 - distance / (maxDistance + 2)) - - if (statusType === 'retry') { - if (intensity > 0.8) return 'bg-amber-500' - if (intensity > 0.5) return 'bg-amber-500/70' - if (intensity > 0.2) return 'bg-amber-500/40' - return 'bg-amber-500/20' - } - - if (statusType === 'compact') { - if (intensity > 0.8) return 'bg-purple-500' - if (intensity > 0.5) return 'bg-purple-500/70' - if (intensity > 0.2) return 'bg-purple-500/40' - return 'bg-purple-500/20' - } - - if (intensity > 0.8) return 'bg-blue-500' - if (intensity > 0.5) return 'bg-blue-500/70' - if (intensity > 0.2) return 'bg-blue-500/40' - return 'bg-blue-500/20' - } - - return ( -
-
- {Array.from({ length: SCANNER_SEGMENTS }).map((_, i) => ( -
- ))} -
-
- ) -}) \ No newline at end of file diff --git a/frontend/src/components/ui/session-status-indicator.tsx b/frontend/src/components/ui/session-status-indicator.tsx index cf1c89ae..aaae0c09 100644 --- a/frontend/src/components/ui/session-status-indicator.tsx +++ b/frontend/src/components/ui/session-status-indicator.tsx @@ -4,6 +4,8 @@ import { useSessionStatusForSession, type SessionStatusType } from '@/stores/ses interface SessionStatusIndicatorProps { sessionID: string className?: string + size?: 'sm' | 'md' + showLabel?: boolean } const SCANNER_WIDTH = 6 @@ -11,7 +13,9 @@ const SCANNER_SEGMENTS = 12 export const SessionStatusIndicator = memo(function SessionStatusIndicator({ sessionID, - className = '' + className = '', + size = 'md', + showLabel = false }: SessionStatusIndicatorProps) { const status = useSessionStatusForSession(sessionID) const [position, setPosition] = useState(0) @@ -42,7 +46,7 @@ export const SessionStatusIndicator = memo(function SessionStatusIndicator({ }, [status.type, direction]) useEffect(() => { - if (status.type !== 'retry') { + if (!showLabel || status.type !== 'retry') { setRetryCountdown(0) return } @@ -56,7 +60,7 @@ export const SessionStatusIndicator = memo(function SessionStatusIndicator({ }, 1000) return () => clearInterval(interval) - }, [status]) + }, [status, showLabel]) if (status.type === 'idle') { return null @@ -92,63 +96,25 @@ export const SessionStatusIndicator = memo(function SessionStatusIndicator({ return 'bg-blue-500/20' } + const segmentClass = size === 'sm' ? 'w-0.5 h-2' : 'w-1 h-4' + const containerGap = showLabel ? 'gap-2' : '' + return ( -
+
{Array.from({ length: SCANNER_SEGMENTS }).map((_, i) => (
))}
- - {status.type === 'retry' && ( - <> - Retry #{status.attempt} - {retryCountdown > 0 && ({retryCountdown}s)} - - )} - + {showLabel && status.type === 'retry' && ( + + Retry #{status.attempt} + {retryCountdown > 0 && ({retryCountdown}s)} + + )}
) }) - -interface CompactStatusIndicatorProps { - sessionID: string - className?: string -} - -export const CompactStatusIndicator = memo(function CompactStatusIndicator({ - sessionID, - className = '' -}: CompactStatusIndicatorProps) { - const status = useSessionStatusForSession(sessionID) - const [frame, setFrame] = useState(0) - - useEffect(() => { - if (status.type !== 'busy' && status.type !== 'retry' && status.type !== 'compact') return - - const interval = setInterval(() => { - setFrame(prev => (prev + 1) % 8) - }, 120) - - return () => clearInterval(interval) - }, [status.type]) - - if (status.type === 'idle') { - return null - } - - const pulseFrames = ['●', '◐', '○', '◑', '●', '◐', '○', '◑'] - const color = status.type === 'retry' ? 'text-amber-500' : status.type === 'compact' ? 'text-purple-500' : 'text-blue-500' - - return ( - - {pulseFrames[frame]} - - {status.type === 'retry' ? `Retry #${status.attempt}` : status.type === 'compact' ? 'Compacting' : 'Working'} - - - ) -})