feat: better tool parsing ui

This commit is contained in:
Brooklyn Nicholson
2026-05-04 16:08:44 -05:00
parent d1d0ed4016
commit 5f334e86fd
24 changed files with 1865 additions and 244 deletions
+99
View File
@@ -0,0 +1,99 @@
import { atom } from 'nanostores'
import { sessionTitle } from '@/lib/chat-runtime'
import type { PreviewServerRestart } from '@/store/preview'
import type { ActionStatusResponse, SessionInfo } from '@/types/hermes'
const HISTORY_LIMIT = 8
const COMPLETED_TTL_MS = 5 * 60 * 1000
export type RailTaskStatus = 'error' | 'running' | 'success'
export interface RailTask {
id: string
label: string
detail: string
status: RailTaskStatus
updatedAt: number
}
export interface DesktopActionTask {
status: ActionStatusResponse
updatedAt: number
}
export const $desktopActionTasks = atom<Record<string, DesktopActionTask>>({})
export function upsertDesktopActionTask(status: ActionStatusResponse): void {
$desktopActionTasks.set(prune({ ...$desktopActionTasks.get(), [status.name]: { status, updatedAt: Date.now() } }))
}
export function buildRailTasks(
workingSessionIds: readonly string[],
sessions: readonly SessionInfo[],
previewRestart: PreviewServerRestart | null,
actionTasks: Record<string, DesktopActionTask>
): RailTask[] {
const sessionsById = new Map(sessions.map(session => [session.id, session]))
const sessionTasks: RailTask[] = workingSessionIds.map((id, index) => {
const session = sessionsById.get(id)
return {
id: `session:${id}`,
label: session ? sessionTitle(session) : 'Session task',
detail: 'Agent task running',
status: 'running',
updatedAt: session?.last_active || Date.now() - index
}
})
const previewTasks: RailTask[] = previewRestart
? [
{
id: `preview:${previewRestart.taskId}`,
label: 'Preview restart',
detail: previewRestart.message || previewRestart.url,
status: previewRestart.status === 'error' ? 'error' : previewRestart.status === 'running' ? 'running' : 'success',
updatedAt: Date.now()
}
]
: []
const actions: RailTask[] = Object.values(actionTasks).map(({ status, updatedAt }) => ({
id: `action:${status.name}`,
label: status.name,
detail: actionDetail(status),
status: actionStatus(status),
updatedAt
}))
return [...sessionTasks, ...previewTasks, ...actions].sort((left, right) => right.updatedAt - left.updatedAt)
}
function actionStatus(status: ActionStatusResponse): RailTaskStatus {
if (status.running) {
return 'running'
}
return status.exit_code === 0 ? 'success' : 'error'
}
function actionDetail(status: ActionStatusResponse): string {
if (status.running) {
return 'Running'
}
return status.exit_code === 0 ? 'Completed' : `Failed (${status.exit_code ?? 'unknown'})`
}
function prune(tasks: Record<string, DesktopActionTask>): Record<string, DesktopActionTask> {
const now = Date.now()
return Object.fromEntries(
Object.entries(tasks)
.filter(([, task]) => task.status.running || now - task.updatedAt <= COMPLETED_TTL_MS)
.sort(([, left], [, right]) => right.updatedAt - left.updatedAt)
.slice(0, HISTORY_LIMIT)
)
}
+12 -1
View File
@@ -3,7 +3,7 @@ import { atom } from 'nanostores'
import type { ContextSuggestion } from '@/app/types'
import type { HermesConnection } from '@/global'
import type { ChatMessage } from '@/lib/chat-messages'
import type { SessionInfo } from '@/types/hermes'
import type { SessionInfo, UsageStats } from '@/types/hermes'
type Updater<T> = T | ((current: T) => T)
@@ -34,6 +34,14 @@ export const $currentServiceTier = atom('')
export const $currentFastMode = atom(false)
export const $currentCwd = atom('')
export const $currentBranch = atom('')
export const $currentUsage = atom<UsageStats>({
calls: 0,
input: 0,
output: 0,
total: 0
})
export const $sessionStartedAt = atom<number | null>(null)
export const $turnStartedAt = atom<number | null>(null)
export const $introPersonality = atom('')
export const $currentPersonality = atom('')
export const $availablePersonalities = atom<string[]>([])
@@ -59,6 +67,9 @@ export const setCurrentServiceTier = (next: Updater<string>) => updateAtom($curr
export const setCurrentFastMode = (next: Updater<boolean>) => updateAtom($currentFastMode, next)
export const setCurrentCwd = (next: Updater<string>) => updateAtom($currentCwd, next)
export const setCurrentBranch = (next: Updater<string>) => updateAtom($currentBranch, next)
export const setCurrentUsage = (next: Updater<UsageStats>) => updateAtom($currentUsage, next)
export const setSessionStartedAt = (next: Updater<number | null>) => updateAtom($sessionStartedAt, next)
export const setTurnStartedAt = (next: Updater<number | null>) => updateAtom($turnStartedAt, next)
export const setIntroPersonality = (next: Updater<string>) => updateAtom($introPersonality, next)
export const setCurrentPersonality = (next: Updater<string>) => updateAtom($currentPersonality, next)
export const setAvailablePersonalities = (next: Updater<string[]>) => updateAtom($availablePersonalities, next)
+15
View File
@@ -0,0 +1,15 @@
import { atom } from 'nanostores'
import { persistBoolean, storedBoolean } from '@/lib/storage'
export type ToolViewMode = 'product' | 'technical'
const TOOL_VIEW_TECHNICAL_STORAGE_KEY = 'hermes.desktop.toolView.technical'
export const $toolViewMode = atom<ToolViewMode>(storedBoolean(TOOL_VIEW_TECHNICAL_STORAGE_KEY, false) ? 'technical' : 'product')
$toolViewMode.subscribe(mode => persistBoolean(TOOL_VIEW_TECHNICAL_STORAGE_KEY, mode === 'technical'))
export function setToolViewMode(mode: ToolViewMode) {
$toolViewMode.set(mode)
}