Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions chat-ui/bun.lock

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions chat-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview",
"typecheck": "tsc -b"
"typecheck": "tsc -b",
"test": "vitest run"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.0",
Expand All @@ -33,6 +34,7 @@
"typescript": "^5.7.0",
"@types/react": "^19.1.0",
"@types/react-dom": "^19.1.0",
"@types/node": "^22.15.0"
"@types/node": "^22.15.0",
"vitest": "^2.1.0"
}
}
109 changes: 109 additions & 0 deletions chat-ui/src/lib/__tests__/chat-store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { describe, expect, it } from "vitest";
import { createChatStore, dispatchFrame } from "../chat-store";

function send(
store: ReturnType<typeof createChatStore>,
event: string,
data: Record<string, unknown>,
): void {
dispatchFrame(store, event, JSON.stringify(data));
}

describe("chat-store reducer: text block lifecycle", () => {
it("accumulates text_delta into a single content block for one text_start", () => {
const store = createChatStore();
send(store, "message.assistant_start", { message_id: "a1" });
send(store, "message.text_start", {
message_id: "a1",
text_block_id: "tb_0_0",
index: 0,
});
send(store, "message.text_delta", {
text_block_id: "tb_0_0",
delta: "Hello",
});
send(store, "message.text_delta", {
text_block_id: "tb_0_0",
delta: " world",
});
send(store, "message.text_end", { text_block_id: "tb_0_0" });
send(store, "message.assistant_end", {
message_id: "a1",
interrupted: false,
});

const state = store.getState();
const last = state.messages[state.messages.length - 1];
expect(last?.role).toBe("assistant");
expect(last?.content.length).toBe(1);
expect(last?.content[0]?.type).toBe("text");
expect(last?.content[0]?.text).toBe("Hello world");
expect(last?.content[0]?.blockId).toBe("tb_0_0");
});

it("text_reconcile replaces accumulated delta text instead of appending", () => {
const store = createChatStore();
send(store, "message.assistant_start", { message_id: "a1" });
send(store, "message.text_start", {
message_id: "a1",
text_block_id: "tb_0_0",
index: 0,
});
send(store, "message.text_delta", {
text_block_id: "tb_0_0",
delta: "Hello world",
});
send(store, "message.text_end", { text_block_id: "tb_0_0" });
send(store, "message.assistant_end", {
message_id: "a1",
interrupted: false,
});
send(store, "message.text_reconcile", {
text_block_id: "tb_0_0",
full_text: "Hello world",
});

const state = store.getState();
const last = state.messages[state.messages.length - 1];
expect(last?.content.length).toBe(1);
expect(last?.content[0]?.text).toBe("Hello world");
});

it("text_reconcile with divergent canonical text snaps the block to the final value", () => {
const store = createChatStore();
send(store, "message.assistant_start", { message_id: "a1" });
send(store, "message.text_start", {
message_id: "a1",
text_block_id: "tb_0_0",
index: 0,
});
send(store, "message.text_delta", {
text_block_id: "tb_0_0",
delta: "Hello wrold",
});
send(store, "message.text_end", { text_block_id: "tb_0_0" });
send(store, "message.text_reconcile", {
text_block_id: "tb_0_0",
full_text: "Hello world",
});

const state = store.getState();
const last = state.messages[state.messages.length - 1];
expect(last?.content.length).toBe(1);
expect(last?.content[0]?.text).toBe("Hello world");
});

it("text_reconcile for a block that was never started is a no-op", () => {
const store = createChatStore();
send(store, "message.assistant_start", { message_id: "a1" });
send(store, "message.text_reconcile", {
text_block_id: "tb_0_0",
full_text: "Hello world",
});

const state = store.getState();
const last = state.messages[state.messages.length - 1];
expect(last?.role).toBe("assistant");
expect(last?.content.length).toBe(0);
});
});
2 changes: 1 addition & 1 deletion chat-ui/tsconfig.app.tsbuildinfo
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/app-shell.tsx","./src/components/assistant-message.tsx","./src/components/attachment-strip.tsx","./src/components/attachment-tile.tsx","./src/components/chat-input-toolbar.tsx","./src/components/chat-input.tsx","./src/components/code-block.tsx","./src/components/command-palette.tsx","./src/components/delete-session-dialog.tsx","./src/components/drop-overlay.tsx","./src/components/empty-state.tsx","./src/components/ios-install-banner.tsx","./src/components/keyboard-help-sheet.tsx","./src/components/markdown.tsx","./src/components/message-actions.tsx","./src/components/message-list.tsx","./src/components/message.tsx","./src/components/notification-banner.tsx","./src/components/sidebar-footer.tsx","./src/components/sidebar-panel.tsx","./src/components/sidebar-session-item.tsx","./src/components/sidebar-session-list.tsx","./src/components/theme-toggle.tsx","./src/components/thinking-block.tsx","./src/components/tool-call-card.tsx","./src/components/user-message.tsx","./src/hooks/use-attachments.ts","./src/hooks/use-auto-scroll.ts","./src/hooks/use-bootstrap.ts","./src/hooks/use-chat.ts","./src/hooks/use-drag-drop.ts","./src/hooks/use-focus-heartbeat.ts","./src/hooks/use-keyboard.ts","./src/hooks/use-mobile.ts","./src/hooks/use-notifications.ts","./src/hooks/use-paste.ts","./src/hooks/use-sessions.ts","./src/hooks/use-theme.ts","./src/lib/chat-dispatch-tools.ts","./src/lib/chat-store.ts","./src/lib/chat-types.ts","./src/lib/client.ts","./src/lib/keymap.ts","./src/lib/utils.ts","./src/routes/chat-route.tsx","./src/routes/new-chat-route.tsx","./src/routes/not-found-route.tsx","./src/routes/session-route.tsx","./src/ui/alert-dialog.tsx","./src/ui/avatar.tsx","./src/ui/badge.tsx","./src/ui/button.tsx","./src/ui/card.tsx","./src/ui/collapsible.tsx","./src/ui/command.tsx","./src/ui/dialog.tsx","./src/ui/dropdown-menu.tsx","./src/ui/input.tsx","./src/ui/label.tsx","./src/ui/popover.tsx","./src/ui/scroll-area.tsx","./src/ui/separator.tsx","./src/ui/sheet.tsx","./src/ui/sidebar.tsx","./src/ui/skeleton.tsx","./src/ui/sonner.tsx","./src/ui/tabs.tsx","./src/ui/textarea.tsx","./src/ui/tooltip.tsx"],"version":"5.9.3"}
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/app-shell.tsx","./src/components/assistant-message.tsx","./src/components/attachment-strip.tsx","./src/components/attachment-tile.tsx","./src/components/chat-input-toolbar.tsx","./src/components/chat-input.tsx","./src/components/code-block.tsx","./src/components/command-palette.tsx","./src/components/delete-session-dialog.tsx","./src/components/drop-overlay.tsx","./src/components/empty-state.tsx","./src/components/ios-install-banner.tsx","./src/components/keyboard-help-sheet.tsx","./src/components/markdown.tsx","./src/components/message-actions.tsx","./src/components/message-list.tsx","./src/components/message.tsx","./src/components/notification-banner.tsx","./src/components/sidebar-footer.tsx","./src/components/sidebar-panel.tsx","./src/components/sidebar-session-item.tsx","./src/components/sidebar-session-list.tsx","./src/components/theme-toggle.tsx","./src/components/thinking-block.tsx","./src/components/tool-call-card.tsx","./src/components/user-message.tsx","./src/hooks/use-attachments.ts","./src/hooks/use-auto-scroll.ts","./src/hooks/use-bootstrap.ts","./src/hooks/use-chat.ts","./src/hooks/use-drag-drop.ts","./src/hooks/use-focus-heartbeat.ts","./src/hooks/use-keyboard.ts","./src/hooks/use-mobile.ts","./src/hooks/use-notifications.ts","./src/hooks/use-paste.ts","./src/hooks/use-sessions.ts","./src/hooks/use-theme.ts","./src/lib/chat-dispatch-tools.ts","./src/lib/chat-store.ts","./src/lib/chat-types.ts","./src/lib/client.ts","./src/lib/keymap.ts","./src/lib/utils.ts","./src/lib/__tests__/chat-store.test.ts","./src/routes/chat-route.tsx","./src/routes/new-chat-route.tsx","./src/routes/not-found-route.tsx","./src/routes/session-route.tsx","./src/ui/alert-dialog.tsx","./src/ui/avatar.tsx","./src/ui/badge.tsx","./src/ui/button.tsx","./src/ui/card.tsx","./src/ui/collapsible.tsx","./src/ui/command.tsx","./src/ui/dialog.tsx","./src/ui/dropdown-menu.tsx","./src/ui/input.tsx","./src/ui/label.tsx","./src/ui/popover.tsx","./src/ui/scroll-area.tsx","./src/ui/separator.tsx","./src/ui/sheet.tsx","./src/ui/sidebar.tsx","./src/ui/skeleton.tsx","./src/ui/sonner.tsx","./src/ui/tabs.tsx","./src/ui/textarea.tsx","./src/ui/tooltip.tsx"],"version":"5.9.3"}
2 changes: 1 addition & 1 deletion chat-ui/tsconfig.node.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
"include": ["vite.config.ts", "vitest.config.ts"]
}
2 changes: 1 addition & 1 deletion chat-ui/tsconfig.node.tsbuildinfo
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"root":["./vite.config.ts"],"version":"5.9.3"}
{"root":["./vite.config.ts","./vitest.config.ts"],"version":"5.9.3"}
15 changes: 15 additions & 0 deletions chat-ui/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import path from "node:path";
import { defineConfig } from "vitest/config";

export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
test: {
environment: "node",
include: ["src/**/__tests__/**/*.test.ts"],
globals: false,
},
});
8 changes: 8 additions & 0 deletions public/dashboard/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -2890,6 +2890,14 @@ body {
padding: var(--space-3);
border-bottom: 1px solid var(--color-base-300);
}
/* .dash-filter-search was authored for a horizontal .dash-filter-bar. Inside
the Memory rail (flex column) its flex: 1 grows vertically and pushes the
first list row off-screen. Clamp it back to content height here. */
.dash-split-pane-rail > .dash-filter-search {
flex: 0 0 auto;
max-width: none;
margin-left: 0;
}
.dash-memory-list {
overflow-y: auto;
flex: 1;
Expand Down
Loading
Loading