diff --git a/.specify/feature.json b/.specify/feature.json index 35781f75..1ce695e1 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1,3 @@ { - "feature_directory": "specs/014-a2a-protocol-upgrade" + "feature_directory": "specs/015-chat-message-reliability" } diff --git a/CLAUDE.md b/CLAUDE.md index 383525b8..9d490a4f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,4 +27,9 @@ string. `en.json` per slice is the source, `bun run i18n:sync` generates `ru`, templates use the injected `$t`, and copy decided in script travels as a key. Never hand-write `ru.json` as the first step. `admin/` stays English-only. +**Client state (`admin` + `app`):** read `docs/state.md` before adding a store, +a fetch or a live feed. An entity lives once in its Pinia store: fetches upsert, +pushes patch, components render by id, `useAsyncData` is for loading state only, +optimistic changes go through the store's `patch()` with a rollback. + Project overview: `README.md`. diff --git a/admin/package.json b/admin/package.json index e1bc9b7a..d116a387 100644 --- a/admin/package.json +++ b/admin/package.json @@ -12,7 +12,7 @@ "pretypecheck": "openapi-ts", "typecheck": "nuxt typecheck", "lint": "echo 'admin lint: not configured (see eslint.config.mjs TODO)'", - "test": "echo 'admin test: no tests yet'" + "test": "bun test slices" }, "dependencies": { "@hey-api/client-axios": "^0.6", diff --git a/admin/slices/agent/agent/components/agent/chat/Tab.vue b/admin/slices/agent/agent/components/agent/chat/Tab.vue index f6621498..ba6dc70e 100644 --- a/admin/slices/agent/agent/components/agent/chat/Tab.vue +++ b/admin/slices/agent/agent/components/agent/chat/Tab.vue @@ -45,12 +45,9 @@ const restartUnderway = computed( // reads as "Agent reconnecting…" for 30s on a freshly opened page, because // bridle only sees its own WS. const agentStatusStore = useAgentStatusStore(); -const liveAgent = computed(() => agentStatusStore.agents[props.agent.id]); -// The SSE record is fresher than the row this page fetched — the drift sweep -// flips 'running' → 'unreachable' between refetches. -const displayStatus = computed( - () => liveAgent.value?.status ?? props.agent.status, -); +// `props.agent` is the store record, which the status stream writes into — +// the drift sweep's 'running' → 'unreachable' arrives here by itself. +const displayStatus = computed(() => props.agent.status); const bridleAgentState = computed(() => { if (restartUnderway.value) return 'restarting'; @@ -81,7 +78,7 @@ const hubUnreachable = computed( const offlineHint = computed(() => hubUnreachable.value ? { - reason: liveAgent.value?.statusReason ?? props.agent.statusReason, + reason: props.agent.statusReason, envHref: `/agents/${props.agent.id}?tab=env`, settingsHref: '/settings/bridle', } diff --git a/admin/slices/agent/agent/components/agent/edit/Provider.vue b/admin/slices/agent/agent/components/agent/edit/Provider.vue index c950b328..49ebbfc1 100644 --- a/admin/slices/agent/agent/components/agent/edit/Provider.vue +++ b/admin/slices/agent/agent/components/agent/edit/Provider.vue @@ -33,8 +33,13 @@ const apiUrl = (typeof process !== 'undefined' ? process.env.API_URL : undefined) ?? 'http://localhost:3333'; +// The agent request is here for `pending` / `refresh`; the page renders the +// store's record (docs/state.md), which `fetchById` and every mutation below +// upsert into. +const agent = computed(() => agentStore.byId(props.id)); + const [ - { data: agent, pending: pendingAgent, refresh: refreshAgent }, + { pending: pendingAgent, refresh: refreshAgent }, { data: templates, pending: pendingTemplates }, { pending: pendingLlms }, { data: knowledges, pending: pendingKnowledges }, @@ -180,10 +185,11 @@ async function onPromote() { promoting.value = true; promoteError.value = null; try { - const updated = agent.value.isAdmin - ? await agentStore.demoteAdmin(agent.value.id) - : await agentStore.promoteAdmin(agent.value.id); - agent.value = { ...updated, status: 'deploying' }; + if (agent.value.isAdmin) await agentStore.demoteAdmin(agent.value.id); + else await agentStore.promoteAdmin(agent.value.id); + // Both redeploy the agent; show it until the refetch confirms. No + // rollback — the change itself has already succeeded. + agentStore.patch(props.id, { status: 'deploying' }); await refreshAgent(); } catch (err) { promoteError.value = (err as Error).message || 'Promote failed'; @@ -357,7 +363,7 @@ async function onRemove() { :api-url="apiUrl" :is-public="agent.isPublic" :allowed-origins="agent.allowedOrigins" - @saved="(updated) => (agent = updated)" + @saved="(updated) => agentStore.upsert(updated)" /> @@ -556,8 +562,10 @@ async function onRemove() { +
{{ part.text }}
+ + +{{ part.text }}
+{{ message.text }}
-{{ message.text }}
- - - -{{ message.agentText }}
+
+ {{ message.agentText }}
+ {{ $t(failureHint) }}
+