create-keel-and-deck-app 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/index.js +76 -0
  2. package/package.json +23 -0
  3. package/template/index.html +12 -0
  4. package/template/package.json +41 -0
  5. package/template/src/App.tsx +193 -0
  6. package/template/src/hooks/use-session-events.ts +36 -0
  7. package/template/src/lib/tauri.ts +68 -0
  8. package/template/src/lib/types.ts +111 -0
  9. package/template/src/main.tsx +10 -0
  10. package/template/src/stores/agents.ts +65 -0
  11. package/template/src/stores/events.ts +27 -0
  12. package/template/src/stores/feeds.ts +34 -0
  13. package/template/src/stores/issues.ts +35 -0
  14. package/template/src/stores/memory.ts +30 -0
  15. package/template/src/stores/ui.ts +17 -0
  16. package/template/src/styles/globals.css +24 -0
  17. package/template/src-tauri/Cargo.toml +28 -0
  18. package/template/src-tauri/build.rs +3 -0
  19. package/template/src-tauri/capabilities/default.json +12 -0
  20. package/template/src-tauri/icons/.gitkeep +0 -0
  21. package/template/src-tauri/src/commands/channels.rs +119 -0
  22. package/template/src-tauri/src/commands/events.rs +43 -0
  23. package/template/src-tauri/src/commands/issues.rs +29 -0
  24. package/template/src-tauri/src/commands/memory.rs +52 -0
  25. package/template/src-tauri/src/commands/mod.rs +8 -0
  26. package/template/src-tauri/src/commands/projects.rs +38 -0
  27. package/template/src-tauri/src/commands/scheduler.rs +67 -0
  28. package/template/src-tauri/src/commands/sessions.rs +80 -0
  29. package/template/src-tauri/src/commands/workspace.rs +62 -0
  30. package/template/src-tauri/src/lib.rs +93 -0
  31. package/template/src-tauri/src/main.rs +6 -0
  32. package/template/src-tauri/src/workspace.rs +21 -0
  33. package/template/src-tauri/tauri.conf.json +30 -0
  34. package/template/tsconfig.json +21 -0
  35. package/template/vite.config.ts +17 -0
