@atercates/claude-deck 0.2.3 → 0.2.5
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/app/api/sessions/[id]/fork/route.ts +0 -1
- package/app/api/sessions/[id]/route.ts +0 -5
- package/app/api/sessions/[id]/summarize/route.ts +2 -3
- package/app/api/sessions/route.ts +2 -11
- package/app/api/sessions/status/acknowledge/route.ts +8 -0
- package/app/api/sessions/status/route.ts +2 -233
- package/app/page.tsx +6 -13
- package/components/ClaudeProjects/ClaudeProjectCard.tsx +19 -31
- package/components/ClaudeProjects/ClaudeSessionCard.tsx +20 -31
- package/components/NewSessionDialog/AdvancedSettings.tsx +3 -12
- package/components/NewSessionDialog/NewSessionDialog.types.ts +0 -10
- package/components/NewSessionDialog/ProjectSelector.tsx +2 -7
- package/components/NewSessionDialog/hooks/useNewSessionForm.ts +3 -36
- package/components/NewSessionDialog/index.tsx +0 -7
- package/components/Pane/DesktopTabBar.tsx +62 -28
- package/components/Pane/index.tsx +5 -0
- package/components/Projects/index.ts +0 -1
- package/components/QuickSwitcher.tsx +63 -11
- package/components/SessionList/ActiveSessionsSection.tsx +116 -0
- package/components/SessionList/hooks/useSessionListMutations.ts +0 -35
- package/components/SessionList/index.tsx +9 -1
- package/components/SessionStatusBar.tsx +155 -0
- package/components/WaitingBanner.tsx +122 -0
- package/components/views/DesktopView.tsx +27 -8
- package/components/views/MobileView.tsx +6 -1
- package/components/views/types.ts +2 -0
- package/data/sessions/index.ts +0 -1
- package/data/sessions/queries.ts +1 -27
- package/data/statuses/queries.ts +68 -34
- package/hooks/useSessions.ts +0 -12
- package/lib/claude/watcher.ts +28 -5
- package/lib/db/queries.ts +4 -64
- package/lib/db/types.ts +0 -8
- package/lib/hooks/reporter.ts +116 -0
- package/lib/hooks/setup.ts +164 -0
- package/lib/orchestration.ts +16 -23
- package/lib/providers/registry.ts +3 -57
- package/lib/providers.ts +19 -100
- package/lib/status-monitor.ts +303 -0
- package/package.json +1 -1
- package/server.ts +5 -1
- package/app/api/groups/[...path]/route.ts +0 -136
- package/app/api/groups/route.ts +0 -93
- package/components/NewSessionDialog/AgentSelector.tsx +0 -37
- package/components/Projects/ProjectCard.tsx +0 -276
- package/components/TmuxSessions.tsx +0 -132
- package/data/groups/index.ts +0 -1
- package/data/groups/mutations.ts +0 -95
- package/hooks/useGroups.ts +0 -37
- package/hooks/useKeybarVisibility.ts +0 -42
- package/lib/claude/process-manager.ts +0 -278
- package/lib/status-detector.ts +0 -375
package/lib/db/queries.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { getDb } from "./index";
|
|
2
2
|
import type {
|
|
3
3
|
Session,
|
|
4
|
-
Group,
|
|
5
4
|
Project,
|
|
6
5
|
ProjectDevServer,
|
|
7
6
|
ProjectRepository,
|
|
@@ -39,14 +38,13 @@ export const queries = {
|
|
|
39
38
|
parentSessionId: string | null,
|
|
40
39
|
model: string | null,
|
|
41
40
|
systemPrompt: string | null,
|
|
42
|
-
groupPath: string,
|
|
43
41
|
agentType: string,
|
|
44
42
|
autoApprove: boolean,
|
|
45
43
|
projectId: string | null
|
|
46
44
|
) =>
|
|
47
45
|
execute(
|
|
48
|
-
`INSERT INTO sessions (id, name, tmux_name, working_directory, parent_session_id, model, system_prompt,
|
|
49
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
46
|
+
`INSERT INTO sessions (id, name, tmux_name, working_directory, parent_session_id, model, system_prompt, agent_type, auto_approve, project_id)
|
|
47
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
50
48
|
[
|
|
51
49
|
id,
|
|
52
50
|
name,
|
|
@@ -55,7 +53,6 @@ export const queries = {
|
|
|
55
53
|
parentSessionId,
|
|
56
54
|
model,
|
|
57
55
|
systemPrompt,
|
|
58
|
-
groupPath,
|
|
59
56
|
agentType,
|
|
60
57
|
autoApprove ? 1 : 0,
|
|
61
58
|
projectId,
|
|
@@ -68,12 +65,6 @@ export const queries = {
|
|
|
68
65
|
getAllSessions: () =>
|
|
69
66
|
query<Session>("SELECT * FROM sessions ORDER BY updated_at DESC"),
|
|
70
67
|
|
|
71
|
-
updateSessionStatus: (status: string, id: string) =>
|
|
72
|
-
execute(
|
|
73
|
-
"UPDATE sessions SET status = ?, updated_at = datetime('now') WHERE id = ?",
|
|
74
|
-
[status, id]
|
|
75
|
-
),
|
|
76
|
-
|
|
77
68
|
updateSessionClaudeId: (claudeSessionId: string, id: string) =>
|
|
78
69
|
execute(
|
|
79
70
|
"UPDATE sessions SET claude_session_id = ?, updated_at = datetime('now') WHERE id = ?",
|
|
@@ -112,24 +103,6 @@ export const queries = {
|
|
|
112
103
|
[prUrl, prNumber, prStatus, id]
|
|
113
104
|
),
|
|
114
105
|
|
|
115
|
-
updateSessionGroup: (groupPath: string, id: string) =>
|
|
116
|
-
execute(
|
|
117
|
-
"UPDATE sessions SET group_path = ?, updated_at = datetime('now') WHERE id = ?",
|
|
118
|
-
[groupPath, id]
|
|
119
|
-
),
|
|
120
|
-
|
|
121
|
-
getSessionsByGroup: (groupPath: string) =>
|
|
122
|
-
query<Session>(
|
|
123
|
-
"SELECT * FROM sessions WHERE group_path = ? ORDER BY updated_at DESC",
|
|
124
|
-
[groupPath]
|
|
125
|
-
),
|
|
126
|
-
|
|
127
|
-
moveSessionsToGroup: (newGroupPath: string, oldGroupPath: string) =>
|
|
128
|
-
execute(
|
|
129
|
-
"UPDATE sessions SET group_path = ?, updated_at = datetime('now') WHERE group_path = ?",
|
|
130
|
-
[newGroupPath, oldGroupPath]
|
|
131
|
-
),
|
|
132
|
-
|
|
133
106
|
updateSessionProject: (projectId: string, id: string) =>
|
|
134
107
|
execute(
|
|
135
108
|
"UPDATE sessions SET project_id = ?, updated_at = datetime('now') WHERE id = ?",
|
|
@@ -162,13 +135,12 @@ export const queries = {
|
|
|
162
135
|
conductorSessionId: string,
|
|
163
136
|
workerTask: string,
|
|
164
137
|
model: string | null,
|
|
165
|
-
groupPath: string,
|
|
166
138
|
agentType: string,
|
|
167
139
|
projectId: string | null
|
|
168
140
|
) =>
|
|
169
141
|
execute(
|
|
170
|
-
`INSERT INTO sessions (id, name, tmux_name, working_directory, conductor_session_id, worker_task, worker_status, model,
|
|
171
|
-
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?,
|
|
142
|
+
`INSERT INTO sessions (id, name, tmux_name, working_directory, conductor_session_id, worker_task, worker_status, model, agent_type, project_id)
|
|
143
|
+
VALUES (?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?)`,
|
|
172
144
|
[
|
|
173
145
|
id,
|
|
174
146
|
name,
|
|
@@ -177,43 +149,11 @@ export const queries = {
|
|
|
177
149
|
conductorSessionId,
|
|
178
150
|
workerTask,
|
|
179
151
|
model,
|
|
180
|
-
groupPath,
|
|
181
152
|
agentType,
|
|
182
153
|
projectId,
|
|
183
154
|
]
|
|
184
155
|
),
|
|
185
156
|
|
|
186
|
-
getAllGroups: () =>
|
|
187
|
-
query<Group>("SELECT * FROM groups ORDER BY sort_order ASC, name ASC"),
|
|
188
|
-
|
|
189
|
-
getGroup: (path: string) =>
|
|
190
|
-
queryOne<Group>("SELECT * FROM groups WHERE path = ?", [path]),
|
|
191
|
-
|
|
192
|
-
createGroup: (path: string, name: string, sortOrder: number) =>
|
|
193
|
-
execute("INSERT INTO groups (path, name, sort_order) VALUES (?, ?, ?)", [
|
|
194
|
-
path,
|
|
195
|
-
name,
|
|
196
|
-
sortOrder,
|
|
197
|
-
]),
|
|
198
|
-
|
|
199
|
-
updateGroupName: (name: string, path: string) =>
|
|
200
|
-
execute("UPDATE groups SET name = ? WHERE path = ?", [name, path]),
|
|
201
|
-
|
|
202
|
-
updateGroupExpanded: (expanded: boolean, path: string) =>
|
|
203
|
-
execute("UPDATE groups SET expanded = ? WHERE path = ?", [
|
|
204
|
-
expanded ? 1 : 0,
|
|
205
|
-
path,
|
|
206
|
-
]),
|
|
207
|
-
|
|
208
|
-
updateGroupOrder: (sortOrder: number, path: string) =>
|
|
209
|
-
execute("UPDATE groups SET sort_order = ? WHERE path = ?", [
|
|
210
|
-
sortOrder,
|
|
211
|
-
path,
|
|
212
|
-
]),
|
|
213
|
-
|
|
214
|
-
deleteGroup: (path: string) =>
|
|
215
|
-
execute("DELETE FROM groups WHERE path = ?", [path]),
|
|
216
|
-
|
|
217
157
|
createProject: (
|
|
218
158
|
id: string,
|
|
219
159
|
name: string,
|
package/lib/db/types.ts
CHANGED
|
@@ -31,14 +31,6 @@ export interface Session {
|
|
|
31
31
|
worker_status: "pending" | "running" | "completed" | "failed" | null;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export interface Group {
|
|
35
|
-
path: string;
|
|
36
|
-
name: string;
|
|
37
|
-
expanded: boolean;
|
|
38
|
-
sort_order: number;
|
|
39
|
-
created_at: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
34
|
export interface Project {
|
|
43
35
|
id: string;
|
|
44
36
|
name: string;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code hook reporter.
|
|
4
|
+
*
|
|
5
|
+
* Invoked by Claude Code hooks on state transitions. Reads JSON from stdin
|
|
6
|
+
* and writes a session state file to ~/.claude-deck/session-states/{session_id}.json.
|
|
7
|
+
*
|
|
8
|
+
* Installed to ~/.claude-deck/hooks/state-reporter by the setup module.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
import * as os from "os";
|
|
14
|
+
|
|
15
|
+
const STATES_DIR = path.join(os.homedir(), ".claude-deck", "session-states");
|
|
16
|
+
|
|
17
|
+
interface HookInput {
|
|
18
|
+
session_id: string;
|
|
19
|
+
hook_event_name: string;
|
|
20
|
+
tool_name?: string;
|
|
21
|
+
tool_input?: unknown;
|
|
22
|
+
last_assistant_message?: string;
|
|
23
|
+
stop_hook_active?: boolean;
|
|
24
|
+
reason?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface StateFile {
|
|
28
|
+
status: "running" | "waiting" | "idle";
|
|
29
|
+
lastLine: string;
|
|
30
|
+
waitingContext?: string;
|
|
31
|
+
ts: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readStdin(): Promise<string> {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
let data = "";
|
|
37
|
+
process.stdin.setEncoding("utf-8");
|
|
38
|
+
process.stdin.on("data", (chunk) => (data += chunk));
|
|
39
|
+
process.stdin.on("end", () => resolve(data));
|
|
40
|
+
// Safety timeout — don't hang if stdin never closes
|
|
41
|
+
setTimeout(() => resolve(data), 1000);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getStatePath(sessionId: string): string {
|
|
46
|
+
return path.join(STATES_DIR, `${sessionId}.json`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function writeState(sessionId: string, state: StateFile): void {
|
|
50
|
+
fs.mkdirSync(STATES_DIR, { recursive: true });
|
|
51
|
+
fs.writeFileSync(getStatePath(sessionId), JSON.stringify(state));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function deleteState(sessionId: string): void {
|
|
55
|
+
try {
|
|
56
|
+
fs.unlinkSync(getStatePath(sessionId));
|
|
57
|
+
} catch {
|
|
58
|
+
// file may not exist
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function main(): Promise<void> {
|
|
63
|
+
const raw = await readStdin();
|
|
64
|
+
if (!raw.trim()) return;
|
|
65
|
+
|
|
66
|
+
let input: HookInput;
|
|
67
|
+
try {
|
|
68
|
+
input = JSON.parse(raw);
|
|
69
|
+
} catch {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { session_id, hook_event_name } = input;
|
|
74
|
+
if (!session_id) return;
|
|
75
|
+
|
|
76
|
+
switch (hook_event_name) {
|
|
77
|
+
case "SessionEnd":
|
|
78
|
+
deleteState(session_id);
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case "PermissionRequest":
|
|
82
|
+
writeState(session_id, {
|
|
83
|
+
status: "waiting",
|
|
84
|
+
lastLine: `Waiting: ${input.tool_name || "permission"}`,
|
|
85
|
+
waitingContext: input.tool_name
|
|
86
|
+
? `Permission requested for ${input.tool_name}`
|
|
87
|
+
: "Permission requested",
|
|
88
|
+
ts: Date.now(),
|
|
89
|
+
});
|
|
90
|
+
break;
|
|
91
|
+
|
|
92
|
+
case "Stop":
|
|
93
|
+
// Only transition to idle if not in a stop-hook re-run loop
|
|
94
|
+
if (!input.stop_hook_active) {
|
|
95
|
+
writeState(session_id, {
|
|
96
|
+
status: "idle",
|
|
97
|
+
lastLine: input.last_assistant_message?.slice(0, 200) || "",
|
|
98
|
+
ts: Date.now(),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
default:
|
|
104
|
+
// UserPromptSubmit, SessionStart, PreToolUse, PostToolUse, PermissionDenied
|
|
105
|
+
writeState(session_id, {
|
|
106
|
+
status: "running",
|
|
107
|
+
lastLine: input.tool_name
|
|
108
|
+
? `Running: ${input.tool_name}`
|
|
109
|
+
: "Running...",
|
|
110
|
+
ts: Date.now(),
|
|
111
|
+
});
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
main().catch(() => process.exit(0));
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hooks setup module.
|
|
3
|
+
*
|
|
4
|
+
* Installs the state-reporter script to ~/.claude-deck/hooks/ and
|
|
5
|
+
* merges hook configuration into ~/.claude/settings.json idempotently.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
import * as os from "os";
|
|
11
|
+
|
|
12
|
+
const CLAUDE_DECK_DIR = path.join(os.homedir(), ".claude-deck");
|
|
13
|
+
const HOOKS_DIR = path.join(CLAUDE_DECK_DIR, "hooks");
|
|
14
|
+
const STATES_DIR = path.join(CLAUDE_DECK_DIR, "session-states");
|
|
15
|
+
const REPORTER_PATH = path.join(HOOKS_DIR, "state-reporter");
|
|
16
|
+
const SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
17
|
+
|
|
18
|
+
// Events we hook into and whether they run async
|
|
19
|
+
const HOOK_EVENTS: Array<{ event: string; async: boolean }> = [
|
|
20
|
+
{ event: "UserPromptSubmit", async: true },
|
|
21
|
+
{ event: "PreToolUse", async: true },
|
|
22
|
+
{ event: "PermissionRequest", async: false },
|
|
23
|
+
{ event: "Elicitation", async: false },
|
|
24
|
+
{ event: "Stop", async: true },
|
|
25
|
+
{ event: "SessionStart", async: true },
|
|
26
|
+
{ event: "SessionEnd", async: true },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// The reporter is a self-contained Node.js script (no tsx, no ESM, no deps)
|
|
30
|
+
const REPORTER_SCRIPT = `#!/usr/bin/env node
|
|
31
|
+
"use strict";
|
|
32
|
+
var fs = require("fs");
|
|
33
|
+
var path = require("path");
|
|
34
|
+
var os = require("os");
|
|
35
|
+
var STATES_DIR = path.join(os.homedir(), ".claude-deck", "session-states");
|
|
36
|
+
|
|
37
|
+
var data = "";
|
|
38
|
+
process.stdin.setEncoding("utf-8");
|
|
39
|
+
process.stdin.on("data", function(c) { data += c; });
|
|
40
|
+
process.stdin.on("end", function() {
|
|
41
|
+
try {
|
|
42
|
+
var input = JSON.parse(data);
|
|
43
|
+
var id = input.session_id;
|
|
44
|
+
if (!id) return;
|
|
45
|
+
fs.mkdirSync(STATES_DIR, { recursive: true });
|
|
46
|
+
var fp = path.join(STATES_DIR, id + ".json");
|
|
47
|
+
var evt = input.hook_event_name;
|
|
48
|
+
|
|
49
|
+
if (evt === "SessionEnd") {
|
|
50
|
+
try { fs.unlinkSync(fp); } catch(e) {}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
var status = "running";
|
|
55
|
+
var lastLine = input.tool_name ? "Running: " + input.tool_name : "Running...";
|
|
56
|
+
var state = { status: status, lastLine: lastLine, ts: Date.now() };
|
|
57
|
+
|
|
58
|
+
if (evt === "SessionStart") {
|
|
59
|
+
state.status = "idle";
|
|
60
|
+
state.lastLine = "";
|
|
61
|
+
} else if (evt === "PermissionRequest" || evt === "Elicitation") {
|
|
62
|
+
state.status = "waiting";
|
|
63
|
+
state.lastLine = "Waiting: " + (input.tool_name || "input required");
|
|
64
|
+
state.waitingContext = input.tool_name
|
|
65
|
+
? "Permission requested for " + input.tool_name
|
|
66
|
+
: "Input required";
|
|
67
|
+
} else if (evt === "Stop" && !input.stop_hook_active) {
|
|
68
|
+
state.status = "idle";
|
|
69
|
+
state.lastLine = (input.last_assistant_message || "").slice(0, 200);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fs.writeFileSync(fp, JSON.stringify(state));
|
|
73
|
+
} catch(e) {}
|
|
74
|
+
});
|
|
75
|
+
setTimeout(function() { process.exit(0); }, 2000);
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
function installReporterScript(): void {
|
|
79
|
+
fs.mkdirSync(HOOKS_DIR, { recursive: true });
|
|
80
|
+
fs.mkdirSync(STATES_DIR, { recursive: true });
|
|
81
|
+
fs.writeFileSync(REPORTER_PATH, REPORTER_SCRIPT, { mode: 0o755 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface HookCommand {
|
|
85
|
+
type: "command";
|
|
86
|
+
command: string;
|
|
87
|
+
async?: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface HookMatcher {
|
|
91
|
+
matcher?: string;
|
|
92
|
+
hooks: HookCommand[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type SettingsHooks = Record<string, HookMatcher[]>;
|
|
96
|
+
|
|
97
|
+
interface Settings {
|
|
98
|
+
hooks?: SettingsHooks;
|
|
99
|
+
[key: string]: unknown;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isOurHook(hook: HookCommand): boolean {
|
|
103
|
+
return hook.command === REPORTER_PATH;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function mergeHooksIntoSettings(): void {
|
|
107
|
+
let settings: Settings = {};
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const raw = fs.readFileSync(SETTINGS_PATH, "utf-8");
|
|
111
|
+
settings = JSON.parse(raw);
|
|
112
|
+
} catch {
|
|
113
|
+
// File doesn't exist or is invalid
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!settings.hooks) {
|
|
117
|
+
settings.hooks = {};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let changed = false;
|
|
121
|
+
|
|
122
|
+
for (const { event, async: isAsync } of HOOK_EVENTS) {
|
|
123
|
+
if (!settings.hooks[event]) {
|
|
124
|
+
settings.hooks[event] = [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const eventHooks = settings.hooks[event];
|
|
128
|
+
const alreadyInstalled = eventHooks.some((matcher) =>
|
|
129
|
+
matcher.hooks?.some((h) => isOurHook(h))
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (!alreadyInstalled) {
|
|
133
|
+
const hookDef: HookCommand = {
|
|
134
|
+
type: "command",
|
|
135
|
+
command: REPORTER_PATH,
|
|
136
|
+
};
|
|
137
|
+
if (isAsync) {
|
|
138
|
+
hookDef.async = true;
|
|
139
|
+
}
|
|
140
|
+
eventHooks.push({ hooks: [hookDef] });
|
|
141
|
+
changed = true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (changed) {
|
|
146
|
+
fs.mkdirSync(path.dirname(SETTINGS_PATH), { recursive: true });
|
|
147
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n");
|
|
148
|
+
console.log("> Hooks configured in ~/.claude/settings.json");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function setupHooks(): void {
|
|
153
|
+
try {
|
|
154
|
+
installReporterScript();
|
|
155
|
+
mergeHooksIntoSettings();
|
|
156
|
+
console.log(
|
|
157
|
+
"> Hook reporter installed at ~/.claude-deck/hooks/state-reporter"
|
|
158
|
+
);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
console.error("Failed to setup hooks:", err);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export { STATES_DIR };
|
package/lib/orchestration.ts
CHANGED
|
@@ -11,8 +11,9 @@ import { promisify } from "util";
|
|
|
11
11
|
import { queries, type Session } from "./db";
|
|
12
12
|
import { createWorktree, deleteWorktree } from "./worktrees";
|
|
13
13
|
import { setupWorktree } from "./env-setup";
|
|
14
|
-
import { type AgentType,
|
|
15
|
-
import {
|
|
14
|
+
import { type AgentType, CLAUDE_COMMAND, buildClaudeFlags } from "./providers";
|
|
15
|
+
import { getStatusSnapshot } from "./status-monitor";
|
|
16
|
+
import { getSessionIdFromName } from "./providers/registry";
|
|
16
17
|
import { wrapWithBanner } from "./banner";
|
|
17
18
|
import { runInBackground } from "./async-operations";
|
|
18
19
|
|
|
@@ -94,7 +95,7 @@ export async function spawnWorker(
|
|
|
94
95
|
|
|
95
96
|
const sessionId = randomUUID();
|
|
96
97
|
const sessionName = taskToSessionName(task);
|
|
97
|
-
|
|
98
|
+
// Provider is always claude
|
|
98
99
|
|
|
99
100
|
let worktreePath: string | null = null;
|
|
100
101
|
let actualWorkingDir = workingDirectory;
|
|
@@ -131,7 +132,7 @@ export async function spawnWorker(
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
// Create session in database
|
|
134
|
-
const tmuxName =
|
|
135
|
+
const tmuxName = `claude-${sessionId}`;
|
|
135
136
|
await queries.createWorkerSession(
|
|
136
137
|
sessionId,
|
|
137
138
|
sessionName,
|
|
@@ -140,7 +141,6 @@ export async function spawnWorker(
|
|
|
140
141
|
conductorSessionId,
|
|
141
142
|
task,
|
|
142
143
|
model,
|
|
143
|
-
"sessions", // group_path
|
|
144
144
|
agentType,
|
|
145
145
|
"uncategorized" // project_id
|
|
146
146
|
);
|
|
@@ -157,15 +157,15 @@ export async function spawnWorker(
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
// Create tmux session and start the agent
|
|
160
|
-
const tmuxSessionName =
|
|
160
|
+
const tmuxSessionName = `claude-${sessionId}`;
|
|
161
161
|
const cwd = actualWorkingDir.replace("~", "$HOME");
|
|
162
162
|
|
|
163
163
|
// Build the initial prompt command (workers use auto-approve by default for automation)
|
|
164
|
-
const flags =
|
|
164
|
+
const flags = buildClaudeFlags({ model, autoApprove: true });
|
|
165
165
|
const flagsStr = flags.join(" ");
|
|
166
166
|
|
|
167
167
|
// Create tmux session with the agent and banner
|
|
168
|
-
const agentCmd = `${
|
|
168
|
+
const agentCmd = `${CLAUDE_COMMAND} ${flagsStr}`;
|
|
169
169
|
const newSessionCmd = wrapWithBanner(agentCmd);
|
|
170
170
|
const createCmd = `tmux set -g mouse on 2>/dev/null; tmux new-session -d -s "${tmuxSessionName}" -c "${cwd}" "${newSessionCmd}"`;
|
|
171
171
|
|
|
@@ -270,16 +270,12 @@ export async function getWorkers(
|
|
|
270
270
|
const workerInfos: WorkerInfo[] = [];
|
|
271
271
|
|
|
272
272
|
for (const worker of workers) {
|
|
273
|
-
const
|
|
274
|
-
const tmuxSessionName = worker.tmux_name || `${provider.id}-${worker.id}`;
|
|
273
|
+
const tmuxSessionName = worker.tmux_name || `claude-${worker.id}`;
|
|
275
274
|
|
|
276
|
-
// Get live status from
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
} catch {
|
|
281
|
-
liveStatus = "dead";
|
|
282
|
-
}
|
|
275
|
+
// Get live status from cached monitor snapshot
|
|
276
|
+
const sessionId = getSessionIdFromName(tmuxSessionName);
|
|
277
|
+
const snapshot = getStatusSnapshot();
|
|
278
|
+
const liveStatus = snapshot[sessionId]?.status || "dead";
|
|
283
279
|
|
|
284
280
|
// Combine DB status with live status
|
|
285
281
|
let status: WorkerInfo["status"];
|
|
@@ -320,8 +316,7 @@ export async function getWorkerOutput(
|
|
|
320
316
|
throw new Error(`Worker ${workerId} not found`);
|
|
321
317
|
}
|
|
322
318
|
|
|
323
|
-
const
|
|
324
|
-
const tmuxSessionName = session.tmux_name || `${provider.id}-${workerId}`;
|
|
319
|
+
const tmuxSessionName = session.tmux_name || `claude-${workerId}`;
|
|
325
320
|
|
|
326
321
|
try {
|
|
327
322
|
const { stdout } = await execAsync(
|
|
@@ -345,8 +340,7 @@ export async function sendToWorker(
|
|
|
345
340
|
throw new Error(`Worker ${workerId} not found`);
|
|
346
341
|
}
|
|
347
342
|
|
|
348
|
-
const
|
|
349
|
-
const tmuxSessionName = session.tmux_name || `${provider.id}-${workerId}`;
|
|
343
|
+
const tmuxSessionName = session.tmux_name || `claude-${workerId}`;
|
|
350
344
|
|
|
351
345
|
try {
|
|
352
346
|
const escapedMessage = message.replace(/"/g, '\\"').replace(/\$/g, "\\$");
|
|
@@ -385,8 +379,7 @@ export async function killWorker(
|
|
|
385
379
|
return;
|
|
386
380
|
}
|
|
387
381
|
|
|
388
|
-
const
|
|
389
|
-
const tmuxSessionName = session.tmux_name || `${provider.id}-${workerId}`;
|
|
382
|
+
const tmuxSessionName = session.tmux_name || `claude-${workerId}`;
|
|
390
383
|
|
|
391
384
|
// Kill tmux session
|
|
392
385
|
try {
|
|
@@ -1,67 +1,13 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
export type ProviderId = (typeof PROVIDER_IDS)[number];
|
|
4
|
-
|
|
5
|
-
export interface ProviderDefinition {
|
|
6
|
-
id: ProviderId;
|
|
7
|
-
name: string;
|
|
8
|
-
description: string;
|
|
9
|
-
cli: string;
|
|
10
|
-
configDir: string;
|
|
11
|
-
autoApproveFlag?: string;
|
|
12
|
-
supportsResume: boolean;
|
|
13
|
-
supportsFork: boolean;
|
|
14
|
-
resumeFlag?: string;
|
|
15
|
-
modelFlag?: string;
|
|
16
|
-
initialPromptFlag?: string;
|
|
17
|
-
defaultArgs?: string[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export const PROVIDERS: ProviderDefinition[] = [
|
|
21
|
-
{
|
|
22
|
-
id: "claude",
|
|
23
|
-
name: "Claude Code",
|
|
24
|
-
description: "Anthropic's official CLI",
|
|
25
|
-
cli: "claude",
|
|
26
|
-
configDir: "~/.claude",
|
|
27
|
-
autoApproveFlag: "--dangerously-skip-permissions",
|
|
28
|
-
supportsResume: true,
|
|
29
|
-
supportsFork: true,
|
|
30
|
-
resumeFlag: "--resume",
|
|
31
|
-
initialPromptFlag: "",
|
|
32
|
-
},
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
export const PROVIDER_MAP = new Map<ProviderId, ProviderDefinition>(
|
|
36
|
-
PROVIDERS.map((provider) => [provider.id, provider])
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
export function getProviderDefinition(id: ProviderId): ProviderDefinition {
|
|
40
|
-
const provider = PROVIDER_MAP.get(id);
|
|
41
|
-
if (!provider) {
|
|
42
|
-
throw new Error(`Unknown provider: ${id}`);
|
|
43
|
-
}
|
|
44
|
-
return provider;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function getAllProviderDefinitions(): ProviderDefinition[] {
|
|
48
|
-
return PROVIDERS;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function isValidProviderId(value: string): value is ProviderId {
|
|
52
|
-
return PROVIDER_MAP.has(value as ProviderId);
|
|
53
|
-
}
|
|
1
|
+
export type ProviderId = "claude";
|
|
54
2
|
|
|
55
3
|
export function getManagedSessionPattern(): RegExp {
|
|
56
|
-
return /^claude-
|
|
4
|
+
return /^claude-(new-)?[0-9a-z]{4,}/i;
|
|
57
5
|
}
|
|
58
6
|
|
|
59
7
|
export function getProviderIdFromSessionName(
|
|
60
8
|
sessionName: string
|
|
61
9
|
): ProviderId | null {
|
|
62
|
-
if (sessionName.startsWith("claude-"))
|
|
63
|
-
return "claude";
|
|
64
|
-
}
|
|
10
|
+
if (sessionName.startsWith("claude-")) return "claude";
|
|
65
11
|
return null;
|
|
66
12
|
}
|
|
67
13
|
|