style(web): fix prettier config, format entire web/ tree
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

.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:
2026-07-27 12:56:07 +02:00
parent b345783eef
commit 873b00ac42
217 changed files with 4945 additions and 3538 deletions

View File

@@ -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())
}
}
}

View File

@@ -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),

View File

@@ -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()
}
)

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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