package/index.js ADDED
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, readdirSync, statSync, readFileSync, writeFileSync } from "fs";
4
+ import { join, resolve, basename } from "path";
5
+ import { fileURLToPath } from "url";
6
+ import { dirname } from "path";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Parse args
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const projectName = process.argv[2];
16
+
17
+ if (!projectName) {
18
+ console.error("\n Usage: npx create-keel-and-deck-app <project-name>\n");
19
+ process.exit(1);
20
+ }
21
+
22
+ if (existsSync(projectName)) {
23
+ console.error(`\n Error: directory "${projectName}" already exists.\n`);
24
+ process.exit(1);
25
+ }
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Copy template
29
+ // ---------------------------------------------------------------------------
30
+
31
+ const templateDir = join(__dirname, "template");
32
+ const targetDir = resolve(projectName);
33
+
34
+ function copyDir(src, dest) {
35
+ mkdirSync(dest, { recursive: true });
36
+
37
+ for (const entry of readdirSync(src)) {
38
+ const srcPath = join(src, entry);
39
+ const destPath = join(dest, entry);
40
+
41
+ if (statSync(srcPath).isDirectory()) {
42
+ copyDir(srcPath, destPath);
43
+ } else {
44
+ let content = readFileSync(srcPath, "utf-8");
45
+ content = content.replaceAll("{{APP_NAME}}", projectName);
46
+ content = content.replaceAll(
47
+ "{{APP_NAME_SNAKE}}",
48
+ projectName.replace(/-/g, "_"),
49
+ );
50
+ content = content.replaceAll(
51
+ "{{APP_NAME_TITLE}}",
52
+ projectName
53
+ .split("-")
54
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
55
+ .join(" "),
56
+ );
57
+ writeFileSync(destPath, content);
58
+ }
59
+ }
60
+ }
61
+
62
+ console.log(`\n Creating ${projectName}...\n`);
63
+ copyDir(templateDir, targetDir);
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Done
67
+ // ---------------------------------------------------------------------------
68
+
69
+ console.log(` Done! To get started:\n`);
70
+ console.log(` cd ${projectName}`);
71
+ console.log(` pnpm install`);
72
+ console.log(` pnpm tauri dev\n`);
73
+ console.log(` Prerequisites:`);
74
+ console.log(` - Rust toolchain (rustup.rs)`);
75
+ console.log(` - Claude CLI (claude.ai/code)`);
76
+ console.log(` - pnpm (pnpm.io)\n`);
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "create-keel-and-deck-app",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a Tauri 2 + React desktop app with the keel-and-deck framework",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-keel-and-deck-app": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "template"
12
+ ],
13
+ "keywords": [
14
+ "tauri",
15
+ "react",
16
+ "desktop",
17
+ "ai",
18
+ "scaffold",
19
+ "keel",
20
+ "deck-ui"
21
+ ],
22
+ "license": "MIT"
23
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{APP_NAME_TITLE}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "{{APP_NAME}}",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc --noEmit && vite build",
9
+ "preview": "vite preview",
10
+ "tauri": "tauri",
11
+ "typecheck": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "@deck-ui/board": "^0.4.0",
15
+ "@deck-ui/chat": "^0.4.0",
16
+ "@deck-ui/connections": "^0.4.0",
17
+ "@deck-ui/core": "^0.2.0",
18
+ "@deck-ui/events": "^0.3.0",
19
+ "@deck-ui/layout": "^0.4.0",
20
+ "@deck-ui/memory": "^0.3.0",
21
+ "@deck-ui/review": "^0.3.0",
22
+ "@deck-ui/routines": "^0.4.0",
23
+ "@deck-ui/skills": "^0.3.0",
24
+ "@deck-ui/workspace": "^0.1.0",
25
+ "@tauri-apps/api": "^2.5.0",
26
+ "react": "^19.1.0",
27
+ "react-dom": "^19.1.0",
28
+ "zustand": "^5.0.5",
29
+ "lucide-react": "^0.577.0"
30
+ },
31
+ "devDependencies": {
32
+ "@tauri-apps/cli": "^2.5.0",
33
+ "@types/react": "^19.2.14",
34
+ "@types/react-dom": "^19.2.3",
35
+ "@tailwindcss/vite": "^4.1.8",
36
+ "@vitejs/plugin-react": "^4.5.2",
37
+ "tailwindcss": "^4.1.8",
38
+ "typescript": "^6.0.2",
39
+ "vite": "^6.3.5"
40
+ }
41
+ }
@@ -0,0 +1,193 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { AppSidebar, TabBar, SplitView } from "@deck-ui/layout";
3
+ import { ChatPanel } from "@deck-ui/chat";
4
+ import type { FeedItem } from "@deck-ui/chat";
5
+ import { FilesBrowser } from "@deck-ui/workspace";
6
+ import { InstructionsPanel } from "@deck-ui/workspace";
7
+ import type { FileEntry, InstructionFile } from "@deck-ui/workspace";
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuTrigger,
13
+ Empty,
14
+ EmptyHeader,
15
+ EmptyTitle,
16
+ EmptyDescription,
17
+ } from "@deck-ui/core";
18
+ import { MessageSquare, Settings, Trash2 } from "lucide-react";
19
+ import { useUIStore, type ViewMode } from "./stores/ui";
20
+ import { useAgentStore } from "./stores/agents";
21
+ import { useFeedStore } from "./stores/feeds";
22
+ import { useSessionEvents } from "./hooks/use-session-events";
23
+ import { tauriSessions, tauriWorkspace } from "./lib/tauri";
24
+ import type { WorkspaceFileInfo } from "./lib/tauri";
25
+
26
+ const TABS = [
27
+ { id: "files", label: "Files" },
28
+ { id: "instructions", label: "Instructions" },
29
+ ];
30
+
31
+ const MAIN_FEED_KEY = "main";
32
+
33
+ export function App() {
34
+ const { viewMode, setViewMode, chatOpen, setChatOpen } = useUIStore();
35
+ const { agents, currentAgent, ready, init, selectAgent, addAgent, deleteAgent } =
36
+ useAgentStore();
37
+ const feedItems = useFeedStore((s) => s.items);
38
+
39
+ useSessionEvents();
40
+
41
+ useEffect(() => {
42
+ init();
43
+ }, [init]);
44
+
45
+ const handleSend = useCallback(
46
+ async (text: string) => {
47
+ if (!currentAgent) return;
48
+ try {
49
+ await tauriSessions.start(currentAgent.id, text);
50
+ } catch (e) {
51
+ console.error("Failed to start session:", e);
52
+ }
53
+ },
54
+ [currentAgent],
55
+ );
56
+
57
+ if (!ready) return null;
58
+
59
+ const chatButton = (
60
+ <button
61
+ onClick={() => setChatOpen(!chatOpen)}
62
+ className="inline-flex items-center gap-1.5 rounded-full h-9 px-3 text-sm font-medium bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
63
+ >
64
+ <MessageSquare className="size-4" />
65
+ Chat
66
+ </button>
67
+ );
68
+
69
+ const settingsMenu = (
70
+ <DropdownMenu>
71
+ <DropdownMenuTrigger asChild>
72
+ <button className="flex items-center justify-center size-9 rounded-lg text-muted-foreground hover:text-foreground hover:bg-accent transition-colors">
73
+ <Settings className="size-4" />
74
+ </button>
75
+ </DropdownMenuTrigger>
76
+ <DropdownMenuContent align="end">
77
+ <DropdownMenuItem
78
+ onClick={() => currentAgent && deleteAgent(currentAgent.id)}
79
+ className="text-destructive focus:text-destructive"
80
+ >
81
+ <Trash2 className="size-4 mr-2" />
82
+ Delete Agent
83
+ </DropdownMenuItem>
84
+ </DropdownMenuContent>
85
+ </DropdownMenu>
86
+ );
87
+
88
+ const mainContent = (
89
+ <div className="flex flex-col flex-1 min-h-0">
90
+ <TabBar
91
+ title={currentAgent?.name ?? "No agent"}
92
+ tabs={TABS}
93
+ activeTab={viewMode}
94
+ onTabChange={(id) => setViewMode(id as ViewMode)}
95
+ actions={
96
+ <div className="flex items-center gap-1">
97
+ {settingsMenu}
98
+ {chatButton}
99
+ </div>
100
+ }
101
+ />
102
+ <div className="flex-1 min-h-0 overflow-auto">
103
+ {viewMode === "files" && <FilesTab />}
104
+ {viewMode === "instructions" && <InstructionsTab />}
105
+ </div>
106
+ </div>
107
+ );
108
+
109
+ const chatPanel = (
110
+ <ChatPanel
111
+ sessionKey={MAIN_FEED_KEY}
112
+ feedItems={feedItems[MAIN_FEED_KEY] ?? []}
113
+ isLoading={false}
114
+ onSend={handleSend}
115
+ placeholder="Ask your agent anything..."
116
+ emptyState={
117
+ <Empty className="border-0">
118
+ <EmptyHeader>
119
+ <EmptyTitle>Start a conversation</EmptyTitle>
120
+ <EmptyDescription>
121
+ Type a message to talk to your agent.
122
+ </EmptyDescription>
123
+ </EmptyHeader>
124
+ </Empty>
125
+ }
126
+ />
127
+ );
128
+
129
+ return (
130
+ <div className="h-screen flex bg-background text-foreground">
131
+ <AppSidebar
132
+ logo={<span className="text-sm font-semibold">{{APP_NAME_TITLE}}</span>}
133
+ items={agents.map((a) => ({ id: a.id, name: a.name }))}
134
+ selectedId={currentAgent?.id}
135
+ onSelect={selectAgent}
136
+ onAdd={addAgent}
137
+ sectionLabel="Your agents"
138
+ >
139
+ {chatOpen ? (
140
+ <SplitView left={mainContent} right={chatPanel} />
141
+ ) : (
142
+ <div className="flex-1 flex flex-col min-h-0">{mainContent}</div>
143
+ )}
144
+ </AppSidebar>
145
+ </div>
146
+ );
147
+ }
148
+
149
+ function FilesTab() {
150
+ const currentAgent = useAgentStore((s) => s.currentAgent);
151
+ // TODO: Wire to tauriWorkspace or a file listing command
152
+ // For now, show empty state
153
+ return (
154
+ <FilesBrowser
155
+ files={[]}
156
+ emptyTitle="Your work shows up here"
157
+ emptyDescription="When your agent creates files, they'll appear here for you to open and review."
158
+ />
159
+ );
160
+ }
161
+
162
+ function InstructionsTab() {
163
+ const currentAgent = useAgentStore((s) => s.currentAgent);
164
+ const [files, setFiles] = useState<InstructionFile[]>([]);
165
+
166
+ useEffect(() => {
167
+ if (!currentAgent) return;
168
+ tauriWorkspace.listFiles(currentAgent.id).then(async (infos) => {
169
+ const loaded: InstructionFile[] = [];
170
+ for (const info of infos.filter((f: WorkspaceFileInfo) => f.exists)) {
171
+ try {
172
+ const content = await tauriWorkspace.readFile(currentAgent.id, info.name);
173
+ loaded.push({ name: info.name, label: info.name, content });
174
+ } catch {
175
+ /* skip unreadable files */
176
+ }
177
+ }
178
+ setFiles(loaded);
179
+ });
180
+ }, [currentAgent]);
181
+
182
+ return (
183
+ <InstructionsPanel
184
+ files={files}
185
+ onSave={async (name, content) => {
186
+ // TODO: Wire to a write_workspace_file Tauri command
187
+ console.log("Save:", name, content.length, "chars");
188
+ }}
189
+ emptyTitle="No instructions yet"
190
+ emptyDescription="Add a CLAUDE.md to this agent's workspace to configure how it behaves."
191
+ />
192
+ );
193
+ }
@@ -0,0 +1,36 @@
1
+ import { useKeelEvent } from "@deck-ui/core";
2
+ import { useFeedStore } from "../stores/feeds";
3
+ import type { FeedItem } from "@deck-ui/chat";
4
+
5
+ interface KeelEventPayload {
6
+ FeedItem?: { session_key: string; item: FeedItem };
7
+ SessionStatus?: { session_key: string; status: string; error?: string };
8
+ Toast?: { message: string; variant: string };
9
+ [key: string]: unknown;
10
+ }
11
+
12
+ /**
13
+ * Subscribe to keel-event from the Rust backend.
14
+ * Dispatches feed items to the feed store.
15
+ * Add more handlers as you add features (kanban, events, memory, etc.).
16
+ */
17
+ export function useSessionEvents() {
18
+ const pushFeedItem = useFeedStore((s) => s.pushFeedItem);
19
+
20
+ useKeelEvent<KeelEventPayload>("keel-event", (payload) => {
21
+ if (payload.FeedItem) {
22
+ pushFeedItem(payload.FeedItem.session_key, payload.FeedItem.item);
23
+ }
24
+
25
+ if (payload.SessionStatus) {
26
+ const { session_key, status, error } = payload.SessionStatus;
27
+ if (status === "error" && error) {
28
+ console.error(`[session:${session_key}]`, error);
29
+ }
30
+ }
31
+
32
+ if (payload.Toast) {
33
+ console.log(`[toast:${payload.Toast.variant}]`, payload.Toast.message);
34
+ }
35
+ });
36
+ }
@@ -0,0 +1,68 @@
1
+ import { invoke } from "@tauri-apps/api/core";
2
+ import type { Project, Issue } from "./types";
3
+ import type { Memory } from "@deck-ui/memory";
4
+
5
+ /** Type-safe wrappers around Tauri invoke() calls. */
6
+
7
+ export const tauriProjects = {
8
+ list: () => invoke<Project[]>("list_projects"),
9
+ create: (name: string, folderPath: string) =>
10
+ invoke<Project>("create_project", { name, folderPath }),
11
+ delete: (projectId: string) =>
12
+ invoke<void>("delete_project", { projectId }),
13
+ };
14
+
15
+ export const tauriIssues = {
16
+ list: (projectId: string) =>
17
+ invoke<Issue[]>("list_issues", { projectId }),
18
+ create: (projectId: string, title: string, description: string) =>
19
+ invoke<Issue>("create_issue", { projectId, title, description }),
20
+ };
21
+
22
+ export const tauriSessions = {
23
+ start: (projectId: string, prompt: string) =>
24
+ invoke<string>("start_session", { projectId, prompt }),
25
+ loadFeed: (projectId: string, feedKey: string) =>
26
+ invoke<unknown[]>("load_chat_feed", { projectId, feedKey }),
27
+ };
28
+
29
+ export const tauriWorkspace = {
30
+ listFiles: (projectId: string) =>
31
+ invoke<WorkspaceFileInfo[]>("list_workspace_files", { projectId }),
32
+ readFile: (projectId: string, fileName: string) =>
33
+ invoke<string>("read_workspace_file", { projectId, fileName }),
34
+ };
35
+
36
+ export interface WorkspaceFileInfo {
37
+ name: string;
38
+ description: string;
39
+ exists: boolean;
40
+ }
41
+
42
+ export const tauriMemory = {
43
+ list: (projectId: string) =>
44
+ invoke<Memory[]>("list_memories", { projectId }),
45
+ create: (projectId: string, content: string, category: string, tags: string[]) =>
46
+ invoke<Memory>("create_memory", { projectId, content, category, tags }),
47
+ delete: (memoryId: string) =>
48
+ invoke<void>("delete_memory", { memoryId }),
49
+ search: (projectId: string, query: string) =>
50
+ invoke<Memory[]>("search_memories", { projectId, query }),
51
+ };
52
+
53
+ export const tauriEvents = {
54
+ list: (projectId: string) =>
55
+ invoke<unknown[]>("list_events", { projectId }),
56
+ };
57
+
58
+ export const tauriChannels = {
59
+ list: () => invoke<unknown[]>("list_channels"),
60
+ connect: (channelId: string) =>
61
+ invoke<void>("connect_channel", { channelId }),
62
+ disconnect: (channelId: string) =>
63
+ invoke<void>("disconnect_channel", { channelId }),
64
+ addChannel: (channelType: string, name: string, config: Record<string, string>) =>
65
+ invoke<unknown>("add_channel", { channelType, name, config }),
66
+ removeChannel: (channelId: string) =>
67
+ invoke<void>("remove_channel", { channelId }),
68
+ };
@@ -0,0 +1,111 @@
1
+ /** Project from keel-db */
2
+ export interface Project {
3
+ id: string;
4
+ name: string;
5
+ folder_path: string;
6
+ created_at: string;
7
+ updated_at: string;
8
+ }
9
+
10
+ /** Issue (kanban card) from keel-db */
11
+ export interface Issue {
12
+ id: string;
13
+ project_id: string;
14
+ title: string;
15
+ description: string;
16
+ status: string;
17
+ tags: string | null;
18
+ position: number;
19
+ session_id: string | null;
20
+ claude_session_id: string | null;
21
+ output_files: string | null;
22
+ created_at: string;
23
+ updated_at: string;
24
+ }
25
+
26
+ /** Events emitted from the Rust backend via keel-tauri */
27
+ export type KeelEvent =
28
+ | {
29
+ type: "FeedItem";
30
+ data: { session_key: string; item: import("@deck-ui/chat").FeedItem };
31
+ }
32
+ | {
33
+ type: "SessionStatus";
34
+ data: { session_key: string; status: string; error: string | null };
35
+ }
36
+ | {
37
+ type: "IssueStatusChanged";
38
+ data: { issue_id: string; status: string };
39
+ }
40
+ | {
41
+ type: "IssuesChanged";
42
+ data: { project_id: string };
43
+ }
44
+ | {
45
+ type: "Toast";
46
+ data: { message: string; variant: string };
47
+ }
48
+ | {
49
+ type: "AuthRequired";
50
+ data: { message: string };
51
+ }
52
+ | {
53
+ type: "CompletionToast";
54
+ data: { title: string; issue_id: string | null };
55
+ }
56
+ | {
57
+ type: "EventReceived";
58
+ data: {
59
+ event_id: string;
60
+ event_type: string;
61
+ source_channel: string;
62
+ source_identifier: string;
63
+ summary: string;
64
+ };
65
+ }
66
+ | {
67
+ type: "EventProcessed";
68
+ data: { event_id: string; status: string };
69
+ }
70
+ | {
71
+ type: "HeartbeatFired";
72
+ data: { prompt: string; project_id: string | null };
73
+ }
74
+ | {
75
+ type: "CronFired";
76
+ data: { job_id: string; job_name: string; prompt: string };
77
+ }
78
+ | {
79
+ type: "ChannelMessageReceived";
80
+ data: {
81
+ channel_type: string;
82
+ channel_id: string;
83
+ sender_name: string;
84
+ text: string;
85
+ };
86
+ }
87
+ | {
88
+ type: "ChannelStatusChanged";
89
+ data: {
90
+ channel_id: string;
91
+ channel_type: string;
92
+ status: string;
93
+ error: string | null;
94
+ };
95
+ }
96
+ | {
97
+ type: "MemoryChanged";
98
+ data: { memory_id: string; project_id: string; category: string };
99
+ }
100
+ | {
101
+ type: "MemoryDeleted";
102
+ data: { memory_id: string; project_id: string };
103
+ }
104
+ | {
105
+ type: "RoutineRunChanged";
106
+ data: { routine_id: string; run_id: string; status: string };
107
+ }
108
+ | {
109
+ type: "RoutinesChanged";
110
+ data: { project_id: string };
111
+ };
@@ -0,0 +1,10 @@
1
+ import { StrictMode } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import { App } from "./App";
4
+ import "./styles/globals.css";
5
+
6
+ createRoot(document.getElementById("root")!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
@@ -0,0 +1,65 @@
1
+ import { create } from "zustand";
2
+ import { tauriProjects } from "../lib/tauri";
3
+ import type { Project } from "../lib/types";
4
+
5
+ const DEFAULT_AGENT_NAME = "{{APP_NAME_TITLE}}";
6
+ const DOCUMENTS_BASE = "~/Documents";
7
+
8
+ interface AgentState {
9
+ agents: Project[];
10
+ currentAgent: Project | null;
11
+ ready: boolean;
12
+ init: () => Promise<void>;
13
+ selectAgent: (id: string) => void;
14
+ addAgent: () => Promise<void>;
15
+ deleteAgent: (id: string) => Promise<void>;
16
+ }
17
+
18
+ export const useAgentStore = create<AgentState>((set, get) => ({
19
+ agents: [],
20
+ currentAgent: null,
21
+ ready: false,
22
+
23
+ init: async () => {
24
+ const projects = await tauriProjects.list();
25
+ if (projects.length === 0) {
26
+ const agent = await tauriProjects.create(
27
+ DEFAULT_AGENT_NAME,
28
+ `${DOCUMENTS_BASE}/${DEFAULT_AGENT_NAME}/`,
29
+ );
30
+ set({ agents: [agent], currentAgent: agent, ready: true });
31
+ return;
32
+ }
33
+ set({ agents: projects, currentAgent: projects[0], ready: true });
34
+ },
35
+
36
+ selectAgent: (id) => {
37
+ const agent = get().agents.find((a) => a.id === id) ?? null;
38
+ set({ currentAgent: agent });
39
+ },
40
+
41
+ addAgent: async () => {
42
+ const name = prompt("Agent name:");
43
+ if (!name?.trim()) return;
44
+ const agent = await tauriProjects.create(
45
+ name.trim(),
46
+ `${DOCUMENTS_BASE}/${name.trim()}/`,
47
+ );
48
+ set((s) => ({
49
+ agents: [...s.agents, agent],
50
+ currentAgent: agent,
51
+ }));
52
+ },
53
+
54
+ deleteAgent: async (id) => {
55
+ await tauriProjects.delete(id);
56
+ set((s) => {
57
+ const agents = s.agents.filter((a) => a.id !== id);
58
+ return {
59
+ agents,
60
+ currentAgent:
61
+ s.currentAgent?.id === id ? agents[0] ?? null : s.currentAgent,
62
+ };
63
+ });
64
+ },
65
+ }));
@@ -0,0 +1,27 @@
1
+ import { create } from "zustand";
2
+ import type { EventEntry } from "@deck-ui/events";
3
+
4
+ interface EventState {
5
+ events: EventEntry[];
6
+ pushEvent: (event: EventEntry) => void;
7
+ updateEventStatus: (eventId: string, status: string) => void;
8
+ clearEvents: () => void;
9
+ }
10
+
11
+ export const useEventStore = create<EventState>((set) => ({
12
+ events: [],
13
+
14
+ pushEvent: (event) =>
15
+ set((s) => ({ events: [...s.events, event] })),
16
+
17
+ updateEventStatus: (eventId, status) =>
18
+ set((s) => ({
19
+ events: s.events.map((e) =>
20
+ e.id === eventId
21
+ ? { ...e, status: status as EventEntry["status"], processedAt: new Date().toISOString() }
22
+ : e
23
+ ),
24
+ })),
25
+
26
+ clearEvents: () => set({ events: [] }),
27
+ }));