From 947caf27daa07f6363242e4c4abb9de3b9ff45f6 Mon Sep 17 00:00:00 2001 From: nfebe Date: Sun, 19 Jul 2026 16:07:28 +0100 Subject: [PATCH] feat(ui): List and resume past assistant conversations The assistant's history view showed a hardcoded placeholder list that did nothing, and a chat was lost on reload with no way back to it. Opening history now loads the user's saved conversations, most recent first, and selecting one reopens its full transcript and binds further messages to its scope. Nothing new is stored client-side; the sessions were already persisted on the agent. --- src/components/ai/AssistChat.vue | 40 ++++++++++++++------------- src/services/api.ts | 11 ++++++++ src/stores/assist.test.ts | 34 +++++++++++++++++++++++ src/stores/assist.ts | 47 +++++++++++++++++++++++++++++++- 4 files changed, 112 insertions(+), 20 deletions(-) diff --git a/src/components/ai/AssistChat.vue b/src/components/ai/AssistChat.vue index 54e93ca..ccfa623 100644 --- a/src/components/ai/AssistChat.vue +++ b/src/components/ai/AssistChat.vue @@ -42,16 +42,18 @@ Past conversations -
+
+

Loading conversations

+
+

Your past conversations will appear here.

- -

History is a preview; saved conversations arrive with persistence.

@@ -265,6 +267,7 @@ const newChat = () => { const toggleHistory = () => { view.value = view.value === "history" ? "chat" : "history"; + if (view.value === "history") store.listSessions(); }; const sampleQueries = computed(() => @@ -286,22 +289,21 @@ const runSample = (q: string) => { store.send(q); }; -interface PastSession { - id: string; - title: string; - when: string; -} - -// Placeholder list so the history flow is visible; real data arrives with the -// chat-persistence API. -const pastSessions = ref([ - { id: "1", title: "Why is wordpress-blog unhealthy?", when: "2 hours ago" }, - { id: "2", title: "Summarize recent security events", when: "Yesterday" }, - { id: "3", title: "Which containers restarted recently?", when: "3 days ago" }, -]); +const formatWhen = (iso: string): string => { + const then = new Date(iso).getTime(); + if (Number.isNaN(then)) return ""; + const mins = Math.floor((Date.now() - then) / 60000); + if (mins < 1) return "just now"; + if (mins < 60) return `${mins}m ago`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + if (days < 7) return `${days}d ago`; + return new Date(then).toLocaleDateString(); +}; -const loadPast = (_session: PastSession) => { - // Loading a stored conversation needs the persistence API; return to chat. +const loadPast = async (id: string) => { + await store.loadSession(id); view.value = "chat"; }; diff --git a/src/services/api.ts b/src/services/api.ts index be4204a..a392680 100755 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -457,8 +457,19 @@ export interface AISession { suggested_actions: AISuggestedAction[]; } +export interface AISessionSummary { + id: string; + scope: "system" | "deployment"; + deployment?: string; + status: "ready" | "awaiting_approval"; + title: string; + created_at: string; + updated_at: string; +} + export const aiApi = { status: () => apiClient.get("/ai/status"), + listSessions: () => apiClient.get<{ sessions: AISessionSummary[] }>("/ai/sessions"), createSession: (body: { scope: "system" | "deployment"; deployment?: string; diff --git a/src/stores/assist.test.ts b/src/stores/assist.test.ts index d580978..49efde9 100644 --- a/src/stores/assist.test.ts +++ b/src/stores/assist.test.ts @@ -10,6 +10,8 @@ vi.mock("@/services/api", () => ({ createSession: vi.fn(), sessionMessage: vi.fn(), approveSession: vi.fn(), + listSessions: vi.fn(), + getSession: vi.fn(), }, containersApi: { exec: vi.fn() }, deploymentsApi: { serviceAction: vi.fn(), getServices: vi.fn() }, @@ -55,6 +57,38 @@ describe("assist store", () => { expect(store.session).toBeNull(); }); + it("lists saved sessions for the history view", async () => { + const { aiApi } = await import("@/services/api"); + vi.mocked(aiApi.listSessions).mockResolvedValue({ + data: { + sessions: [ + { id: "ais_2", scope: "system", status: "ready", title: "recent one", updated_at: "2026-07-18T10:00:00Z" }, + ], + }, + } as any); + + const store = useAssistStore(); + await store.listSessions(); + + expect(store.sessions).toHaveLength(1); + expect(store.sessions[0].title).toBe("recent one"); + }); + + it("resumes a saved session and binds later turns to its scope", async () => { + const { aiApi } = await import("@/services/api"); + vi.mocked(aiApi.getSession).mockResolvedValue({ + data: session({ id: "ais_9", scope: "deployment", deployment: "shop" }), + } as any); + + const store = useAssistStore(); + await store.loadSession("ais_9"); + + expect(aiApi.getSession).toHaveBeenCalledWith("ais_9"); + expect(store.session?.id).toBe("ais_9"); + expect(store.scope).toBe("deployment"); + expect(store.deployment).toBe("shop"); + }); + it("creates a session from a seed message", async () => { await enable(); const { aiApi } = await import("@/services/api"); diff --git a/src/stores/assist.ts b/src/stores/assist.ts index e20c18c..6aead75 100644 --- a/src/stores/assist.ts +++ b/src/stores/assist.ts @@ -1,6 +1,13 @@ import { defineStore } from "pinia"; import { ref } from "vue"; -import { aiApi, containersApi, deploymentsApi, type AISession, type AISuggestedAction } from "@/services/api"; +import { + aiApi, + containersApi, + deploymentsApi, + type AISession, + type AISessionSummary, + type AISuggestedAction, +} from "@/services/api"; import { useAIStore } from "@/stores/ai"; import { usePlanFlowStore } from "@/stores/planFlow"; @@ -41,6 +48,9 @@ export const useAssistStore = defineStore("assist", () => { // Per-call decisions while an approval batch is open; once every // pending call is decided the batch is submitted. const decisions = ref>({}); + // Saved sessions the current user can resume. + const sessions = ref([]); + const listing = ref(false); function reset() { session.value = null; @@ -173,6 +183,37 @@ export const useAssistStore = defineStore("assist", () => { } } + // listSessions loads the user's saved conversations for the history view. + async function listSessions() { + listing.value = true; + try { + const { data } = await aiApi.listSessions(); + sessions.value = data.sessions || []; + } catch (err: any) { + error.value = err.response?.data?.error || err.message; + } finally { + listing.value = false; + } + } + + // loadSession reopens a saved conversation and binds later turns to its scope. + async function loadSession(id: string) { + loading.value = true; + error.value = ""; + suggestionOutputs.value = {}; + decisions.value = {}; + try { + const { data } = await aiApi.getSession(id); + session.value = data; + scope.value = data.scope; + deployment.value = data.deployment; + } catch (err: any) { + error.value = err.response?.data?.error || err.message; + } finally { + loading.value = false; + } + } + function close() { visible.value = false; embedded.value = false; @@ -192,8 +233,12 @@ export const useAssistStore = defineStore("assist", () => { runningIndex, suggestionOutputs, decisions, + sessions, + listing, open, send, + listSessions, + loadSession, decide, approveAll, declineAll,