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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import { mergeFeedItem } from "@deck-ui/chat";
|
|
3
|
+
import type { FeedItem } from "@deck-ui/chat";
|
|
4
|
+
|
|
5
|
+
interface FeedState {
|
|
6
|
+
items: Record<string, FeedItem[]>;
|
|
7
|
+
pushFeedItem: (sessionKey: string, item: FeedItem) => void;
|
|
8
|
+
setFeed: (sessionKey: string, items: FeedItem[]) => void;
|
|
9
|
+
clearFeed: (sessionKey: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const useFeedStore = create<FeedState>((set) => ({
|
|
13
|
+
items: {},
|
|
14
|
+
|
|
15
|
+
pushFeedItem: (sessionKey, item) =>
|
|
16
|
+
set((s) => ({
|
|
17
|
+
items: {
|
|
18
|
+
...s.items,
|
|
19
|
+
[sessionKey]: mergeFeedItem(s.items[sessionKey] ?? [], item),
|
|
20
|
+
},
|
|
21
|
+
})),
|
|
22
|
+
|
|
23
|
+
setFeed: (sessionKey, items) =>
|
|
24
|
+
set((s) => ({
|
|
25
|
+
items: { ...s.items, [sessionKey]: items },
|
|
26
|
+
})),
|
|
27
|
+
|
|
28
|
+
clearFeed: (sessionKey) =>
|
|
29
|
+
set((s) => {
|
|
30
|
+
const next = { ...s.items };
|
|
31
|
+
delete next[sessionKey];
|
|
32
|
+
return { items: next };
|
|
33
|
+
}),
|
|
34
|
+
}));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import { tauriIssues } from "../lib/tauri";
|
|
3
|
+
import type { Issue } from "../lib/types";
|
|
4
|
+
|
|
5
|
+
interface IssueState {
|
|
6
|
+
issues: Issue[];
|
|
7
|
+
loading: boolean;
|
|
8
|
+
loadIssues: (projectId: string) => Promise<void>;
|
|
9
|
+
createIssue: (projectId: string, title: string, description: string) => Promise<void>;
|
|
10
|
+
updateIssueStatus: (issueId: string, status: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const useIssueStore = create<IssueState>((set, get) => ({
|
|
14
|
+
issues: [],
|
|
15
|
+
loading: false,
|
|
16
|
+
|
|
17
|
+
loadIssues: async (projectId) => {
|
|
18
|
+
set({ loading: true });
|
|
19
|
+
const issues = await tauriIssues.list(projectId);
|
|
20
|
+
set({ issues, loading: false });
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
createIssue: async (projectId, title, description) => {
|
|
24
|
+
const issue = await tauriIssues.create(projectId, title, description);
|
|
25
|
+
set((s) => ({ issues: [...s.issues, issue] }));
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
updateIssueStatus: (issueId, status) => {
|
|
29
|
+
set((s) => ({
|
|
30
|
+
issues: s.issues.map((i) =>
|
|
31
|
+
i.id === issueId ? { ...i, status } : i,
|
|
32
|
+
),
|
|
33
|
+
}));
|
|
34
|
+
},
|
|
35
|
+
}));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import type { Memory } from "@deck-ui/memory";
|
|
3
|
+
import { tauriMemory } from "../lib/tauri";
|
|
4
|
+
|
|
5
|
+
interface MemoryState {
|
|
6
|
+
memories: Memory[];
|
|
7
|
+
loading: boolean;
|
|
8
|
+
loadMemories: (projectId: string) => Promise<void>;
|
|
9
|
+
deleteMemory: (memoryId: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const useMemoryStore = create<MemoryState>((set) => ({
|
|
13
|
+
memories: [],
|
|
14
|
+
loading: false,
|
|
15
|
+
|
|
16
|
+
loadMemories: async (projectId) => {
|
|
17
|
+
set({ loading: true });
|
|
18
|
+
try {
|
|
19
|
+
const memories = await tauriMemory.list(projectId);
|
|
20
|
+
set({ memories, loading: false });
|
|
21
|
+
} catch {
|
|
22
|
+
set({ loading: false });
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
deleteMemory: (memoryId) =>
|
|
27
|
+
set((s) => ({
|
|
28
|
+
memories: s.memories.filter((m) => m.id !== memoryId),
|
|
29
|
+
})),
|
|
30
|
+
}));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
export type ViewMode = "files" | "instructions";
|
|
4
|
+
|
|
5
|
+
interface UIState {
|
|
6
|
+
viewMode: ViewMode;
|
|
7
|
+
chatOpen: boolean;
|
|
8
|
+
setViewMode: (mode: ViewMode) => void;
|
|
9
|
+
setChatOpen: (open: boolean) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const useUIStore = create<UIState>((set) => ({
|
|
13
|
+
viewMode: "files",
|
|
14
|
+
chatOpen: false,
|
|
15
|
+
setViewMode: (viewMode) => set({ viewMode }),
|
|
16
|
+
setChatOpen: (chatOpen) => set({ chatOpen }),
|
|
17
|
+
}));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "@deck-ui/core/src/globals.css";
|
|
3
|
+
@import "@deck-ui/core/src/styles.css";
|
|
4
|
+
@import "@deck-ui/chat/src/styles.css";
|
|
5
|
+
|
|
6
|
+
/* Scan local source + deck-ui packages for Tailwind classes */
|
|
7
|
+
@source "../";
|
|
8
|
+
@source "../../node_modules/@deck-ui/board/src";
|
|
9
|
+
@source "../../node_modules/@deck-ui/chat/src";
|
|
10
|
+
@source "../../node_modules/@deck-ui/layout/src";
|
|
11
|
+
@source "../../node_modules/@deck-ui/core/src";
|
|
12
|
+
@source "../../node_modules/@deck-ui/skills/src";
|
|
13
|
+
@source "../../node_modules/@deck-ui/routines/src";
|
|
14
|
+
@source "../../node_modules/@deck-ui/connections/src";
|
|
15
|
+
@source "../../node_modules/@deck-ui/events/src";
|
|
16
|
+
@source "../../node_modules/@deck-ui/memory/src";
|
|
17
|
+
@source "../../node_modules/@deck-ui/review/src";
|
|
18
|
+
|
|
19
|
+
body {
|
|
20
|
+
margin: 0;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
font-family: ui-sans-serif, -apple-system, system-ui, "Segoe UI", Helvetica,
|
|
23
|
+
Arial, sans-serif;
|
|
24
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "{{APP_NAME_SNAKE}}"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[lib]
|
|
7
|
+
name = "{{APP_NAME_SNAKE}}_lib"
|
|
8
|
+
crate-type = ["staticlib", "cdylib", "rlib"]
|
|
9
|
+
|
|
10
|
+
[build-dependencies]
|
|
11
|
+
tauri-build = { version = "2", features = [] }
|
|
12
|
+
|
|
13
|
+
[dependencies]
|
|
14
|
+
tauri = { version = "2", features = [] }
|
|
15
|
+
serde = { version = "1", features = ["derive"] }
|
|
16
|
+
serde_json = "1"
|
|
17
|
+
tokio = { version = "1", features = ["full"] }
|
|
18
|
+
uuid = { version = "1", features = ["v4"] }
|
|
19
|
+
keel-tauri = { git = "https://github.com/ja-818/keel-and-deck.git" }
|
|
20
|
+
keel-db = { git = "https://github.com/ja-818/keel-and-deck.git" }
|
|
21
|
+
keel-sessions = { git = "https://github.com/ja-818/keel-and-deck.git" }
|
|
22
|
+
keel-events = { git = "https://github.com/ja-818/keel-and-deck.git" }
|
|
23
|
+
keel-scheduler = { git = "https://github.com/ja-818/keel-and-deck.git" }
|
|
24
|
+
keel-channels = { git = "https://github.com/ja-818/keel-and-deck.git" }
|
|
25
|
+
keel-memory = { git = "https://github.com/ja-818/keel-and-deck.git" }
|
|
26
|
+
anyhow = "1"
|
|
27
|
+
chrono = { version = "0.4", features = ["serde"] }
|
|
28
|
+
libsql = "0.6"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/nickelpack/tauri/v2/crates/tauri-utils/schema.json",
|
|
3
|
+
"identifier": "default",
|
|
4
|
+
"description": "Default capability for the main window",
|
|
5
|
+
"windows": ["main"],
|
|
6
|
+
"permissions": [
|
|
7
|
+
"core:default",
|
|
8
|
+
"core:event:default",
|
|
9
|
+
"core:event:allow-listen",
|
|
10
|
+
"core:event:allow-emit"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
use keel_tauri::state::AppState;
|
|
2
|
+
use tauri::State;
|
|
3
|
+
|
|
4
|
+
/// List all configured channels from the channels table.
|
|
5
|
+
/// Returns an empty list if the table does not exist yet.
|
|
6
|
+
#[tauri::command]
|
|
7
|
+
pub async fn list_channels(
|
|
8
|
+
state: State<'_, AppState>,
|
|
9
|
+
) -> Result<Vec<serde_json::Value>, String> {
|
|
10
|
+
let rows = state
|
|
11
|
+
.db
|
|
12
|
+
.conn()
|
|
13
|
+
.query(
|
|
14
|
+
"SELECT id, channel_type, name, status, config, created_at \
|
|
15
|
+
FROM channels ORDER BY created_at DESC",
|
|
16
|
+
libsql::params![],
|
|
17
|
+
)
|
|
18
|
+
.await;
|
|
19
|
+
|
|
20
|
+
match rows {
|
|
21
|
+
Ok(mut rows) => {
|
|
22
|
+
let mut results = Vec::new();
|
|
23
|
+
while let Ok(Some(row)) = rows.next().await {
|
|
24
|
+
let entry = serde_json::json!({
|
|
25
|
+
"id": row.get::<String>(0).unwrap_or_default(),
|
|
26
|
+
"channel_type": row.get::<String>(1).unwrap_or_default(),
|
|
27
|
+
"name": row.get::<String>(2).unwrap_or_default(),
|
|
28
|
+
"status": row.get::<String>(3).unwrap_or_default(),
|
|
29
|
+
"config": row.get::<String>(4).unwrap_or_default(),
|
|
30
|
+
"created_at": row.get::<String>(5).unwrap_or_default(),
|
|
31
|
+
});
|
|
32
|
+
results.push(entry);
|
|
33
|
+
}
|
|
34
|
+
Ok(results)
|
|
35
|
+
}
|
|
36
|
+
Err(_) => Ok(Vec::new()),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[tauri::command]
|
|
41
|
+
pub async fn add_channel(
|
|
42
|
+
state: State<'_, AppState>,
|
|
43
|
+
channel_type: String,
|
|
44
|
+
name: String,
|
|
45
|
+
config: serde_json::Value,
|
|
46
|
+
) -> Result<serde_json::Value, String> {
|
|
47
|
+
let id = uuid::Uuid::new_v4().to_string();
|
|
48
|
+
let now = chrono::Utc::now().to_rfc3339();
|
|
49
|
+
let config_str = serde_json::to_string(&config).map_err(|e| e.to_string())?;
|
|
50
|
+
|
|
51
|
+
state
|
|
52
|
+
.db
|
|
53
|
+
.conn()
|
|
54
|
+
.execute(
|
|
55
|
+
"INSERT INTO channels (id, channel_type, name, status, config, created_at) \
|
|
56
|
+
VALUES (?1, ?2, ?3, 'disconnected', ?4, ?5)",
|
|
57
|
+
[&id, &channel_type, &name, &config_str, &now],
|
|
58
|
+
)
|
|
59
|
+
.await
|
|
60
|
+
.map_err(|e| e.to_string())?;
|
|
61
|
+
|
|
62
|
+
Ok(serde_json::json!({
|
|
63
|
+
"id": id,
|
|
64
|
+
"channel_type": channel_type,
|
|
65
|
+
"name": name,
|
|
66
|
+
"status": "disconnected",
|
|
67
|
+
"config": config_str,
|
|
68
|
+
"created_at": now,
|
|
69
|
+
}))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[tauri::command]
|
|
73
|
+
pub async fn remove_channel(
|
|
74
|
+
state: State<'_, AppState>,
|
|
75
|
+
channel_id: String,
|
|
76
|
+
) -> Result<(), String> {
|
|
77
|
+
state
|
|
78
|
+
.db
|
|
79
|
+
.conn()
|
|
80
|
+
.execute("DELETE FROM channels WHERE id = ?1", [&channel_id])
|
|
81
|
+
.await
|
|
82
|
+
.map_err(|e| e.to_string())?;
|
|
83
|
+
Ok(())
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[tauri::command]
|
|
87
|
+
pub async fn connect_channel(
|
|
88
|
+
state: State<'_, AppState>,
|
|
89
|
+
channel_id: String,
|
|
90
|
+
) -> Result<(), String> {
|
|
91
|
+
state
|
|
92
|
+
.db
|
|
93
|
+
.conn()
|
|
94
|
+
.execute(
|
|
95
|
+
"UPDATE channels SET status = 'connecting' WHERE id = ?1",
|
|
96
|
+
[&channel_id],
|
|
97
|
+
)
|
|
98
|
+
.await
|
|
99
|
+
.map_err(|e| e.to_string())?;
|
|
100
|
+
// Actual connection logic would be handled by a background task.
|
|
101
|
+
Ok(())
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#[tauri::command]
|
|
105
|
+
pub async fn disconnect_channel(
|
|
106
|
+
state: State<'_, AppState>,
|
|
107
|
+
channel_id: String,
|
|
108
|
+
) -> Result<(), String> {
|
|
109
|
+
state
|
|
110
|
+
.db
|
|
111
|
+
.conn()
|
|
112
|
+
.execute(
|
|
113
|
+
"UPDATE channels SET status = 'disconnected' WHERE id = ?1",
|
|
114
|
+
[&channel_id],
|
|
115
|
+
)
|
|
116
|
+
.await
|
|
117
|
+
.map_err(|e| e.to_string())?;
|
|
118
|
+
Ok(())
|
|
119
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
use keel_tauri::state::AppState;
|
|
2
|
+
use tauri::State;
|
|
3
|
+
|
|
4
|
+
/// List recent events from the event log table.
|
|
5
|
+
/// Falls back to an empty list if the table does not yet exist.
|
|
6
|
+
#[tauri::command]
|
|
7
|
+
pub async fn list_events(
|
|
8
|
+
state: State<'_, AppState>,
|
|
9
|
+
project_id: String,
|
|
10
|
+
) -> Result<Vec<serde_json::Value>, String> {
|
|
11
|
+
let rows = state
|
|
12
|
+
.db
|
|
13
|
+
.conn()
|
|
14
|
+
.query(
|
|
15
|
+
"SELECT id, input_type, source_channel, source_identifier, \
|
|
16
|
+
payload, project_id, created_at \
|
|
17
|
+
FROM event_log WHERE project_id = ?1 \
|
|
18
|
+
ORDER BY created_at DESC LIMIT 100",
|
|
19
|
+
[project_id],
|
|
20
|
+
)
|
|
21
|
+
.await;
|
|
22
|
+
|
|
23
|
+
match rows {
|
|
24
|
+
Ok(mut rows) => {
|
|
25
|
+
let mut results = Vec::new();
|
|
26
|
+
while let Ok(Some(row)) = rows.next().await {
|
|
27
|
+
let entry = serde_json::json!({
|
|
28
|
+
"id": row.get::<String>(0).unwrap_or_default(),
|
|
29
|
+
"input_type": row.get::<String>(1).unwrap_or_default(),
|
|
30
|
+
"source_channel": row.get::<String>(2).unwrap_or_default(),
|
|
31
|
+
"source_identifier": row.get::<String>(3).unwrap_or_default(),
|
|
32
|
+
"payload": row.get::<String>(4).unwrap_or_default(),
|
|
33
|
+
"project_id": row.get::<String>(5).unwrap_or_default(),
|
|
34
|
+
"created_at": row.get::<String>(6).unwrap_or_default(),
|
|
35
|
+
});
|
|
36
|
+
results.push(entry);
|
|
37
|
+
}
|
|
38
|
+
Ok(results)
|
|
39
|
+
}
|
|
40
|
+
// Table may not exist yet in fresh databases.
|
|
41
|
+
Err(_) => Ok(Vec::new()),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
use keel_tauri::keel_db::Issue;
|
|
2
|
+
use keel_tauri::state::AppState;
|
|
3
|
+
use tauri::State;
|
|
4
|
+
|
|
5
|
+
#[tauri::command]
|
|
6
|
+
pub async fn list_issues(
|
|
7
|
+
state: State<'_, AppState>,
|
|
8
|
+
project_id: String,
|
|
9
|
+
) -> Result<Vec<Issue>, String> {
|
|
10
|
+
state
|
|
11
|
+
.db
|
|
12
|
+
.list_issues(&project_id)
|
|
13
|
+
.await
|
|
14
|
+
.map_err(|e| e.to_string())
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[tauri::command]
|
|
18
|
+
pub async fn create_issue(
|
|
19
|
+
state: State<'_, AppState>,
|
|
20
|
+
project_id: String,
|
|
21
|
+
title: String,
|
|
22
|
+
description: String,
|
|
23
|
+
) -> Result<Issue, String> {
|
|
24
|
+
state
|
|
25
|
+
.db
|
|
26
|
+
.create_issue(&project_id, &title, &description, None)
|
|
27
|
+
.await
|
|
28
|
+
.map_err(|e| e.to_string())
|
|
29
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
use keel_tauri::keel_memory::{Memory, MemoryCategory, MemoryQuery, MemoryStore};
|
|
2
|
+
use tauri::State;
|
|
3
|
+
|
|
4
|
+
#[tauri::command]
|
|
5
|
+
pub async fn list_memories(
|
|
6
|
+
store: State<'_, MemoryStore>,
|
|
7
|
+
project_id: String,
|
|
8
|
+
) -> Result<Vec<Memory>, String> {
|
|
9
|
+
store
|
|
10
|
+
.list_by_project(&project_id)
|
|
11
|
+
.await
|
|
12
|
+
.map_err(|e| e.to_string())
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[tauri::command]
|
|
16
|
+
pub async fn create_memory(
|
|
17
|
+
store: State<'_, MemoryStore>,
|
|
18
|
+
project_id: String,
|
|
19
|
+
content: String,
|
|
20
|
+
category: String,
|
|
21
|
+
tags: Vec<String>,
|
|
22
|
+
) -> Result<Memory, String> {
|
|
23
|
+
let cat: MemoryCategory = category.parse().map_err(|e: anyhow::Error| e.to_string())?;
|
|
24
|
+
store
|
|
25
|
+
.create(&project_id, &content, cat, "user", tags)
|
|
26
|
+
.await
|
|
27
|
+
.map_err(|e| e.to_string())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[tauri::command]
|
|
31
|
+
pub async fn delete_memory(
|
|
32
|
+
store: State<'_, MemoryStore>,
|
|
33
|
+
memory_id: String,
|
|
34
|
+
) -> Result<(), String> {
|
|
35
|
+
store.delete(&memory_id).await.map_err(|e| e.to_string())
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#[tauri::command]
|
|
39
|
+
pub async fn search_memories(
|
|
40
|
+
store: State<'_, MemoryStore>,
|
|
41
|
+
project_id: String,
|
|
42
|
+
query: String,
|
|
43
|
+
) -> Result<Vec<Memory>, String> {
|
|
44
|
+
store
|
|
45
|
+
.search(MemoryQuery {
|
|
46
|
+
project_id: Some(project_id),
|
|
47
|
+
search_text: Some(query),
|
|
48
|
+
..Default::default()
|
|
49
|
+
})
|
|
50
|
+
.await
|
|
51
|
+
.map_err(|e| e.to_string())
|
|
52
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
use crate::workspace;
|
|
2
|
+
use keel_tauri::keel_db::Project;
|
|
3
|
+
use keel_tauri::state::AppState;
|
|
4
|
+
use tauri::State;
|
|
5
|
+
|
|
6
|
+
#[tauri::command]
|
|
7
|
+
pub async fn list_projects(state: State<'_, AppState>) -> Result<Vec<Project>, String> {
|
|
8
|
+
state.db.list_projects().await.map_err(|e| e.to_string())
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
#[tauri::command]
|
|
12
|
+
pub async fn create_project(
|
|
13
|
+
state: State<'_, AppState>,
|
|
14
|
+
name: String,
|
|
15
|
+
folder_path: String,
|
|
16
|
+
) -> Result<Project, String> {
|
|
17
|
+
// Seed workspace files (CLAUDE.md, etc.)
|
|
18
|
+
workspace::seed_workspace(&folder_path);
|
|
19
|
+
|
|
20
|
+
state
|
|
21
|
+
.db
|
|
22
|
+
.create_project(&name, &folder_path)
|
|
23
|
+
.await
|
|
24
|
+
.map_err(|e| e.to_string())
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[tauri::command]
|
|
28
|
+
pub async fn delete_project(
|
|
29
|
+
state: State<'_, AppState>,
|
|
30
|
+
project_id: String,
|
|
31
|
+
) -> Result<(), String> {
|
|
32
|
+
// Only removes from DB — does NOT delete the folder on disk.
|
|
33
|
+
state
|
|
34
|
+
.db
|
|
35
|
+
.delete_project(&project_id)
|
|
36
|
+
.await
|
|
37
|
+
.map_err(|e| e.to_string())
|
|
38
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
use keel_tauri::state::AppState;
|
|
2
|
+
use tauri::State;
|
|
3
|
+
|
|
4
|
+
#[tauri::command]
|
|
5
|
+
pub async fn add_heartbeat(
|
|
6
|
+
state: State<'_, AppState>,
|
|
7
|
+
config: serde_json::Value,
|
|
8
|
+
) -> Result<String, String> {
|
|
9
|
+
let scheduler = state
|
|
10
|
+
.scheduler
|
|
11
|
+
.as_ref()
|
|
12
|
+
.ok_or_else(|| "Scheduler not initialized".to_string())?;
|
|
13
|
+
|
|
14
|
+
let hb_config: keel_scheduler::HeartbeatConfig =
|
|
15
|
+
serde_json::from_value(config).map_err(|e| e.to_string())?;
|
|
16
|
+
|
|
17
|
+
let mut sched = scheduler.lock().await;
|
|
18
|
+
let id = sched.add_heartbeat(hb_config);
|
|
19
|
+
Ok(id)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[tauri::command]
|
|
23
|
+
pub async fn remove_heartbeat(
|
|
24
|
+
state: State<'_, AppState>,
|
|
25
|
+
id: String,
|
|
26
|
+
) -> Result<(), String> {
|
|
27
|
+
let scheduler = state
|
|
28
|
+
.scheduler
|
|
29
|
+
.as_ref()
|
|
30
|
+
.ok_or_else(|| "Scheduler not initialized".to_string())?;
|
|
31
|
+
|
|
32
|
+
let mut sched = scheduler.lock().await;
|
|
33
|
+
sched.remove_heartbeat(&id);
|
|
34
|
+
Ok(())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[tauri::command]
|
|
38
|
+
pub async fn add_cron(
|
|
39
|
+
state: State<'_, AppState>,
|
|
40
|
+
config: serde_json::Value,
|
|
41
|
+
) -> Result<String, String> {
|
|
42
|
+
let scheduler = state
|
|
43
|
+
.scheduler
|
|
44
|
+
.as_ref()
|
|
45
|
+
.ok_or_else(|| "Scheduler not initialized".to_string())?;
|
|
46
|
+
|
|
47
|
+
let cron_config: keel_scheduler::CronJobConfig =
|
|
48
|
+
serde_json::from_value(config).map_err(|e| e.to_string())?;
|
|
49
|
+
|
|
50
|
+
let mut sched = scheduler.lock().await;
|
|
51
|
+
sched.add_cron(cron_config).map_err(|e| e.to_string())
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[tauri::command]
|
|
55
|
+
pub async fn remove_cron(
|
|
56
|
+
state: State<'_, AppState>,
|
|
57
|
+
id: String,
|
|
58
|
+
) -> Result<(), String> {
|
|
59
|
+
let scheduler = state
|
|
60
|
+
.scheduler
|
|
61
|
+
.as_ref()
|
|
62
|
+
.ok_or_else(|| "Scheduler not initialized".to_string())?;
|
|
63
|
+
|
|
64
|
+
let mut sched = scheduler.lock().await;
|
|
65
|
+
sched.remove_cron(&id);
|
|
66
|
+
Ok(())
|
|
67
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
use crate::workspace;
|
|
2
|
+
use keel_tauri::chat_session::ChatSessionState;
|
|
3
|
+
use keel_tauri::paths::expand_tilde;
|
|
4
|
+
use keel_tauri::session_runner::{spawn_and_monitor, PersistOptions};
|
|
5
|
+
use keel_tauri::state::AppState;
|
|
6
|
+
use tauri::State;
|
|
7
|
+
|
|
8
|
+
#[tauri::command]
|
|
9
|
+
pub async fn start_session(
|
|
10
|
+
app_handle: tauri::AppHandle,
|
|
11
|
+
state: State<'_, AppState>,
|
|
12
|
+
chat_state: State<'_, ChatSessionState>,
|
|
13
|
+
project_id: String,
|
|
14
|
+
prompt: String,
|
|
15
|
+
) -> Result<String, String> {
|
|
16
|
+
let project = state
|
|
17
|
+
.db
|
|
18
|
+
.get_project(&project_id)
|
|
19
|
+
.await
|
|
20
|
+
.map_err(|e| e.to_string())?
|
|
21
|
+
.ok_or_else(|| "Project not found".to_string())?;
|
|
22
|
+
|
|
23
|
+
let working_dir = expand_tilde(&project.folder_path);
|
|
24
|
+
|
|
25
|
+
// Seed workspace files on first use.
|
|
26
|
+
workspace::seed_workspace(&project.folder_path);
|
|
27
|
+
|
|
28
|
+
// Build system prompt from CLAUDE.md if it exists.
|
|
29
|
+
let system_prompt = {
|
|
30
|
+
let claude_md = working_dir.join("CLAUDE.md");
|
|
31
|
+
if claude_md.exists() {
|
|
32
|
+
std::fs::read_to_string(&claude_md).ok()
|
|
33
|
+
} else {
|
|
34
|
+
None
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Resume previous session if we have one.
|
|
39
|
+
let resume_id = chat_state.get();
|
|
40
|
+
|
|
41
|
+
let session_key = "main".to_string();
|
|
42
|
+
|
|
43
|
+
let _handle = spawn_and_monitor(
|
|
44
|
+
&app_handle,
|
|
45
|
+
&session_key,
|
|
46
|
+
&prompt,
|
|
47
|
+
resume_id.as_deref(),
|
|
48
|
+
Some(working_dir),
|
|
49
|
+
system_prompt.as_deref(),
|
|
50
|
+
Some(chat_state.inner().clone()),
|
|
51
|
+
Some(PersistOptions {
|
|
52
|
+
db: state.db.clone(),
|
|
53
|
+
project_id: project_id.clone(),
|
|
54
|
+
feed_key: session_key.clone(),
|
|
55
|
+
source: "desktop".to_string(),
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
Ok(session_key)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[tauri::command]
|
|
63
|
+
pub async fn load_chat_feed(
|
|
64
|
+
state: State<'_, AppState>,
|
|
65
|
+
project_id: String,
|
|
66
|
+
feed_key: String,
|
|
67
|
+
) -> Result<Vec<serde_json::Value>, String> {
|
|
68
|
+
let rows = state
|
|
69
|
+
.db
|
|
70
|
+
.list_chat_feed(&project_id, &feed_key)
|
|
71
|
+
.await
|
|
72
|
+
.map_err(|e| e.to_string())?;
|
|
73
|
+
|
|
74
|
+
let items: Vec<serde_json::Value> = rows
|
|
75
|
+
.into_iter()
|
|
76
|
+
.filter_map(|row| serde_json::from_str(&row.data_json).ok())
|
|
77
|
+
.collect();
|
|
78
|
+
|
|
79
|
+
Ok(items)
|
|
80
|
+
}
|