style(web): fix prettier config, format entire web/ tree
.prettierrc.json was missing "semi": false, so prettier wanted to add semicolons to a codebase written without them (763 semicolon-free statements vs. 150 with, in hand-written .ts; zero hand-written .svelte files use them at all). That's why prettier --check failed on 249 files — not because the code was unformatted, but because the config didn't match the actual house style. Added "semi": false; left printWidth/etc as configured (printWidth barely moves the failure count: 218/213/212 files at 100/120/140). Ran `prettier --write .` with the corrected config. Verified semantics-preserving before and after: - eslint: 142 problems both before and after, byte-identical - build passes, 38/38 tests pass - token-stream diff (whitespace/semicolons/quotes normalized) on all 218 changed files: only 52 had any remaining token change, all either trailing-comma removal (matching trailingComma: "none") or import/ ternary reflow — no semantic changes - live smoke test: Knowledge, Tasks, Fleet map, and a chat window (AgentTrace, markdown, Scope graph, activity rail) all render correctly, no console errors Most of the diff is shadcn/ui vendor files (lib/components/ui/) moving from the CLI's own style (double quotes, tabs, semicolons) to house style; re-running `shadcn-svelte add` on a component will need a follow-up format pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,9 +7,19 @@ export { type ToolCallResult }
|
||||
|
||||
export interface ActivityEntry {
|
||||
id: string
|
||||
type: 'goal' | 'plan' | 'step_running' | 'step_done' | 'step_failed' |
|
||||
'tool_running' | 'tool_done' | 'tool_error' |
|
||||
'knowledge' | 'complete' | 'question' | 'error'
|
||||
type:
|
||||
| 'goal'
|
||||
| 'plan'
|
||||
| 'step_running'
|
||||
| 'step_done'
|
||||
| 'step_failed'
|
||||
| 'tool_running'
|
||||
| 'tool_done'
|
||||
| 'tool_error'
|
||||
| 'knowledge'
|
||||
| 'complete'
|
||||
| 'question'
|
||||
| 'error'
|
||||
description: string
|
||||
detail?: string
|
||||
args?: string
|
||||
@@ -43,13 +53,23 @@ function stringifyResult(result: unknown): string {
|
||||
// Pure derivation, parameterized so it can back both the global "current
|
||||
// session" activityLog below and a per-session activityLogFor(sessionId) for
|
||||
// a floating task window.
|
||||
function computeActivityLog($msgs: ChatMessage[], $steps: PlanStep[], $task: Session | null): ActivityEntry[] {
|
||||
function computeActivityLog(
|
||||
$msgs: ChatMessage[],
|
||||
$steps: PlanStep[],
|
||||
$task: Session | null
|
||||
): ActivityEntry[] {
|
||||
const entries: ActivityEntry[] = []
|
||||
const now = Date.now()
|
||||
|
||||
// Goal
|
||||
if ($task?.goal) {
|
||||
entries.push({ id: 'goal', type: 'goal', description: $task.goal, timestamp: 0, status: 'done' })
|
||||
entries.push({
|
||||
id: 'goal',
|
||||
type: 'goal',
|
||||
description: $task.goal,
|
||||
timestamp: 0,
|
||||
status: 'done'
|
||||
})
|
||||
}
|
||||
|
||||
// Plan steps
|
||||
@@ -58,7 +78,8 @@ function computeActivityLog($msgs: ChatMessage[], $steps: PlanStep[], $task: Ses
|
||||
const stepLabel = s.title || `Step ${s.seq}`
|
||||
entries.push({
|
||||
id: s.id,
|
||||
type: s.status === 'running' ? 'step_running' : s.status === 'done' ? 'step_done' : 'step_failed',
|
||||
type:
|
||||
s.status === 'running' ? 'step_running' : s.status === 'done' ? 'step_done' : 'step_failed',
|
||||
description: `Step ${s.seq}: ${stepLabel}`,
|
||||
detail: s.detail || undefined,
|
||||
timestamp: s.started_at ? new Date(s.started_at).getTime() : now,
|
||||
@@ -96,8 +117,8 @@ function computeActivityLog($msgs: ChatMessage[], $steps: PlanStep[], $task: Ses
|
||||
status: 'running'
|
||||
})
|
||||
} else if (t.type === 'tool_result') {
|
||||
const running = entries.find((e) =>
|
||||
e.type === 'tool_running' && e.id === t.id && e.status === 'running'
|
||||
const running = entries.find(
|
||||
(e) => e.type === 'tool_running' && e.id === t.id && e.status === 'running'
|
||||
)
|
||||
if (running && t.error) {
|
||||
running.type = 'tool_error'
|
||||
@@ -182,7 +203,9 @@ export function activityLogFor(sessionId: string): Readable<ActivityEntry[]> {
|
||||
const chat = chatFor(sessionId)
|
||||
const ws = workspaceFor(sessionId)
|
||||
const task = taskFor(sessionId)
|
||||
return derived([chat.messages, ws.planSteps, task], ([$msgs, $steps, $task]) => computeActivityLog($msgs, $steps, $task))
|
||||
return derived([chat.messages, ws.planSteps, task], ([$msgs, $steps, $task]) =>
|
||||
computeActivityLog($msgs, $steps, $task)
|
||||
)
|
||||
}
|
||||
|
||||
// Humanized, past/present-tense description of what a tool call is doing
|
||||
@@ -190,18 +213,28 @@ export function activityLogFor(sessionId: string): Readable<ActivityEntry[]> {
|
||||
// so the chat's agent trace can read as a thinking log instead of an API log.
|
||||
export function toolActivityLabel(t: ToolCallResult): string {
|
||||
const args = t.args ?? {}
|
||||
const str = (v: unknown): string => typeof v === 'string' ? v : ''
|
||||
const str = (v: unknown): string => (typeof v === 'string' ? v : '')
|
||||
switch (t.name) {
|
||||
case 'set_goal': return 'Set goal'
|
||||
case 'propose_plan': return 'Proposed plan'
|
||||
case 'search_knowledge': return `Research: ${str(args.query)}`
|
||||
case 'get_entity': return `Lookup: ${str(args.slug_or_id)}`
|
||||
case 'get_entity_knowledge': return 'Check prior knowledge'
|
||||
case 'get_relations': return 'Check relationships'
|
||||
case 'list_lxcs': return 'List containers'
|
||||
case 'list_entities': return 'List entities'
|
||||
case 'get_health_summary': return 'Fleet health'
|
||||
case 'get_state_snapshot': return 'State snapshot'
|
||||
case 'set_goal':
|
||||
return 'Set goal'
|
||||
case 'propose_plan':
|
||||
return 'Proposed plan'
|
||||
case 'search_knowledge':
|
||||
return `Research: ${str(args.query)}`
|
||||
case 'get_entity':
|
||||
return `Lookup: ${str(args.slug_or_id)}`
|
||||
case 'get_entity_knowledge':
|
||||
return 'Check prior knowledge'
|
||||
case 'get_relations':
|
||||
return 'Check relationships'
|
||||
case 'list_lxcs':
|
||||
return 'List containers'
|
||||
case 'list_entities':
|
||||
return 'List entities'
|
||||
case 'get_health_summary':
|
||||
return 'Fleet health'
|
||||
case 'get_state_snapshot':
|
||||
return 'State snapshot'
|
||||
case 'run': {
|
||||
const purpose = str(args.purpose)
|
||||
const target = str(args.target)
|
||||
@@ -209,14 +242,21 @@ export function toolActivityLabel(t: ToolCallResult): string {
|
||||
if (target) return `Run on ${target}`
|
||||
return 'Run command'
|
||||
}
|
||||
case 'get_execution_status': return 'Check execution'
|
||||
case 'update_plan_step': return 'Update plan'
|
||||
case 'upsert_knowledge': return 'Record knowledge'
|
||||
case 'complete_task': return 'Complete task'
|
||||
case 'ping_service': return 'Check service'
|
||||
case 'ask_operator': return 'Ask operator'
|
||||
case 'get_execution_status':
|
||||
return 'Check execution'
|
||||
case 'update_plan_step':
|
||||
return 'Update plan'
|
||||
case 'upsert_knowledge':
|
||||
return 'Record knowledge'
|
||||
case 'complete_task':
|
||||
return 'Complete task'
|
||||
case 'ping_service':
|
||||
return 'Check service'
|
||||
case 'ask_operator':
|
||||
return 'Ask operator'
|
||||
// Unmapped tool (new/uncommon) — humanize the raw name rather than
|
||||
// showing it verbatim, e.g. "revoke_execution" -> "Revoke execution".
|
||||
default: return t.name.replace(/_/g, ' ').replace(/^./, (c) => c.toUpperCase())
|
||||
default:
|
||||
return t.name.replace(/_/g, ' ').replace(/^./, (c) => c.toUpperCase())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,12 @@ function storedConfig(): BackgroundConfig {
|
||||
return {
|
||||
pattern: VALID_IDS.has(parsed.pattern) ? parsed.pattern : DEFAULTS.pattern,
|
||||
color: isHexColor(parsed.color) ? parsed.color : DEFAULTS.color,
|
||||
fillColor: parsed.fillColor === null ? null : isHexColor(parsed.fillColor) ? parsed.fillColor : DEFAULTS.fillColor,
|
||||
fillColor:
|
||||
parsed.fillColor === null
|
||||
? null
|
||||
: isHexColor(parsed.fillColor)
|
||||
? parsed.fillColor
|
||||
: DEFAULTS.fillColor,
|
||||
opacity: clamp01(parsed.opacity, DEFAULTS.opacity),
|
||||
fade: clamp01(parsed.fade, DEFAULTS.fade),
|
||||
scale: clampScale(parsed.scale),
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { writable, get, type Writable } from 'svelte/store'
|
||||
import { streamChat, fetchSessions, fetchMessages, fetchMessagesOrNotFound, deleteSession as apiDeleteSession } from '$lib/api'
|
||||
import {
|
||||
streamChat,
|
||||
fetchSessions,
|
||||
fetchMessages,
|
||||
fetchMessagesOrNotFound,
|
||||
deleteSession as apiDeleteSession
|
||||
} from '$lib/api'
|
||||
import type { ChatEvent, Session, Message } from '$lib/api'
|
||||
import type { ToolCallResult } from '$lib/types'
|
||||
|
||||
@@ -46,7 +52,11 @@ function extractApprovals(tools: ToolCallResult[]): PendingApproval[] {
|
||||
const purpose = typeof args.purpose === 'string' ? args.purpose : undefined
|
||||
out.push({
|
||||
executionId: m[1],
|
||||
action: purpose ? purpose.slice(0, 60) : typeof args.action === 'string' ? args.action : t.name,
|
||||
action: purpose
|
||||
? purpose.slice(0, 60)
|
||||
: typeof args.action === 'string'
|
||||
? args.action
|
||||
: t.name,
|
||||
target: typeof args.target === 'string' ? args.target : 'unknown',
|
||||
destructive: /\bDESTRUCTIVE\b/.test(text),
|
||||
command: typeof args.command === 'string' ? args.command : undefined,
|
||||
@@ -78,7 +88,6 @@ export function addChatError(message: string, action?: string) {
|
||||
chatErrors.update((e) => [...e, { id: crypto.randomUUID(), message, action }])
|
||||
}
|
||||
|
||||
|
||||
// Per-session controller tracking. Multiple tasks can stream concurrently
|
||||
// (see sendMessage's session guard above this used to be a single global
|
||||
// `activeController`, which meant cancelStream()/newChat() always aborted
|
||||
@@ -281,9 +290,7 @@ export function sendMessage(text: string) {
|
||||
messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
if (last && last.role === 'assistant') {
|
||||
const tools = last.tools.map((t) =>
|
||||
t.id === ev.data.id ? updated : t
|
||||
)
|
||||
const tools = last.tools.map((t) => (t.id === ev.data.id ? updated : t))
|
||||
ms[ms.length - 1] = { ...last, tools, pendingApprovals: extractApprovals(tools) }
|
||||
}
|
||||
return [...ms]
|
||||
@@ -428,7 +435,10 @@ export function reconnect() {
|
||||
(_ev: ChatEvent) => {},
|
||||
(_err: string) => {
|
||||
connectionState.set('disconnected')
|
||||
addChatError('Reconnect failed. The task may still be running — try sending a message to wake the agent.', 'Dismiss')
|
||||
addChatError(
|
||||
'Reconnect failed. The task may still be running — try sending a message to wake the agent.',
|
||||
'Dismiss'
|
||||
)
|
||||
},
|
||||
() => {
|
||||
if (get(currentSession) === sid) {
|
||||
@@ -579,7 +589,13 @@ export function sendSessionMessage(sessionId: string, text: string) {
|
||||
|
||||
const userMsg: ChatMessage = { id: mid(), role: 'user', text, tools: [], pendingApprovals: [] }
|
||||
chat.messages.update((ms) => [...ms, userMsg])
|
||||
const assistantMsg: ChatMessage = { id: mid(), role: 'assistant', text: '', tools: [], pendingApprovals: [] }
|
||||
const assistantMsg: ChatMessage = {
|
||||
id: mid(),
|
||||
role: 'assistant',
|
||||
text: '',
|
||||
tools: [],
|
||||
pendingApprovals: []
|
||||
}
|
||||
chat.messages.update((ms) => [...ms, assistantMsg])
|
||||
|
||||
const activeTools: Map<string, ToolCallResult> = new Map()
|
||||
@@ -591,7 +607,12 @@ export function sendSessionMessage(sessionId: string, text: string) {
|
||||
(ev: ChatEvent) => {
|
||||
if (ev.type === 'session') return // sessionId is already known for a window
|
||||
if (ev.type === 'tool_use') {
|
||||
const tr: ToolCallResult = { type: 'tool_use', name: ev.data.name, id: ev.data.id, args: ev.data.args }
|
||||
const tr: ToolCallResult = {
|
||||
type: 'tool_use',
|
||||
name: ev.data.name,
|
||||
id: ev.data.id,
|
||||
args: ev.data.args
|
||||
}
|
||||
activeTools.set(ev.data.id, tr)
|
||||
chat.messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
@@ -603,7 +624,12 @@ export function sendSessionMessage(sessionId: string, text: string) {
|
||||
} else if (ev.type === 'tool_result') {
|
||||
const existing = activeTools.get(ev.data.id)
|
||||
if (existing) {
|
||||
const updated: ToolCallResult = { ...existing, type: 'tool_result', result: ev.data.result, error: ev.data.error }
|
||||
const updated: ToolCallResult = {
|
||||
...existing,
|
||||
type: 'tool_result',
|
||||
result: ev.data.result,
|
||||
error: ev.data.error
|
||||
}
|
||||
activeTools.set(ev.data.id, updated)
|
||||
chat.messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
@@ -689,7 +715,13 @@ export function cancelSessionStream(sessionId: string) {
|
||||
// window behaves exactly like any other task window.
|
||||
export function startTask(text: string, onSession: (sessionId: string) => void): void {
|
||||
const userMsg: ChatMessage = { id: mid(), role: 'user', text, tools: [], pendingApprovals: [] }
|
||||
const assistantMsg: ChatMessage = { id: mid(), role: 'assistant', text: '', tools: [], pendingApprovals: [] }
|
||||
const assistantMsg: ChatMessage = {
|
||||
id: mid(),
|
||||
role: 'assistant',
|
||||
text: '',
|
||||
tools: [],
|
||||
pendingApprovals: []
|
||||
}
|
||||
const activeTools: Map<string, ToolCallResult> = new Map()
|
||||
let receivedDone = false
|
||||
let sessionId: string | null = null
|
||||
@@ -700,7 +732,12 @@ export function startTask(text: string, onSession: (sessionId: string) => void):
|
||||
const c = chat
|
||||
if (!c || !sessionId) return
|
||||
if (ev.type === 'tool_use') {
|
||||
const tr: ToolCallResult = { type: 'tool_use', name: ev.data.name, id: ev.data.id, args: ev.data.args }
|
||||
const tr: ToolCallResult = {
|
||||
type: 'tool_use',
|
||||
name: ev.data.name,
|
||||
id: ev.data.id,
|
||||
args: ev.data.args
|
||||
}
|
||||
activeTools.set(ev.data.id, tr)
|
||||
c.messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
@@ -712,7 +749,12 @@ export function startTask(text: string, onSession: (sessionId: string) => void):
|
||||
} else if (ev.type === 'tool_result') {
|
||||
const existing = activeTools.get(ev.data.id)
|
||||
if (existing) {
|
||||
const updated: ToolCallResult = { ...existing, type: 'tool_result', result: ev.data.result, error: ev.data.error }
|
||||
const updated: ToolCallResult = {
|
||||
...existing,
|
||||
type: 'tool_result',
|
||||
result: ev.data.result,
|
||||
error: ev.data.error
|
||||
}
|
||||
activeTools.set(ev.data.id, updated)
|
||||
c.messages.update((ms) => {
|
||||
const last = ms[ms.length - 1]
|
||||
@@ -792,7 +834,8 @@ export function startTask(text: string, onSession: (sessionId: string) => void):
|
||||
},
|
||||
() => {
|
||||
if (chat) chat.streaming.set(false)
|
||||
if (sessionId && activeControllers.get(sessionId) === controller) activeControllers.delete(sessionId)
|
||||
if (sessionId && activeControllers.get(sessionId) === controller)
|
||||
activeControllers.delete(sessionId)
|
||||
loadSessions()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -12,10 +12,25 @@ vi.mock('$lib/apps', () => {
|
||||
{ id: 'knowledge' },
|
||||
{ id: 'learning' }
|
||||
]
|
||||
return { apps: { subscribe: (cb: (v: typeof list) => void) => { cb(list); return () => {} } } }
|
||||
return {
|
||||
apps: {
|
||||
subscribe: (cb: (v: typeof list) => void) => {
|
||||
cb(list)
|
||||
return () => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
import { GRID, iconPositions, placeIcon, iconPixelPos, maxCols, getIconPositions, resetIconLayout } from './icons'
|
||||
import {
|
||||
GRID,
|
||||
iconPositions,
|
||||
placeIcon,
|
||||
iconPixelPos,
|
||||
maxCols,
|
||||
getIconPositions,
|
||||
resetIconLayout
|
||||
} from './icons'
|
||||
|
||||
// placeIcon mutates the shared module-level store, so each test starts from
|
||||
// a known, empty layout rather than whatever the previous test (or apps.ts's
|
||||
|
||||
@@ -43,8 +43,15 @@ function persist(positions: Record<string, IconPos>): void {
|
||||
|
||||
iconPositions.subscribe(persist)
|
||||
|
||||
function occupied(positions: Record<string, IconPos>, col: number, row: number, exceptId: string): boolean {
|
||||
return Object.entries(positions).some(([id, p]) => id !== exceptId && p.col === col && p.row === row)
|
||||
function occupied(
|
||||
positions: Record<string, IconPos>,
|
||||
col: number,
|
||||
row: number,
|
||||
exceptId: string
|
||||
): boolean {
|
||||
return Object.entries(positions).some(
|
||||
([id, p]) => id !== exceptId && p.col === col && p.row === row
|
||||
)
|
||||
}
|
||||
|
||||
// Finds the nearest free cell to (col, row) via an expanding ring search,
|
||||
@@ -56,7 +63,8 @@ function nearestFreeCell(
|
||||
row: number,
|
||||
exceptId: string
|
||||
): IconPos {
|
||||
if (!occupied(positions, col, row, exceptId)) return { col: Math.max(0, col), row: Math.max(0, row) }
|
||||
if (!occupied(positions, col, row, exceptId))
|
||||
return { col: Math.max(0, col), row: Math.max(0, row) }
|
||||
for (let radius = 1; radius < 64; radius++) {
|
||||
for (let dc = -radius; dc <= radius; dc++) {
|
||||
for (let dr = -radius; dr <= radius; dr++) {
|
||||
@@ -106,7 +114,10 @@ export function resetIconLayout(): void {
|
||||
// classic OS default: one left-edge column, registry order. Returns the
|
||||
// same positions object if nothing needed seeding (so callers can skip a
|
||||
// no-op set), otherwise a fresh merged object.
|
||||
function seedMissing(positions: Record<string, IconPos>, list: { id: string }[]): Record<string, IconPos> {
|
||||
function seedMissing(
|
||||
positions: Record<string, IconPos>,
|
||||
list: { id: string }[]
|
||||
): Record<string, IconPos> {
|
||||
let next: Record<string, IconPos> | null = null
|
||||
let row = 0
|
||||
for (const app of list) {
|
||||
|
||||
@@ -25,9 +25,9 @@ export const wm = createManager({ defaultSize: { width: 480, height: 560 } })
|
||||
// reach its own titlebar controls. Clamps requested width/height down to
|
||||
// the current viewport and caps maxWidth/maxHeight the same way, so
|
||||
// dragging a resize handle can't push it past the edge either.
|
||||
function clampToDesktop<T extends { width?: number; height?: number; maxWidth?: number; maxHeight?: number }>(
|
||||
init: T
|
||||
): T {
|
||||
function clampToDesktop<
|
||||
T extends { width?: number; height?: number; maxWidth?: number; maxHeight?: number }
|
||||
>(init: T): T {
|
||||
const { viewport } = wm.getState()
|
||||
if (viewport.width <= 0 || viewport.height <= 0) return init
|
||||
return {
|
||||
@@ -114,14 +114,16 @@ export function openAppWindow(appId: string): void {
|
||||
wm.focus(id)
|
||||
return
|
||||
}
|
||||
wm.open(clampToDesktop({
|
||||
id,
|
||||
title: app.title,
|
||||
width: app.width,
|
||||
height: app.height,
|
||||
minWidth: app.minWidth,
|
||||
minHeight: app.minHeight
|
||||
}))
|
||||
wm.open(
|
||||
clampToDesktop({
|
||||
id,
|
||||
title: app.title,
|
||||
width: app.width,
|
||||
height: app.height,
|
||||
minWidth: app.minWidth,
|
||||
minHeight: app.minHeight
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Opens a window for the entity, or focuses (and restores, if minimized) the
|
||||
@@ -152,7 +154,16 @@ export function openNewTaskWindow(): void {
|
||||
wm.focus(NEW_TASK_WINDOW_ID)
|
||||
return
|
||||
}
|
||||
wm.open(clampToDesktop({ id: NEW_TASK_WINDOW_ID, title: 'New task', width: 900, height: 640, minWidth: 600, minHeight: 400 }))
|
||||
wm.open(
|
||||
clampToDesktop({
|
||||
id: NEW_TASK_WINDOW_ID,
|
||||
title: 'New task',
|
||||
width: 900,
|
||||
height: 640,
|
||||
minWidth: 600,
|
||||
minHeight: 400
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
// Same dedupe/restore/focus pattern as openEntityWindow, for a task/session's
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { writable, derived, get, type Writable, type Readable } from 'svelte/store'
|
||||
import { liveEvents, subscribeEvents } from './events'
|
||||
import { currentSession, sessions, loadSessions } from './chat'
|
||||
import { fetchPlan, fetchQuestions, type PlanStep, type SessionQuestion, type Session } from '$lib/api'
|
||||
import {
|
||||
fetchPlan,
|
||||
fetchQuestions,
|
||||
type PlanStep,
|
||||
type SessionQuestion,
|
||||
type Session
|
||||
} from '$lib/api'
|
||||
import type {
|
||||
PlanProposedData,
|
||||
PlanStepEventData,
|
||||
@@ -67,13 +73,18 @@ export const healthDiffs = globalWorkspace.healthDiffs
|
||||
// The task's own fields (goal/status/outcome/summary) live on the session row.
|
||||
// Rather than a dedicated endpoint, derive from the sessions list (already
|
||||
// fetched for the task board) and keep it fresh here on task-lifecycle events.
|
||||
export const currentTask = derived([sessions, currentSession], ([$sessions, $id]) =>
|
||||
$sessions.find((s) => s.id === $id) ?? null
|
||||
export const currentTask = derived(
|
||||
[sessions, currentSession],
|
||||
([$sessions, $id]) => $sessions.find((s) => s.id === $id) ?? null
|
||||
)
|
||||
|
||||
// Events that can change agent_sessions.status/goal/outcome — see applyEventTo.
|
||||
const STATUS_AFFECTING = new Set([
|
||||
'goal.set', 'task.status', 'plan.proposed', 'question.raised', 'question.answered'
|
||||
'goal.set',
|
||||
'task.status',
|
||||
'plan.proposed',
|
||||
'question.raised',
|
||||
'question.answered'
|
||||
])
|
||||
|
||||
let refreshTimer: ReturnType<typeof setTimeout> | null = null
|
||||
@@ -89,7 +100,11 @@ function applyPlanStepEventTo(ws: WorkspaceState, data: PlanStepEventData) {
|
||||
const i = steps.findIndex((s) => (stepID && s.id === stepID) || (seq != null && s.seq === seq))
|
||||
if (i === -1) return steps
|
||||
const next = [...steps]
|
||||
next[i] = { ...next[i], status: data.status ?? next[i].status, execution_id: data.execution_id ?? next[i].execution_id }
|
||||
next[i] = {
|
||||
...next[i],
|
||||
status: data.status ?? next[i].status,
|
||||
execution_id: data.execution_id ?? next[i].execution_id
|
||||
}
|
||||
return next
|
||||
})
|
||||
}
|
||||
@@ -97,7 +112,11 @@ function applyPlanStepEventTo(ws: WorkspaceState, data: PlanStepEventData) {
|
||||
// Applies a live event to `ws` if it belongs to session `sid` — shared by the
|
||||
// global "current session" watcher and every per-session floating-window
|
||||
// watcher, each passing its own target state and session id.
|
||||
function applyEventTo(ws: WorkspaceState, sid: string, ev: { type: string; correlation_id?: string | null; data?: unknown }) {
|
||||
function applyEventTo(
|
||||
ws: WorkspaceState,
|
||||
sid: string,
|
||||
ev: { type: string; correlation_id?: string | null; data?: unknown }
|
||||
) {
|
||||
if (ev.correlation_id !== sid) return
|
||||
const data = (ev.data ?? {}) as Record<string, unknown>
|
||||
|
||||
@@ -120,8 +139,12 @@ function applyEventTo(ws: WorkspaceState, sid: string, ev: { type: string; corre
|
||||
const d = data as unknown as PlanProposedData
|
||||
if (Array.isArray(d.steps)) {
|
||||
const incoming = d.steps.map((s) => ({
|
||||
id: s.id, seq: s.seq, title: s.title, detail: s.detail ?? '',
|
||||
status: 'pending' as const, target_slug: s.target_slug || undefined
|
||||
id: s.id,
|
||||
seq: s.seq,
|
||||
title: s.title,
|
||||
detail: s.detail ?? '',
|
||||
status: 'pending' as const,
|
||||
target_slug: s.target_slug || undefined
|
||||
}))
|
||||
ws.planSteps.update((existing) => (d.appended ? [...existing, ...incoming] : incoming))
|
||||
}
|
||||
@@ -135,9 +158,11 @@ function applyEventTo(ws: WorkspaceState, sid: string, ev: { type: string; corre
|
||||
const d = data as unknown as QuestionRaisedData
|
||||
ws.questions.update((qs) => [
|
||||
{
|
||||
id: d.question_id, prompt: d.prompt ?? '',
|
||||
id: d.question_id,
|
||||
prompt: d.prompt ?? '',
|
||||
context: { why: d.why, options: d.options, entities: d.entities },
|
||||
status: 'open', created_at: new Date().toISOString()
|
||||
status: 'open',
|
||||
created_at: new Date().toISOString()
|
||||
},
|
||||
...qs.filter((q) => q.id !== d.question_id)
|
||||
])
|
||||
@@ -154,7 +179,9 @@ function applyEventTo(ws: WorkspaceState, sid: string, ev: { type: string; corre
|
||||
const d = data as unknown as EntityTouchedData
|
||||
if (d.slug) {
|
||||
const now = Date.now()
|
||||
ws.touched.update((t) => [{ slug: d.slug, tool: d.tool ?? '', ts: now }, ...t].slice(0, TOUCHED_MAX))
|
||||
ws.touched.update((t) =>
|
||||
[{ slug: d.slug, tool: d.tool ?? '', ts: now }, ...t].slice(0, TOUCHED_MAX)
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -172,7 +199,12 @@ function applyHealthChangedTo(ws: WorkspaceState, ev: { type: string; data?: unk
|
||||
if (!data.slug) return
|
||||
const isRelevant = get(ws.touched).some((t) => t.slug === data.slug)
|
||||
if (!isRelevant) return
|
||||
ws.healthDiffs.update((d) => [{ slug: data.slug, from: data.from ?? '', to: data.to ?? '', ts: Date.now() }, ...d].slice(0, 8))
|
||||
ws.healthDiffs.update((d) =>
|
||||
[{ slug: data.slug, from: data.from ?? '', to: data.to ?? '', ts: Date.now() }, ...d].slice(
|
||||
0,
|
||||
8
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let hydratedFor: string | null = null
|
||||
|
||||
Reference in New Issue
Block a user