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.
- package/index.js +76 -0
- package/package.json +23 -0
- package/template/index.html +12 -0
- package/template/package.json +41 -0
- package/template/src/App.tsx +193 -0
- package/template/src/hooks/use-session-events.ts +36 -0
- package/template/src/lib/tauri.ts +68 -0
- package/template/src/lib/types.ts +111 -0
- package/template/src/main.tsx +10 -0
- package/template/src/stores/agents.ts +65 -0
- package/template/src/stores/events.ts +27 -0
- package/template/src/stores/feeds.ts +34 -0
- package/template/src/stores/issues.ts +35 -0
- package/template/src/stores/memory.ts +30 -0
- package/template/src/stores/ui.ts +17 -0
- package/template/src/styles/globals.css +24 -0
- package/template/src-tauri/Cargo.toml +28 -0
- package/template/src-tauri/build.rs +3 -0
- package/template/src-tauri/capabilities/default.json +12 -0
- package/template/src-tauri/icons/.gitkeep +0 -0
- package/template/src-tauri/src/commands/channels.rs +119 -0
- package/template/src-tauri/src/commands/events.rs +43 -0
- package/template/src-tauri/src/commands/issues.rs +29 -0
- package/template/src-tauri/src/commands/memory.rs +52 -0
- package/template/src-tauri/src/commands/mod.rs +8 -0
- package/template/src-tauri/src/commands/projects.rs +38 -0
- package/template/src-tauri/src/commands/scheduler.rs +67 -0
- package/template/src-tauri/src/commands/sessions.rs +80 -0
- package/template/src-tauri/src/commands/workspace.rs +62 -0
- package/template/src-tauri/src/lib.rs +93 -0
- package/template/src-tauri/src/main.rs +6 -0
- package/template/src-tauri/src/workspace.rs +21 -0
- package/template/src-tauri/tauri.conf.json +30 -0
- package/template/tsconfig.json +21 -0
- 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,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
|
+
}));
|