refactor: remove dead code components from web/src/components

Remove 5 unused component files that were not imported anywhere:
- SessionDetail.tsx (replaced by SessionChat)
- PermissionPanel.tsx (only used by dead SessionDetail)
- MessageList.tsx
- PermissionBanner.tsx
- PermissionDialog.tsx

Confirmed via grep searches and successful build.
This commit is contained in:
weishu
2025-12-17 16:04:15 +08:00
parent 502b63c236
commit 3faa68f988
5 changed files with 0 additions and 843 deletions
-288
View File
@@ -1,288 +0,0 @@
import type { DecryptedMessage } from '@/types/api'
import { Button } from '@/components/ui/button'
function isObject(value: unknown): value is Record<string, unknown> {
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, unknown>): 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<string, unknown>).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, unknown>): 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 (
<div className="flex flex-col gap-2">
{props.hasMore ? (
<Button
variant="secondary"
size="sm"
onClick={props.onLoadMore}
disabled={props.isLoadingMore}
>
Load older
</Button>
) : null}
<div className="flex flex-col gap-2">
{props.messages.map((m) => {
const text = extractText(m.content)
return (
<div key={m.id} className="rounded-md border border-[var(--app-border)] bg-[var(--app-subtle-bg)] p-2">
<div className="flex items-start gap-2">
<div className="text-sm">{getRoleEmoji(m.content)}</div>
<div className="flex-1">
{text ? (
<div className="whitespace-pre-wrap text-sm">{text}</div>
) : (
<pre className="whitespace-pre-wrap text-xs text-[var(--app-hint)]">
{JSON.stringify(m.content, null, 2)}
</pre>
)}
</div>
</div>
</div>
)
})}
</div>
</div>
)
}
-147
View File
@@ -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<string, unknown> {
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<string | null>(null)
const editArgs = useMemo(() => getEditArgs(props.request.arguments), [props.request.arguments])
async function run(action: () => Promise<void>, 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 (
<div className="border-b border-[var(--app-border)] bg-amber-500/10 p-3">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<div className="text-sm font-semibold truncate">
{props.request.tool}
{editArgs.filePath ? `: ${editArgs.filePath}` : ''}
</div>
<div className="text-xs text-[var(--app-hint)] truncate">
{props.requestId}
</div>
</div>
<div className="flex flex-wrap items-center justify-end gap-2">
<Button
size="sm"
disabled={isWorking || props.disabled}
onClick={() => run(() => props.api.approvePermission(props.sessionId, props.requestId, 'default'), 'success')}
>
Allow
</Button>
<Button
variant="secondary"
size="sm"
disabled={isWorking || props.disabled}
onClick={() => run(() => props.api.approvePermission(props.sessionId, props.requestId, 'acceptEdits'), 'success')}
>
Allow+Edits
</Button>
<Button
variant="secondary"
size="sm"
disabled={isWorking || props.disabled}
onClick={() => run(() => props.api.approvePermission(props.sessionId, props.requestId, 'bypassPermissions'), 'success')}
>
Bypass
</Button>
<Button
variant="destructive"
size="sm"
disabled={isWorking || props.disabled}
onClick={() => run(() => props.api.denyPermission(props.sessionId, props.requestId), 'success')}
>
Deny
</Button>
</div>
</div>
<div className="mt-3">
{props.request.tool === 'Edit' && editArgs.oldString !== null && editArgs.newString !== null ? (
<DiffView
oldString={editArgs.oldString}
newString={editArgs.newString}
filePath={editArgs.filePath ?? undefined}
/>
) : (
<CodeBlock
code={safeStringify(props.request.arguments)}
language="json"
/>
)}
</div>
{error ? (
<div className="mt-2 text-sm text-red-600">
{error}
</div>
) : null}
</div>
)
}
-135
View File
@@ -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<void>
onDeny: () => Promise<void>
actionsDisabled?: boolean
}) {
const [open, setOpen] = useState(false)
const [isWorking, setIsWorking] = useState(false)
const [error, setError] = useState<string | null>(null)
const formattedArgs = useMemo(() => formatArgs(props.request.tool, props.request.arguments), [props.request])
async function run(action: () => Promise<void>) {
setIsWorking(true)
setError(null)
try {
await action()
setOpen(false)
} catch (e) {
setError(parseErrorMessage(e))
} finally {
setIsWorking(false)
}
}
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="secondary" size="sm">
View
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Permission Request</DialogTitle>
<DialogDescription>
{props.request.tool} ({props.requestId.slice(0, 8)})
</DialogDescription>
</DialogHeader>
<div className="mt-3 flex flex-col gap-3">
<pre className="max-h-64 overflow-auto rounded-md border border-[var(--app-border)] bg-[var(--app-subtle-bg)] p-2 text-xs whitespace-pre-wrap">
{formattedArgs || '(no arguments)'}
</pre>
{error ? <div className="text-sm text-red-600">{error}</div> : null}
<div className="flex flex-wrap gap-2">
<Button
variant="default"
size="sm"
disabled={isWorking || props.actionsDisabled}
onClick={() => run(() => props.onApprove('default'))}
>
Allow
</Button>
<Button
variant="secondary"
size="sm"
disabled={isWorking || props.actionsDisabled}
onClick={() => run(() => props.onApprove('acceptEdits'))}
>
Allow + Edits
</Button>
<Button
variant="secondary"
size="sm"
disabled={isWorking || props.actionsDisabled}
onClick={() => run(() => props.onApprove('bypassPermissions'))}
>
Bypass
</Button>
<Button
variant="destructive"
size="sm"
disabled={isWorking || props.actionsDisabled}
onClick={() => run(() => props.onDeny())}
>
Deny
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)
}
-158
View File
@@ -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<string, unknown> {
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<string | null>(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<void>, 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 (
<div className="border-t border-[var(--app-border)] bg-[var(--app-bg)] p-3">
{/* Header */}
<div className="mb-2">
<div className="text-sm font-semibold">
{props.request.tool}
</div>
{filePath ? (
<div className="text-xs text-[var(--app-hint)] truncate">
{filePath}
</div>
) : null}
</div>
{/* Content preview */}
<div className="mb-3 max-h-40 overflow-auto rounded border border-[var(--app-border)]">
{isEdit ? (
<DiffView
oldString={editArgs.oldString!}
newString={editArgs.newString!}
/>
) : (
<CodeBlock
code={safeStringify(props.request.arguments)}
language="json"
/>
)}
</div>
{/* Error */}
{error ? (
<div className="mb-2 text-sm text-red-600">
{error}
</div>
) : null}
{/* 2x2 Button grid */}
<div className="grid grid-cols-2 gap-2">
<Button
size="sm"
disabled={isWorking || props.disabled}
onClick={() => run(() => props.api.approvePermission(props.sessionId, props.requestId, 'default'), 'success')}
>
Allow
</Button>
<Button
variant="destructive"
size="sm"
disabled={isWorking || props.disabled}
onClick={() => run(() => props.api.denyPermission(props.sessionId, props.requestId), 'success')}
>
Deny
</Button>
<Button
variant="secondary"
size="sm"
disabled={isWorking || props.disabled}
onClick={() => run(() => props.api.approvePermission(props.sessionId, props.requestId, 'acceptEdits'), 'success')}
>
Allow+Edits
</Button>
<Button
variant="secondary"
size="sm"
disabled={isWorking || props.disabled}
onClick={() => run(() => props.api.approvePermission(props.sessionId, props.requestId, 'bypassPermissions'), 'success')}
>
Bypass
</Button>
</div>
</div>
)
}
-115
View File
@@ -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 (
<div className="flex flex-col gap-3">
{!isTelegram && (
<>
<div className="flex items-center justify-between gap-2">
<Button variant="secondary" size="sm" onClick={props.onBack}>
Back
</Button>
<Button variant="secondary" size="sm" onClick={props.onRefreshAll} disabled={props.isLoadingMessages}>
Refresh
</Button>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle className="truncate">{getSessionTitle(props.session)}</CardTitle>
<CardDescription className="truncate">
{props.session.metadata?.path ?? props.session.id}
</CardDescription>
</CardHeader>
</Card>
</>
)}
<Card>
<CardHeader className="pb-2">
<CardTitle>Messages</CardTitle>
<CardDescription>Decrypted message history</CardDescription>
</CardHeader>
<CardContent className="pt-0">
{props.messagesWarning ? (
<div className="mb-2 rounded-md border border-amber-500/30 bg-amber-500/10 p-2 text-xs">
{props.messagesWarning}
</div>
) : null}
{props.hasMoreMessages ? (
<Button
variant="secondary"
size="sm"
onClick={props.onLoadMore}
disabled={props.isLoadingMoreMessages}
>
{props.isLoadingMoreMessages ? 'Loading…' : 'Load older'}
</Button>
) : null}
{props.isLoadingMessages ? (
<div className="text-sm text-[var(--app-hint)]">Loading</div>
) : (
<div className="mt-2 flex flex-col gap-2">
{props.messages.map((m) => (
<MessageBubble key={m.id} message={m} />
))}
</div>
)}
</CardContent>
</Card>
{requests.length > 0 ? (
<PermissionPanel
api={props.api}
sessionId={props.session.id}
requestId={requests[0].requestId}
request={requests[0].request}
disabled={!props.session.active}
onDone={props.onRefreshSession}
/>
) : null}
</div>
)
}