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