Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/codex-claw/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Route as ApiTasksRouteImport } from './routes/api/tasks'
import { Route as ApiStreamRouteImport } from './routes/api/stream'
import { Route as ApiSessionsRouteImport } from './routes/api/sessions'
import { Route as ApiSendRouteImport } from './routes/api/send'
import { Route as ApiRunEventsRouteImport } from './routes/api/run-events'
import { Route as ApiRepoContextRouteImport } from './routes/api/repo-context'
import { Route as ApiPingRouteImport } from './routes/api/ping'
import { Route as ApiPathsRouteImport } from './routes/api/paths'
Expand Down Expand Up @@ -72,6 +73,11 @@ const ApiSendRoute = ApiSendRouteImport.update({
path: '/api/send',
getParentRoute: () => rootRouteImport,
} as any)
const ApiRunEventsRoute = ApiRunEventsRouteImport.update({
id: '/api/run-events',
path: '/api/run-events',
getParentRoute: () => rootRouteImport,
} as any)
const ApiRepoContextRoute = ApiRepoContextRouteImport.update({
id: '/api/repo-context',
path: '/api/repo-context',
Expand Down Expand Up @@ -125,6 +131,7 @@ export interface FileRoutesByFullPath {
'/api/paths': typeof ApiPathsRoute
'/api/ping': typeof ApiPingRoute
'/api/repo-context': typeof ApiRepoContextRoute
'/api/run-events': typeof ApiRunEventsRoute
'/api/send': typeof ApiSendRoute
'/api/sessions': typeof ApiSessionsRoute
'/api/stream': typeof ApiStreamRoute
Expand All @@ -144,6 +151,7 @@ export interface FileRoutesByTo {
'/api/paths': typeof ApiPathsRoute
'/api/ping': typeof ApiPingRoute
'/api/repo-context': typeof ApiRepoContextRoute
'/api/run-events': typeof ApiRunEventsRoute
'/api/send': typeof ApiSendRoute
'/api/sessions': typeof ApiSessionsRoute
'/api/stream': typeof ApiStreamRoute
Expand All @@ -164,6 +172,7 @@ export interface FileRoutesById {
'/api/paths': typeof ApiPathsRoute
'/api/ping': typeof ApiPingRoute
'/api/repo-context': typeof ApiRepoContextRoute
'/api/run-events': typeof ApiRunEventsRoute
'/api/send': typeof ApiSendRoute
'/api/sessions': typeof ApiSessionsRoute
'/api/stream': typeof ApiStreamRoute
Expand All @@ -185,6 +194,7 @@ export interface FileRouteTypes {
| '/api/paths'
| '/api/ping'
| '/api/repo-context'
| '/api/run-events'
| '/api/send'
| '/api/sessions'
| '/api/stream'
Expand All @@ -204,6 +214,7 @@ export interface FileRouteTypes {
| '/api/paths'
| '/api/ping'
| '/api/repo-context'
| '/api/run-events'
| '/api/send'
| '/api/sessions'
| '/api/stream'
Expand All @@ -223,6 +234,7 @@ export interface FileRouteTypes {
| '/api/paths'
| '/api/ping'
| '/api/repo-context'
| '/api/run-events'
| '/api/send'
| '/api/sessions'
| '/api/stream'
Expand All @@ -243,6 +255,7 @@ export interface RootRouteChildren {
ApiPathsRoute: typeof ApiPathsRoute
ApiPingRoute: typeof ApiPingRoute
ApiRepoContextRoute: typeof ApiRepoContextRoute
ApiRunEventsRoute: typeof ApiRunEventsRoute
ApiSendRoute: typeof ApiSendRoute
ApiSessionsRoute: typeof ApiSessionsRoute
ApiStreamRoute: typeof ApiStreamRoute
Expand Down Expand Up @@ -316,6 +329,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ApiSendRouteImport
parentRoute: typeof rootRouteImport
}
'/api/run-events': {
id: '/api/run-events'
path: '/api/run-events'
fullPath: '/api/run-events'
preLoaderRoute: typeof ApiRunEventsRouteImport
parentRoute: typeof rootRouteImport
}
'/api/repo-context': {
id: '/api/repo-context'
path: '/api/repo-context'
Expand Down Expand Up @@ -387,6 +407,7 @@ const rootRouteChildren: RootRouteChildren = {
ApiPathsRoute: ApiPathsRoute,
ApiPingRoute: ApiPingRoute,
ApiRepoContextRoute: ApiRepoContextRoute,
ApiRunEventsRoute: ApiRunEventsRoute,
ApiSendRoute: ApiSendRoute,
ApiSessionsRoute: ApiSessionsRoute,
ApiStreamRoute: ApiStreamRoute,
Expand Down
38 changes: 38 additions & 0 deletions apps/codex-claw/src/routes/api/run-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createFileRoute } from '@tanstack/react-router'
import { json } from '@tanstack/react-start'
import { getCodexRunEventLog } from '../../server/codex-cli'

function downloadName(id: string) {
return 'codexclaw-run-' + id.replace(/[^a-zA-Z0-9._-]/g, '-') + '.json'
}

export const Route = createFileRoute('/api/run-events')({
server: {
handlers: {
GET: ({ request }) => {
try {
const url = new URL(request.url)
const id = url.searchParams.get('id') ?? ''
const payload = getCodexRunEventLog({ id })
if (url.searchParams.get('download') === '1') {
return new Response(JSON.stringify(payload, null, 2), {
headers: {
'content-type': 'application/json; charset=utf-8',
'content-disposition':
'attachment; filename="' + downloadName(id) + '"',
},
})
}
return json(payload)
} catch (err) {
return json(
{
error: err instanceof Error ? err.message : String(err),
},
{ status: 404 },
)
}
},
},
},
})
144 changes: 143 additions & 1 deletion apps/codex-claw/src/screens/chat/components/task-queue-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,21 @@ import { HugeiconsIcon } from '@hugeicons/react'
import {
Cancel01Icon,
CheckmarkCircle01Icon,
Download01Icon,
RefreshIcon,
TimelineIcon,
} from '@hugeicons/core-free-icons'
import {
cancelCodexTask,
fetchCodexTasks,
retryCodexTask,
} from '../chat-queries'
import type { CodexTaskRecord, CodexTaskStatus } from '../types'
import type {
CodexRunTimelineEvent,
CodexRunTokenMetrics,
CodexTaskRecord,
CodexTaskStatus,
} from '../types'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'

Expand Down Expand Up @@ -49,6 +56,65 @@ function formatDuration(task: CodexTaskRecord) {
return String(minutes) + 'm ' + String(remainder) + 's'
}

function formatMs(duration: number) {
if (duration < 1000) return String(Math.max(0, Math.round(duration))) + 'ms'
const seconds = duration / 1000
if (seconds < 60) return seconds.toFixed(seconds < 10 ? 1 : 0) + 's'
const minutes = Math.floor(seconds / 60)
const remainder = Math.round(seconds % 60)
return String(minutes) + 'm ' + String(remainder) + 's'
}

function tokenMetricLabel(metrics?: CodexRunTokenMetrics) {
if (!metrics) return 'Token/context metrics unavailable'
const parts: Array<string> = []
if (typeof metrics.totalTokens === 'number') {
parts.push(metrics.totalTokens.toLocaleString() + ' total')
}
if (typeof metrics.inputTokens === 'number') {
parts.push(metrics.inputTokens.toLocaleString() + ' in')
}
if (typeof metrics.outputTokens === 'number') {
parts.push(metrics.outputTokens.toLocaleString() + ' out')
}
if (typeof metrics.contextTokens === 'number') {
parts.push(metrics.contextTokens.toLocaleString() + ' context')
}
return parts.length > 0
? parts.join(' · ')
: 'Token/context metrics unavailable'
}

function eventClass(kind: CodexRunTimelineEvent['kind']) {
if (kind === 'error') return 'bg-red-100 text-red-700'
if (kind === 'exit' || kind === 'final-message') {
return 'bg-emerald-100 text-emerald-700'
}
if (kind === 'tool-call' || kind === 'tool-result') {
return 'bg-blue-100 text-blue-700'
}
return 'bg-primary-100 text-primary-700'
}

function eventMeta(event: CodexRunTimelineEvent) {
const parts: Array<string> = []
if (event.commandName) parts.push(event.commandName)
if (typeof event.exitCode === 'number') {
parts.push('exit ' + String(event.exitCode))
} else if (event.exitCode === null) {
parts.push('exit unavailable')
}
if (event.status) parts.push(event.status)
return parts.join(' · ')
}

function exportRunEvents(task: CodexTaskRecord) {
if (typeof window === 'undefined') return
const url =
'/api/run-events?id=' + encodeURIComponent(task.id) + '&download=1'
window.location.assign(url)
}

function canCancel(task: CodexTaskRecord) {
return task.status === 'queued' || task.status === 'running'
}
Expand Down Expand Up @@ -184,6 +250,14 @@ export function TaskQueuePanel({ open, onClose }: TaskQueuePanelProps) {
{typeof task.exitCode === 'number' ? task.exitCode : 'open'}
</span>
</div>
<div className="mt-1 grid gap-1 text-xs text-primary-500 sm:grid-cols-2">
<span className="tabular-nums">
{task.timeline.length.toLocaleString()} timeline events
</span>
<span className="truncate tabular-nums">
{tokenMetricLabel(task.tokenMetrics)}
</span>
</div>
{task.error ? (
<div className="mt-1 line-clamp-2 text-xs text-red-600">
{task.error}
Expand All @@ -206,6 +280,74 @@ export function TaskQueuePanel({ open, onClose }: TaskQueuePanelProps) {
>
Retry
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => exportRunEvents(task)}
>
<HugeiconsIcon
icon={Download01Icon}
size={20}
strokeWidth={1.5}
/>
JSON
</Button>
</div>
</div>
<div className="mt-3 rounded-lg border border-primary-200 bg-surface">
<div className="flex items-center gap-2 border-b border-primary-200 px-3 py-2 text-xs text-primary-600">
<HugeiconsIcon
icon={TimelineIcon}
size={20}
strokeWidth={1.5}
/>
<span className="font-medium text-primary-900">
Run timeline
</span>
<span className="tabular-nums">
final status: {statusLabel(task.status)}
</span>
</div>
<div className="max-h-44 overflow-auto">
{task.timeline.length === 0 ? (
<div className="px-3 py-3 text-xs text-primary-500">
No normalized run events recorded for this run.
</div>
) : null}
{task.timeline.map((event) => (
<div
key={event.id}
className="border-t border-primary-100 px-3 py-2 first:border-t-0"
>
<div className="flex min-w-0 items-start gap-2">
<span className="w-16 shrink-0 text-xs tabular-nums text-primary-500">
+{formatMs(event.relativeMs)}
</span>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 flex-wrap items-center gap-2">
<span
className={cn(
'rounded-full px-2 py-0.5 text-[11px]',
eventClass(event.kind),
)}
>
{event.label}
</span>
{eventMeta(event) ? (
<span className="min-w-0 truncate text-xs text-primary-500">
{eventMeta(event)}
</span>
) : null}
</div>
{event.message ? (
<div className="mt-1 line-clamp-2 whitespace-pre-wrap text-xs text-primary-600">
{event.message}
</div>
) : null}
</div>
</div>
</div>
))}
</div>
</div>
</div>
Expand Down
35 changes: 35 additions & 0 deletions apps/codex-claw/src/screens/chat/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,39 @@ export type CodexTaskEvent = {
note?: string
}

export type CodexRunEventKind =
| 'prompt-start'
| 'assistant-delta'
| 'tool-call'
| 'tool-result'
| 'final-message'
| 'exit'
| 'error'
| 'status'

export type CodexRunTokenMetrics = {
inputTokens?: number
outputTokens?: number
totalTokens?: number
contextTokens?: number
raw?: Record<string, unknown>
}

export type CodexRunTimelineEvent = {
id: string
kind: CodexRunEventKind
at: number
relativeMs: number
label: string
message?: string
commandName?: string
toolCallId?: string
exitCode?: number | null
status?: string
tokenMetrics?: CodexRunTokenMetrics
details?: Record<string, unknown>
}

export type CodexTaskRecord = {
id: string
sessionKey: string
Expand All @@ -302,6 +335,8 @@ export type CodexTaskRecord = {
error?: string
retryOf?: string
events: Array<CodexTaskEvent>
timeline: Array<CodexRunTimelineEvent>
tokenMetrics?: CodexRunTokenMetrics
}

export type TaskListResponse = {
Expand Down
Loading
Loading