mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user