diff --git a/web/src/components/MessageList.tsx b/web/src/components/MessageList.tsx deleted file mode 100644 index bc95122a..00000000 --- a/web/src/components/MessageList.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import type { DecryptedMessage } from '@/types/api' -import { Button } from '@/components/ui/button' - -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - -type RoleWrappedMessage = { - role: string - content: unknown -} - -function isRoleWrappedMessage(value: unknown): value is RoleWrappedMessage { - if (!isObject(value)) return false - return typeof value.role === 'string' && 'content' in value -} - -function getMessageInner(value: unknown): unknown { - return isRoleWrappedMessage(value) ? value.content : value -} - -function truncate(text: string, maxLen: number): string { - if (text.length <= maxLen) return text - return text.slice(0, maxLen - 3) + '...' -} - -function formatEventLabel(event: unknown): string { - if (!isObject(event)) return 'Event' - const type = event.type - if (type === 'ready') return 'βœ… Ready for input' - if (type === 'switch') { - const mode = event.mode === 'local' ? 'local' : 'remote' - return `πŸ”„ Switched to ${mode}` - } - if (type === 'permission-mode-changed') { - const mode = typeof event.mode === 'string' ? event.mode : 'default' - return `πŸ” Permission mode: ${mode}` - } - if (type === 'message') { - return typeof event.message === 'string' ? event.message : 'Message' - } - - try { - return JSON.stringify(event) - } catch { - return 'Event' - } -} - -function formatToolUseSummary(toolUse: Record): string { - const name = typeof toolUse.name === 'string' - ? toolUse.name - : typeof toolUse.tool === 'string' - ? toolUse.tool - : 'Tool' - - const input = toolUse.input ?? toolUse.arguments ?? toolUse.args - if (isObject(input)) { - const filePath = typeof input.file_path === 'string' - ? input.file_path - : typeof input.path === 'string' - ? input.path - : null - if (filePath) { - return `πŸ”§ ${name} ${filePath}` - } - if (typeof input.command === 'string') { - return `πŸ”§ ${name} ${truncate(input.command, 160)}` - } - if (typeof input.pattern === 'string') { - return `πŸ”§ ${name} ${input.pattern}` - } - const prompt = typeof input.description === 'string' - ? input.description - : typeof input.prompt === 'string' - ? input.prompt - : null - if (prompt) { - return `πŸ”§ ${name} ${truncate(prompt, 160)}` - } - } - - return `πŸ”§ ${name}` -} - -function extractTextFromToolResult(resultContent: unknown): string | null { - if (!resultContent) { - return null - } - - if (typeof resultContent === 'string') { - return truncate(resultContent, 300) - } - - if (Array.isArray(resultContent)) { - const textBlocks = resultContent - .filter((block) => isObject(block) && block.type === 'text' && typeof block.text === 'string') - .map((block) => (block as Record).text as string) - .filter((text) => text.trim().length > 0) - if (textBlocks.length > 0) { - return truncate(textBlocks.join('\n'), 300) - } - } - - if (isObject(resultContent) && typeof resultContent.text === 'string') { - return truncate(resultContent.text, 300) - } - - try { - return truncate(JSON.stringify(resultContent), 300) - } catch { - return null - } -} - -function formatToolResultSummary(toolResult: Record): string { - const isError = Boolean(toolResult.is_error ?? toolResult.isError) - const status = isError ? '❌' : 'βœ“' - const resultContent = toolResult.content ?? toolResult.result ?? toolResult.output - const extracted = extractTextFromToolResult(resultContent) - return extracted ? `${status} Tool result: ${extracted}` : `${status} Tool result` -} - -function extractTextFromBlock(block: unknown): string | null { - if (!block) return null - if (typeof block === 'string') return block - if (!isObject(block)) return null - - const type = block.type - - if (type === 'text' && typeof block.text === 'string') { - return block.text - } - - if (type === 'event') { - return formatEventLabel(block.data) - } - - if (type === 'tool_use') { - return formatToolUseSummary(block) - } - - if (type === 'tool_result') { - return formatToolResultSummary(block) - } - - if (type === 'output') { - return extractTextFromOutput(block.data) - } - - return null -} - -function extractTextFromOutput(data: unknown): string | null { - if (!isObject(data)) { - return null - } - - const outputType = data.type - - if (outputType === 'summary' && typeof data.summary === 'string') { - return `πŸ“ ${data.summary}` - } - - if (outputType === 'event') { - const event = (data.data ?? data.event ?? data) as unknown - return formatEventLabel(event) - } - - if (outputType === 'assistant') { - const message = isObject(data.message) ? data.message : null - const assistantContent = (message?.content ?? null) as unknown - - if (typeof assistantContent === 'string') { - return assistantContent - } - - if (Array.isArray(assistantContent)) { - const parts = assistantContent - .map((block) => extractTextFromBlock(block)) - .filter((part): part is string => Boolean(part && part.trim().length > 0)) - if (parts.length > 0) { - return parts.join('\n') - } - } - - return null - } - - if (outputType === 'tool_use') { - return formatToolUseSummary(data) - } - - if (outputType === 'tool_result') { - return formatToolResultSummary(data) - } - - return null -} - -function extractText(content: unknown): string { - const inner = getMessageInner(content) - if (inner === null || inner === undefined) return '' - if (typeof inner === 'string') return inner - - const fromBlock = extractTextFromBlock(inner) - if (fromBlock) { - return fromBlock - } - - if (Array.isArray(inner)) { - const parts = inner - .map((block) => extractTextFromBlock(block)) - .filter((part): part is string => Boolean(part && part.trim().length > 0)) - if (parts.length > 0) { - return parts.join('\n') - } - } - - return '' -} - -function getRoleEmoji(content: unknown): string { - if (isRoleWrappedMessage(content)) { - if (content.role === 'user') return 'πŸ‘€' - if (content.role === 'assistant' || content.role === 'agent') return 'πŸ€–' - } - - const inner = getMessageInner(content) - if (isObject(inner) && inner.type === 'event') return '🟦' - if (isObject(inner) && inner.type === 'tool_use') return 'πŸ”§' - if (isObject(inner) && inner.type === 'tool_result') return 'πŸ”§' - if (isObject(inner) && inner.type === 'output') { - const data = inner.data - if (isObject(data)) { - if (data.type === 'assistant') return 'πŸ€–' - if (data.type === 'tool_use' || data.type === 'tool_result') return 'πŸ”§' - if (data.type === 'event') return '🟦' - if (data.type === 'summary') return 'πŸ“' - } - } - - return 'πŸ’¬' -} - -export function MessageList(props: { - messages: DecryptedMessage[] - hasMore: boolean - isLoadingMore: boolean - onLoadMore: () => void -}) { - return ( -
- {props.hasMore ? ( - - ) : null} - -
- {props.messages.map((m) => { - const text = extractText(m.content) - return ( -
-
-
{getRoleEmoji(m.content)}
-
- {text ? ( -
{text}
- ) : ( -
-                                            {JSON.stringify(m.content, null, 2)}
-                                        
- )} -
-
-
- ) - })} -
-
- ) -} diff --git a/web/src/components/PermissionBanner.tsx b/web/src/components/PermissionBanner.tsx deleted file mode 100644 index dd33a1ac..00000000 --- a/web/src/components/PermissionBanner.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { useMemo, useState } from 'react' -import type { ApiClient } from '@/api/client' -import type { AgentStateRequest } from '@/types/api' -import { Button } from '@/components/ui/button' -import { DiffView } from '@/components/DiffView' -import { CodeBlock } from '@/components/CodeBlock' -import { getTelegramWebApp } from '@/hooks/useTelegram' - -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - -function getEditArgs(args: unknown): { filePath: string | null; oldString: string | null; newString: string | null } { - if (!isObject(args)) { - return { filePath: null, oldString: null, newString: null } - } - const filePath = typeof args.file_path === 'string' - ? args.file_path - : typeof args.path === 'string' - ? args.path - : null - - const oldString = typeof args.old_string === 'string' ? args.old_string : null - const newString = typeof args.new_string === 'string' ? args.new_string : null - - return { filePath, oldString, newString } -} - -function safeStringify(value: unknown): string { - try { - return JSON.stringify(value, null, 2) - } catch { - return String(value) - } -} - -function parseErrorMessage(e: unknown): string { - const message = e instanceof Error ? e.message : 'Request failed' - // Check for "Session is inactive" error (HTTP 409) - if (message.includes('Session is inactive') || message.includes('409')) { - return 'Session became inactive. Wait for it to reconnect and try again.' - } - // Check for "Request not found" error (HTTP 404) - if (message.includes('Request not found') || message.includes('not found')) { - return 'Permission request no longer exists. It may have been handled already.' - } - return message -} - -export function PermissionBanner(props: { - api: ApiClient - sessionId: string - requestId: string - request: AgentStateRequest - onDone: () => void - disabled?: boolean -}) { - const [isWorking, setIsWorking] = useState(false) - const [error, setError] = useState(null) - - const editArgs = useMemo(() => getEditArgs(props.request.arguments), [props.request.arguments]) - - async function run(action: () => Promise, haptic: 'success' | 'error') { - setIsWorking(true) - setError(null) - try { - await action() - getTelegramWebApp()?.HapticFeedback?.notificationOccurred(haptic) - props.onDone() - } catch (e) { - getTelegramWebApp()?.HapticFeedback?.notificationOccurred('error') - setError(parseErrorMessage(e)) - } finally { - setIsWorking(false) - } - } - - return ( -
-
-
-
- ⚠️ {props.request.tool} - {editArgs.filePath ? `: ${editArgs.filePath}` : ''} -
-
- {props.requestId} -
-
-
- - - - -
-
- -
- {props.request.tool === 'Edit' && editArgs.oldString !== null && editArgs.newString !== null ? ( - - ) : ( - - )} -
- - {error ? ( -
- {error} -
- ) : null} -
- ) -} diff --git a/web/src/components/PermissionDialog.tsx b/web/src/components/PermissionDialog.tsx deleted file mode 100644 index b9145fcf..00000000 --- a/web/src/components/PermissionDialog.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import { useMemo, useState } from 'react' -import type { AgentStateRequest } from '@/types/api' -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' -import { Button } from '@/components/ui/button' - -function formatArgs(tool: string, args: unknown): string { - if (!args || typeof args !== 'object') { - return '' - } - const obj = args as any - - if (tool === 'Edit') { - const filePath = obj.file_path ?? obj.path - const oldString = obj.old_string - const newString = obj.new_string - return [ - filePath ? `file: ${filePath}` : null, - oldString ? `old_string:\n${oldString}` : null, - newString ? `new_string:\n${newString}` : null - ].filter(Boolean).join('\n\n') - } - - if (tool === 'Write') { - const filePath = obj.file_path ?? obj.path - const content = obj.content - return [ - filePath ? `file: ${filePath}` : null, - typeof content === 'string' ? `content:\n${content}` : null - ].filter(Boolean).join('\n\n') - } - - return JSON.stringify(args, null, 2) -} - -function parseErrorMessage(e: unknown): string { - const message = e instanceof Error ? e.message : 'Request failed' - // Check for "Session is inactive" error (HTTP 409) - if (message.includes('Session is inactive') || message.includes('409')) { - return 'Session became inactive. Wait for it to reconnect and try again.' - } - // Check for "Request not found" error (HTTP 404) - if (message.includes('Request not found') || message.includes('not found')) { - return 'Permission request no longer exists. It may have been handled already.' - } - return message -} - -export function PermissionDialog(props: { - sessionId: string - requestId: string - request: AgentStateRequest - onApprove: (mode?: 'default' | 'acceptEdits' | 'bypassPermissions') => Promise - onDeny: () => Promise - actionsDisabled?: boolean -}) { - const [open, setOpen] = useState(false) - const [isWorking, setIsWorking] = useState(false) - const [error, setError] = useState(null) - - const formattedArgs = useMemo(() => formatArgs(props.request.tool, props.request.arguments), [props.request]) - - async function run(action: () => Promise) { - setIsWorking(true) - setError(null) - try { - await action() - setOpen(false) - } catch (e) { - setError(parseErrorMessage(e)) - } finally { - setIsWorking(false) - } - } - - return ( - - - - - - - Permission Request - - {props.request.tool} ({props.requestId.slice(0, 8)}) - - - -
-
-                        {formattedArgs || '(no arguments)'}
-                    
- - {error ?
{error}
: null} - -
- - - - -
-
-
-
- ) -} diff --git a/web/src/components/PermissionPanel.tsx b/web/src/components/PermissionPanel.tsx deleted file mode 100644 index 6a916fbe..00000000 --- a/web/src/components/PermissionPanel.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { useMemo, useState } from 'react' -import type { ApiClient } from '@/api/client' -import type { AgentStateRequest } from '@/types/api' -import { Button } from '@/components/ui/button' -import { DiffView } from '@/components/DiffView' -import { CodeBlock } from '@/components/CodeBlock' -import { getTelegramWebApp } from '@/hooks/useTelegram' - -function isObject(value: unknown): value is Record { - return Boolean(value) && typeof value === 'object' -} - -function getEditArgs(args: unknown): { filePath: string | null; oldString: string | null; newString: string | null } { - if (!isObject(args)) { - return { filePath: null, oldString: null, newString: null } - } - const filePath = typeof args.file_path === 'string' - ? args.file_path - : typeof args.path === 'string' - ? args.path - : null - - const oldString = typeof args.old_string === 'string' ? args.old_string : null - const newString = typeof args.new_string === 'string' ? args.new_string : null - - return { filePath, oldString, newString } -} - -function getFilePath(args: unknown): string | null { - if (!isObject(args)) return null - if (typeof args.file_path === 'string') return args.file_path - if (typeof args.path === 'string') return args.path - return null -} - -function safeStringify(value: unknown): string { - try { - return JSON.stringify(value, null, 2) - } catch { - return String(value) - } -} - -function parseErrorMessage(e: unknown): string { - const message = e instanceof Error ? e.message : 'Request failed' - if (message.includes('Session is inactive') || message.includes('409')) { - return 'Session became inactive. Wait for it to reconnect.' - } - if (message.includes('Request not found') || message.includes('not found')) { - return 'Request no longer exists.' - } - return message -} - -export function PermissionPanel(props: { - api: ApiClient - sessionId: string - requestId: string - request: AgentStateRequest - disabled: boolean - onDone: () => void -}) { - const [isWorking, setIsWorking] = useState(false) - const [error, setError] = useState(null) - - const editArgs = useMemo(() => getEditArgs(props.request.arguments), [props.request.arguments]) - const filePath = useMemo(() => getFilePath(props.request.arguments), [props.request.arguments]) - - async function run(action: () => Promise, haptic: 'success' | 'error') { - setIsWorking(true) - setError(null) - try { - await action() - getTelegramWebApp()?.HapticFeedback?.notificationOccurred(haptic) - props.onDone() - } catch (e) { - getTelegramWebApp()?.HapticFeedback?.notificationOccurred('error') - setError(parseErrorMessage(e)) - } finally { - setIsWorking(false) - } - } - - const isEdit = props.request.tool === 'Edit' && editArgs.oldString !== null && editArgs.newString !== null - - return ( -
- {/* Header */} -
-
- ⚠️ {props.request.tool} -
- {filePath ? ( -
- {filePath} -
- ) : null} -
- - {/* Content preview */} -
- {isEdit ? ( - - ) : ( - - )} -
- - {/* Error */} - {error ? ( -
- {error} -
- ) : null} - - {/* 2x2 Button grid */} -
- - - - -
-
- ) -} diff --git a/web/src/components/SessionDetail.tsx b/web/src/components/SessionDetail.tsx deleted file mode 100644 index 6891384b..00000000 --- a/web/src/components/SessionDetail.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { useMemo } from 'react' -import type { ApiClient } from '@/api/client' -import { getTelegramWebApp } from '@/hooks/useTelegram' -import type { DecryptedMessage, Session } from '@/types/api' -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import { MessageBubble } from '@/components/MessageBubble' -import { PermissionPanel } from '@/components/PermissionPanel' - -function getSessionTitle(session: Session): string { - if (session.metadata?.name) { - return session.metadata.name - } - if (session.metadata?.summary?.text) { - return session.metadata.summary.text - } - if (session.metadata?.path) { - const parts = session.metadata.path.split('/').filter(Boolean) - return parts.length > 0 ? parts[parts.length - 1] : session.id.slice(0, 8) - } - return session.id.slice(0, 8) -} - -export function SessionDetail(props: { - api: ApiClient - session: Session - messages: DecryptedMessage[] - messagesWarning: string | null - hasMoreMessages: boolean - isLoadingMessages: boolean - isLoadingMoreMessages: boolean - onBack: () => void - onRefreshAll: () => void - onRefreshSession: () => void - onLoadMore: () => void -}) { - const requests = useMemo(() => { - const rec = props.session.agentState?.requests ?? null - if (!rec) return [] - return Object.entries(rec).map(([requestId, request]) => ({ requestId, request })) - }, [props.session]) - - const isTelegram = getTelegramWebApp() !== null - - return ( -
- {!isTelegram && ( - <> -
- - -
- - - - {getSessionTitle(props.session)} - - {props.session.metadata?.path ?? props.session.id} - - - - - )} - - - - Messages - Decrypted message history - - - {props.messagesWarning ? ( -
- {props.messagesWarning} -
- ) : null} - {props.hasMoreMessages ? ( - - ) : null} - - {props.isLoadingMessages ? ( -
Loading…
- ) : ( -
- {props.messages.map((m) => ( - - ))} -
- )} -
-
- - {requests.length > 0 ? ( - - ) : null} -
- ) -}