fix: circular import chat↔workspace — extract activityLog to activity.ts
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { activityLog, type ActivityEntry } from '$lib/stores/chat'
|
import { activityLog, type ActivityEntry } from '$lib/stores/activity'
|
||||||
import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle'
|
import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle'
|
||||||
import CircleCheckIcon from '@lucide/svelte/icons/circle-check'
|
import CircleCheckIcon from '@lucide/svelte/icons/circle-check'
|
||||||
import CircleXIcon from '@lucide/svelte/icons/circle-x'
|
import CircleXIcon from '@lucide/svelte/icons/circle-x'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ActivityEntry } from '$lib/stores/chat'
|
import type { ActivityEntry } from '$lib/stores/activity'
|
||||||
import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle'
|
import LoaderCircleIcon from '@lucide/svelte/icons/loader-circle'
|
||||||
import CheckIcon from '@lucide/svelte/icons/check'
|
import CheckIcon from '@lucide/svelte/icons/check'
|
||||||
import XIcon from '@lucide/svelte/icons/x'
|
import XIcon from '@lucide/svelte/icons/x'
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { startWorkspace, planSteps, currentTask } from '$lib/stores/workspace'
|
import { startWorkspace, planSteps, currentTask } from '$lib/stores/workspace'
|
||||||
import { activityLog, streaming } from '$lib/stores/chat'
|
import { activityLog } from '$lib/stores/activity'
|
||||||
|
import { streaming } from '$lib/stores/chat'
|
||||||
import OperatorQuestion from './OperatorQuestion.svelte'
|
import OperatorQuestion from './OperatorQuestion.svelte'
|
||||||
import SessionGraph from './SessionGraph.svelte'
|
import SessionGraph from './SessionGraph.svelte'
|
||||||
import ActivityTimeline from './ActivityTimeline.svelte'
|
import ActivityTimeline from './ActivityTimeline.svelte'
|
||||||
|
|||||||
147
web/src/lib/stores/activity.ts
Normal file
147
web/src/lib/stores/activity.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { derived } from 'svelte/store'
|
||||||
|
import { messages, type ToolCallResult } from './chat'
|
||||||
|
import { planSteps, currentTask } from './workspace'
|
||||||
|
|
||||||
|
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'
|
||||||
|
description: string
|
||||||
|
detail?: string
|
||||||
|
timestamp: number
|
||||||
|
toolName?: string
|
||||||
|
status: 'running' | 'done' | 'failed'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const activityLog = derived([messages, planSteps, currentTask], ([$msgs, $steps, $task]) => {
|
||||||
|
const entries: ActivityEntry[] = []
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
// Goal
|
||||||
|
if ($task?.goal) {
|
||||||
|
entries.push({ id: 'goal', type: 'goal', description: $task.goal, timestamp: 0, status: 'done' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plan steps
|
||||||
|
for (const s of $steps) {
|
||||||
|
if (s.status === 'pending') continue
|
||||||
|
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',
|
||||||
|
description: `Step ${s.seq}: ${stepLabel}`,
|
||||||
|
detail: s.detail || undefined,
|
||||||
|
timestamp: s.started_at ? new Date(s.started_at).getTime() : now,
|
||||||
|
status: s.status === 'running' ? 'running' : s.status === 'done' ? 'done' : 'failed'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool calls (from messages)
|
||||||
|
let entryIdx = 0
|
||||||
|
for (let mi = 0; mi < $msgs.length; mi++) {
|
||||||
|
for (const t of $msgs[mi].tools) {
|
||||||
|
const label = toolActivityLabel(t)
|
||||||
|
if (t.type === 'tool_use') {
|
||||||
|
entries.push({
|
||||||
|
id: t.id ?? `tool_${mi}_${entryIdx++}`,
|
||||||
|
type: 'tool_running',
|
||||||
|
description: label,
|
||||||
|
timestamp: now - ($msgs.length - mi) * 1000,
|
||||||
|
toolName: t.name,
|
||||||
|
status: 'running'
|
||||||
|
})
|
||||||
|
} else if (t.type === 'tool_result') {
|
||||||
|
// Find and update matching tool_use entry
|
||||||
|
const running = entries.find((e) =>
|
||||||
|
e.type === 'tool_running' && e.id === t.id && e.status === 'running'
|
||||||
|
)
|
||||||
|
if (running && t.error) {
|
||||||
|
running.type = 'tool_error'
|
||||||
|
running.status = 'failed'
|
||||||
|
running.description = `${t.name}: ${t.error.slice(0, 80)}`
|
||||||
|
} else if (running) {
|
||||||
|
running.type = 'tool_done'
|
||||||
|
running.status = 'done'
|
||||||
|
running.detail = typeof t.result === 'string'
|
||||||
|
? t.result.slice(0, 200)
|
||||||
|
: JSON.stringify(t.result ?? '').slice(0, 200)
|
||||||
|
} else {
|
||||||
|
entries.push({
|
||||||
|
id: t.id ?? `tool_${mi}_${entryIdx++}`,
|
||||||
|
type: t.error ? 'tool_error' : 'tool_done',
|
||||||
|
description: t.error ? `${t.name}: ${t.error.slice(0, 80)}` : t.name,
|
||||||
|
detail: !t.error ? (typeof t.result === 'string' ? t.result.slice(0, 200) : '') : undefined,
|
||||||
|
timestamp: now - ($msgs.length - mi) * 1000,
|
||||||
|
toolName: t.name,
|
||||||
|
status: t.error ? 'failed' : 'done'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Knowledge recorded — detect from upsert_knowledge tool results
|
||||||
|
for (let mi = 0; mi < $msgs.length; mi++) {
|
||||||
|
for (const t of $msgs[mi].tools) {
|
||||||
|
if (t.type === 'tool_result' && t.name === 'upsert_knowledge' && !t.error) {
|
||||||
|
const title = t.args?.title ?? ''
|
||||||
|
entries.push({
|
||||||
|
id: `knowledge_${mi}`,
|
||||||
|
type: 'knowledge',
|
||||||
|
description: title ? `Recorded: ${title.slice(0, 60)}` : 'Recorded knowledge',
|
||||||
|
timestamp: now - ($msgs.length - mi) * 1000,
|
||||||
|
status: 'done'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Task completion
|
||||||
|
if ($task?.outcome) {
|
||||||
|
entries.push({
|
||||||
|
id: 'complete',
|
||||||
|
type: 'complete',
|
||||||
|
description: $task.summary || `Task ${$task.outcome}`,
|
||||||
|
timestamp: now,
|
||||||
|
status: $task.outcome === 'failure' ? 'failed' : 'done'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort newest first
|
||||||
|
entries.sort((a, b) => b.timestamp - a.timestamp)
|
||||||
|
|
||||||
|
return entries
|
||||||
|
})
|
||||||
|
|
||||||
|
function toolActivityLabel(t: ToolCallResult): string {
|
||||||
|
const args = t.args ?? {}
|
||||||
|
switch (t.name) {
|
||||||
|
case 'set_goal': return 'Set goal'
|
||||||
|
case 'propose_plan': return 'Proposed plan'
|
||||||
|
case 'search_knowledge': return `Research: ${args.query || ''}`
|
||||||
|
case 'get_entity': return `Lookup: ${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 = args.purpose as string || ''
|
||||||
|
const target = (args.target as string) || ''
|
||||||
|
if (purpose) return purpose
|
||||||
|
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'
|
||||||
|
default: return t.name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { writable, derived, get } from 'svelte/store'
|
import { writable, get } from 'svelte/store'
|
||||||
import { streamChat, fetchSessions, fetchMessages, deleteSession as apiDeleteSession } from '$lib/api'
|
import { streamChat, fetchSessions, fetchMessages, deleteSession as apiDeleteSession } from '$lib/api'
|
||||||
import { planSteps, currentTask } from '$lib/stores/workspace'
|
|
||||||
import type { ChatEvent, Session, Message } from '$lib/api'
|
import type { ChatEvent, Session, Message } from '$lib/api'
|
||||||
|
|
||||||
export interface PendingApproval {
|
export interface PendingApproval {
|
||||||
@@ -82,149 +81,6 @@ export function addChatError(message: string, action?: string) {
|
|||||||
chatErrors.update((e) => [...e, { id: crypto.randomUUID(), message, action }])
|
chatErrors.update((e) => [...e, { id: crypto.randomUUID(), message, action }])
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Activity timeline ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export interface ActivityEntry {
|
|
||||||
id: string
|
|
||||||
type: 'goal' | 'plan' | 'step_running' | 'step_done' | 'step_failed' |
|
|
||||||
'tool_running' | 'tool_done' | 'tool_error' |
|
|
||||||
'knowledge' | 'complete' | 'question' | 'error'
|
|
||||||
description: string
|
|
||||||
detail?: string
|
|
||||||
timestamp: number
|
|
||||||
toolName?: string
|
|
||||||
status: 'running' | 'done' | 'failed'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const activityLog = derived([messages, planSteps, currentTask], ([$msgs, $steps, $task]) => {
|
|
||||||
const entries: ActivityEntry[] = []
|
|
||||||
const now = Date.now()
|
|
||||||
|
|
||||||
// Goal
|
|
||||||
if ($task?.goal) {
|
|
||||||
entries.push({ id: 'goal', type: 'goal', description: $task.goal, timestamp: 0, status: 'done' })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Plan steps
|
|
||||||
for (const s of $steps) {
|
|
||||||
if (s.status === 'pending') continue
|
|
||||||
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',
|
|
||||||
description: `Step ${s.seq}: ${stepLabel}`,
|
|
||||||
detail: s.detail || undefined,
|
|
||||||
timestamp: s.started_at ? new Date(s.started_at).getTime() : now,
|
|
||||||
status: s.status === 'running' ? 'running' : s.status === 'done' ? 'done' : 'failed'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tool calls (from messages)
|
|
||||||
let entryIdx = 0
|
|
||||||
for (let mi = 0; mi < $msgs.length; mi++) {
|
|
||||||
for (const t of $msgs[mi].tools) {
|
|
||||||
const label = toolActivityLabel(t)
|
|
||||||
if (t.type === 'tool_use') {
|
|
||||||
entries.push({
|
|
||||||
id: t.id ?? `tool_${mi}_${entryIdx++}`,
|
|
||||||
type: 'tool_running',
|
|
||||||
description: label,
|
|
||||||
timestamp: now - ($msgs.length - mi) * 1000,
|
|
||||||
toolName: t.name,
|
|
||||||
status: 'running'
|
|
||||||
})
|
|
||||||
} else if (t.type === 'tool_result') {
|
|
||||||
// Find and update matching tool_use entry
|
|
||||||
const running = entries.find((e) =>
|
|
||||||
e.type === 'tool_running' && e.id === t.id && e.status === 'running'
|
|
||||||
)
|
|
||||||
if (running && t.error) {
|
|
||||||
running.type = 'tool_error'
|
|
||||||
running.status = 'failed'
|
|
||||||
running.description = `${t.name}: ${t.error.slice(0, 80)}`
|
|
||||||
} else if (running) {
|
|
||||||
running.type = 'tool_done'
|
|
||||||
running.status = 'done'
|
|
||||||
running.detail = typeof t.result === 'string'
|
|
||||||
? t.result.slice(0, 200)
|
|
||||||
: JSON.stringify(t.result ?? '').slice(0, 200)
|
|
||||||
} else {
|
|
||||||
entries.push({
|
|
||||||
id: t.id ?? `tool_${mi}_${entryIdx++}`,
|
|
||||||
type: t.error ? 'tool_error' : 'tool_done',
|
|
||||||
description: t.error ? `${t.name}: ${t.error.slice(0, 80)}` : t.name,
|
|
||||||
detail: !t.error ? (typeof t.result === 'string' ? t.result.slice(0, 200) : '') : undefined,
|
|
||||||
timestamp: now - ($msgs.length - mi) * 1000,
|
|
||||||
toolName: t.name,
|
|
||||||
status: t.error ? 'failed' : 'done'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Knowledge recorded — detect from upsert_knowledge tool results
|
|
||||||
for (let mi = 0; mi < $msgs.length; mi++) {
|
|
||||||
for (const t of $msgs[mi].tools) {
|
|
||||||
if (t.type === 'tool_result' && t.name === 'upsert_knowledge' && !t.error) {
|
|
||||||
const title = t.args?.title ?? ''
|
|
||||||
entries.push({
|
|
||||||
id: `knowledge_${mi}`,
|
|
||||||
type: 'knowledge',
|
|
||||||
description: title ? `Recorded: ${title.slice(0, 60)}` : 'Recorded knowledge',
|
|
||||||
timestamp: now - ($msgs.length - mi) * 1000,
|
|
||||||
status: 'done'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Task completion
|
|
||||||
if ($task?.outcome) {
|
|
||||||
entries.push({
|
|
||||||
id: 'complete',
|
|
||||||
type: 'complete',
|
|
||||||
description: $task.summary || `Task ${$task.outcome}`,
|
|
||||||
timestamp: now,
|
|
||||||
status: $task.outcome === 'failure' ? 'failed' : 'done'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort newest first
|
|
||||||
entries.sort((a, b) => b.timestamp - a.timestamp)
|
|
||||||
|
|
||||||
return entries
|
|
||||||
})
|
|
||||||
|
|
||||||
function toolActivityLabel(t: ToolCallResult): string {
|
|
||||||
const args = t.args ?? {}
|
|
||||||
switch (t.name) {
|
|
||||||
case 'set_goal': return 'Set goal'
|
|
||||||
case 'propose_plan': return 'Proposed plan'
|
|
||||||
case 'search_knowledge': return `Research: ${args.query || ''}`
|
|
||||||
case 'get_entity': return `Lookup: ${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 = args.purpose as string || ''
|
|
||||||
const target = (args.target as string) || ''
|
|
||||||
if (purpose) return purpose
|
|
||||||
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'
|
|
||||||
default: return t.name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Per-session controller tracking. Multiple tasks can stream concurrently
|
// Per-session controller tracking. Multiple tasks can stream concurrently
|
||||||
// (see sendMessage's session guard above this used to be a single global
|
// (see sendMessage's session guard above this used to be a single global
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { messages, streaming, connectionState, currentSession, sendMessage, cancelStream, reconnect, error, chatErrors, dismissError, activityLog } from '$lib/stores/chat'
|
import { messages, streaming, connectionState, currentSession, sendMessage, cancelStream, reconnect, error, chatErrors, dismissError } from '$lib/stores/chat'
|
||||||
|
import { activityLog } from '$lib/stores/activity'
|
||||||
import { currentTask } from '$lib/stores/workspace'
|
import { currentTask } from '$lib/stores/workspace'
|
||||||
import SessionRail from '$lib/components/SessionRail.svelte'
|
import SessionRail from '$lib/components/SessionRail.svelte'
|
||||||
import TaskContextPanel from '$lib/components/TaskContextPanel.svelte'
|
import TaskContextPanel from '$lib/components/TaskContextPanel.svelte'
|
||||||
|
|||||||
Reference in New Issue
Block a user