feat: journal dashboard widgets, agent polish, and edit-mode live previews
CI / checks (push) Failing after 2m7s
CI / build (push) Successful in 4m36s

Journal dashboard widgets and quick-add; rich-text quick-add dialogs.

Dashboard draft sync for live edit previews; assistant bubble + API tools.

Journal UX: stress slider, mood grid, query cap fix.
This commit is contained in:
ginnoir
2026-07-04 22:03:45 -05:00
parent 4a924a4107
commit a09747c314
76 changed files with 4594 additions and 611 deletions
+23 -6
View File
@@ -1,13 +1,30 @@
import { expect, test } from "@playwright/test";
test("assistant chat smoke", async ({ page }) => {
await page.goto("/assistant");
await expect(page.getByRole("heading", { name: "Assistant" })).toBeVisible();
test("assistant bubble is hidden until opted in", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("button", { name: "Open assistant" })).toHaveCount(0);
});
await page.getByLabel("Message").fill("hello assistant");
test("assistant chat smoke after opt-in", async ({ page }) => {
await page.goto("/settings?s=appearance");
const assistantSwitch = page.getByRole("switch", { name: "AI assistant" });
if (!(await assistantSwitch.isChecked())) {
await assistantSwitch.click();
}
await page.goto("/");
await page.getByRole("button", { name: "Open assistant" }).click();
await expect(page.getByRole("dialog", { name: "Assistant" })).toBeVisible();
await page.getByLabel("Assistant message").fill("hello assistant");
await page.getByRole("button", { name: "Send" }).click();
await expect(page.getByText("You")).toBeVisible();
await expect(page.getByText("hello assistant")).toBeVisible();
await expect(page.getByText("Assistant", { exact: true }).nth(1)).toBeVisible();
const dialog = page.getByRole("dialog", { name: "Assistant" });
await expect(dialog.locator(".animate-spin").first()).toBeVisible({ timeout: 5000 });
await expect(dialog.locator(".animate-spin")).toHaveCount(0, { timeout: 30_000 });
await page.reload();
await page.getByRole("button", { name: "Open assistant" }).click();
await expect(page.getByText("hello assistant")).toBeVisible();
});
+51
View File
@@ -0,0 +1,51 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { describeToolActivity } from "../../src/modules/agent/tool-labels";
import { encodeSseEvent, thinkingLabel } from "../../src/modules/agent/server/progress";
import { createMockLlmClient } from "../../src/lib/llm/mock";
import { runAgentChat } from "../../src/modules/agent/server/run";
describe("describeToolActivity", () => {
it("uses item text when adding to a list", () => {
const label = describeToolActivity("add_list_item", JSON.stringify({ text: "milk" }));
assert.match(label, /milk/);
});
it("mentions the API path for call_api", () => {
const label = describeToolActivity(
"call_api",
JSON.stringify({ method: "GET", path: "/api/v1/notes" }),
);
assert.match(label, /\/api\/v1\/notes/);
});
});
describe("agent progress", () => {
it("encodes SSE payloads", () => {
const encoded = encodeSseEvent({ type: "thinking", label: "Planning…", round: 0 });
assert.equal(encoded, 'data: {"type":"thinking","label":"Planning…","round":0}\n\n');
});
it("uses different labels per round", () => {
assert.match(thinkingLabel(0), /request/i);
assert.match(thinkingLabel(1), /found/i);
});
it("emits progress events during tool execution", async () => {
const events: string[] = [];
await runAgentChat({
messages: [{ role: "user", content: "add milk to the shopping list" }],
request: new Request("http://localhost:3000/api/agent/chat"),
llm: createMockLlmClient(),
executeTool: async () =>
JSON.stringify({ status: 201, body: { id: "item-1", text: "milk" } }),
onProgress: (event) => {
if (event.type === "tool") events.push(event.label);
if (event.type === "thinking") events.push(event.label);
},
});
assert.ok(events.some((label) => label.includes("milk")));
assert.ok(events.some((label) => /request|found/i.test(label)));
});
});
+52
View File
@@ -0,0 +1,52 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
clearAssistantChat,
loadAssistantChat,
saveAssistantChat,
} from "../../src/modules/agent/assistant-chat-storage";
const storage = new Map<string, string>();
Object.defineProperty(globalThis, "localStorage", {
value: {
getItem: (key: string) => storage.get(key) ?? null,
setItem: (key: string, value: string) => {
storage.set(key, value);
},
removeItem: (key: string) => {
storage.delete(key);
},
},
configurable: true,
});
describe("assistant chat storage", () => {
it("round-trips messages per user", () => {
storage.clear();
const userId = "user-a";
const messages = [
{ role: "user" as const, content: "hello" },
{ role: "assistant" as const, content: "hi there" },
];
saveAssistantChat(userId, messages);
assert.deepEqual(loadAssistantChat(userId), messages);
});
it("keeps sessions isolated by user id", () => {
storage.clear();
saveAssistantChat("user-a", [{ role: "user", content: "for a" }]);
saveAssistantChat("user-b", [{ role: "user", content: "for b" }]);
assert.deepEqual(loadAssistantChat("user-a"), [{ role: "user", content: "for a" }]);
assert.deepEqual(loadAssistantChat("user-b"), [{ role: "user", content: "for b" }]);
});
it("clears stored chat", () => {
storage.clear();
saveAssistantChat("user-a", [{ role: "user", content: "hello" }]);
clearAssistantChat("user-a");
assert.deepEqual(loadAssistantChat("user-a"), []);
});
});