claudeck 1.3.0 → 1.4.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/README.md +7 -3
- package/db/sqlite.js +1697 -0
- package/db.js +3 -1645
- package/package.json +2 -1
- package/plugins/tasks/server.js +21 -21
- package/public/css/ui/messages.css +25 -0
- package/public/js/core/api.js +23 -6
- package/public/js/core/ws.js +12 -0
- package/public/js/features/chat.js +4 -0
- package/public/js/features/sessions.js +102 -10
- package/public/js/ui/messages.js +42 -0
- package/public/js/ui/parallel.js +2 -4
- package/server/agent-loop.js +27 -26
- package/server/memory-extractor.js +4 -4
- package/server/memory-injector.js +11 -11
- package/server/memory-optimizer.js +2 -2
- package/server/notification-logger.js +5 -5
- package/server/orchestrator.js +16 -15
- package/server/push-sender.js +2 -2
- package/server/routes/agents.js +2 -2
- package/server/routes/memory.js +20 -20
- package/server/routes/messages.js +41 -10
- package/server/routes/notifications.js +20 -20
- package/server/routes/sessions.js +17 -17
- package/server/routes/stats.js +37 -37
- package/server/routes/worktrees.js +9 -9
- package/server/summarizer.js +3 -3
- package/server/ws-handler.js +153 -53
- package/server.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudeck",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A browser-based UI for Claude Code — chat, run workflows, manage MCP servers, track costs, and orchestrate autonomous agents from a local web interface. Installable as a PWA.",
|
|
6
6
|
"main": "server.js",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"cli.js",
|
|
12
12
|
"server.js",
|
|
13
13
|
"db.js",
|
|
14
|
+
"db/",
|
|
14
15
|
"server/",
|
|
15
16
|
"public/",
|
|
16
17
|
"config/",
|
package/plugins/tasks/server.js
CHANGED
|
@@ -3,63 +3,63 @@ import { listTodos, createTodo, updateTodo, archiveTodo, deleteTodo, createBrag,
|
|
|
3
3
|
|
|
4
4
|
const router = Router();
|
|
5
5
|
|
|
6
|
-
router.get("/", (req, res) => {
|
|
6
|
+
router.get("/", async (req, res) => {
|
|
7
7
|
try {
|
|
8
8
|
const archived = req.query.archived === "1";
|
|
9
|
-
const todos = listTodos(archived);
|
|
9
|
+
const todos = await listTodos(archived);
|
|
10
10
|
res.json(todos);
|
|
11
11
|
} catch (err) {
|
|
12
12
|
res.status(500).json({ error: err.message });
|
|
13
13
|
}
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
router.get("/counts", (req, res) => {
|
|
16
|
+
router.get("/counts", async (req, res) => {
|
|
17
17
|
try {
|
|
18
|
-
res.json(getTodoCounts());
|
|
18
|
+
res.json(await getTodoCounts());
|
|
19
19
|
} catch (err) {
|
|
20
20
|
res.status(500).json({ error: err.message });
|
|
21
21
|
}
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
router.post("/", (req, res) => {
|
|
24
|
+
router.post("/", async (req, res) => {
|
|
25
25
|
try {
|
|
26
26
|
const { text } = req.body;
|
|
27
27
|
if (!text || typeof text !== "string") {
|
|
28
28
|
return res.status(400).json({ error: "text is required" });
|
|
29
29
|
}
|
|
30
|
-
const info = createTodo(text.trim());
|
|
30
|
+
const info = await createTodo(text.trim());
|
|
31
31
|
res.json({ id: info.lastInsertRowid });
|
|
32
32
|
} catch (err) {
|
|
33
33
|
res.status(500).json({ error: err.message });
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
router.put("/:id", (req, res) => {
|
|
37
|
+
router.put("/:id", async (req, res) => {
|
|
38
38
|
try {
|
|
39
39
|
const id = Number(req.params.id);
|
|
40
40
|
const { text, done, priority } = req.body;
|
|
41
|
-
updateTodo(id, text ?? null, done ?? null, priority ?? null);
|
|
41
|
+
await updateTodo(id, text ?? null, done ?? null, priority ?? null);
|
|
42
42
|
res.json({ ok: true });
|
|
43
43
|
} catch (err) {
|
|
44
44
|
res.status(500).json({ error: err.message });
|
|
45
45
|
}
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
router.put("/:id/archive", (req, res) => {
|
|
48
|
+
router.put("/:id/archive", async (req, res) => {
|
|
49
49
|
try {
|
|
50
50
|
const id = Number(req.params.id);
|
|
51
51
|
const { archived } = req.body;
|
|
52
|
-
archiveTodo(id, archived ?? true);
|
|
52
|
+
await archiveTodo(id, archived ?? true);
|
|
53
53
|
res.json({ ok: true });
|
|
54
54
|
} catch (err) {
|
|
55
55
|
res.status(500).json({ error: err.message });
|
|
56
56
|
}
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
router.delete("/:id", (req, res) => {
|
|
59
|
+
router.delete("/:id", async (req, res) => {
|
|
60
60
|
try {
|
|
61
61
|
const id = Number(req.params.id);
|
|
62
|
-
deleteTodo(id);
|
|
62
|
+
await deleteTodo(id);
|
|
63
63
|
res.json({ ok: true });
|
|
64
64
|
} catch (err) {
|
|
65
65
|
res.status(500).json({ error: err.message });
|
|
@@ -67,15 +67,15 @@ router.delete("/:id", (req, res) => {
|
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
// ── Brags ──────────────────────────────────────────────
|
|
70
|
-
router.get("/brags", (req, res) => {
|
|
70
|
+
router.get("/brags", async (req, res) => {
|
|
71
71
|
try {
|
|
72
|
-
res.json(listBrags());
|
|
72
|
+
res.json(await listBrags());
|
|
73
73
|
} catch (err) {
|
|
74
74
|
res.status(500).json({ error: err.message });
|
|
75
75
|
}
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
router.post("/:id/brag", (req, res) => {
|
|
78
|
+
router.post("/:id/brag", async (req, res) => {
|
|
79
79
|
try {
|
|
80
80
|
const todoId = Number(req.params.id);
|
|
81
81
|
const { summary } = req.body;
|
|
@@ -86,27 +86,27 @@ router.post("/:id/brag", (req, res) => {
|
|
|
86
86
|
return res.status(400).json({ error: "summary must be 500 chars or less" });
|
|
87
87
|
}
|
|
88
88
|
// Get the todo text before archiving
|
|
89
|
-
const todos = listTodos(false);
|
|
89
|
+
const todos = await listTodos(false);
|
|
90
90
|
const todo = todos.find(t => t.id === todoId);
|
|
91
|
-
const archivedTodos = listTodos(true);
|
|
91
|
+
const archivedTodos = await listTodos(true);
|
|
92
92
|
const archivedTodo = archivedTodos.find(t => t.id === todoId);
|
|
93
93
|
const foundTodo = todo || archivedTodo;
|
|
94
94
|
if (!foundTodo) {
|
|
95
95
|
return res.status(404).json({ error: "Todo not found" });
|
|
96
96
|
}
|
|
97
|
-
const info = createBrag(todoId, foundTodo.text, summary.trim());
|
|
97
|
+
const info = await createBrag(todoId, foundTodo.text, summary.trim());
|
|
98
98
|
// Archive the todo
|
|
99
|
-
archiveTodo(todoId, true);
|
|
99
|
+
await archiveTodo(todoId, true);
|
|
100
100
|
res.json({ id: info.lastInsertRowid });
|
|
101
101
|
} catch (err) {
|
|
102
102
|
res.status(500).json({ error: err.message });
|
|
103
103
|
}
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
-
router.delete("/brags/:id", (req, res) => {
|
|
106
|
+
router.delete("/brags/:id", async (req, res) => {
|
|
107
107
|
try {
|
|
108
108
|
const id = Number(req.params.id);
|
|
109
|
-
deleteBrag(id);
|
|
109
|
+
await deleteBrag(id);
|
|
110
110
|
res.json({ ok: true });
|
|
111
111
|
} catch (err) {
|
|
112
112
|
res.status(500).json({ error: err.message });
|
|
@@ -25,6 +25,31 @@
|
|
|
25
25
|
gap: 18px;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/* ── Load-more indicator (lazy loading) ─────────────── */
|
|
29
|
+
.load-more-indicator {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
gap: 8px;
|
|
34
|
+
padding: 10px 0;
|
|
35
|
+
color: var(--text-muted);
|
|
36
|
+
font-size: 0.8rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.load-more-spinner {
|
|
40
|
+
display: inline-block;
|
|
41
|
+
width: 14px;
|
|
42
|
+
height: 14px;
|
|
43
|
+
border: 2px solid var(--border);
|
|
44
|
+
border-top-color: var(--accent);
|
|
45
|
+
border-radius: 50%;
|
|
46
|
+
animation: load-more-spin 0.6s linear infinite;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@keyframes load-more-spin {
|
|
50
|
+
to { transform: rotate(360deg); }
|
|
51
|
+
}
|
|
52
|
+
|
|
28
53
|
.messages:empty::after {
|
|
29
54
|
content: "";
|
|
30
55
|
display: none;
|
package/public/js/core/api.js
CHANGED
|
@@ -36,18 +36,35 @@ export async function fetchActiveSessionIds() {
|
|
|
36
36
|
return data.activeSessionIds || [];
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
const
|
|
39
|
+
function _appendPaginationParams(url, { limit, before } = {}) {
|
|
40
|
+
const params = new URLSearchParams();
|
|
41
|
+
if (limit) params.set("limit", limit);
|
|
42
|
+
if (before) params.set("before", before);
|
|
43
|
+
const qs = params.toString();
|
|
44
|
+
return qs ? `${url}?${qs}` : url;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function fetchMessages(sessionId, opts) {
|
|
48
|
+
const url = _appendPaginationParams(
|
|
49
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/messages`, opts
|
|
50
|
+
);
|
|
51
|
+
const res = await fetch(url);
|
|
41
52
|
return res.json();
|
|
42
53
|
}
|
|
43
54
|
|
|
44
|
-
export async function fetchMessagesByChatId(sessionId, chatId) {
|
|
45
|
-
const
|
|
55
|
+
export async function fetchMessagesByChatId(sessionId, chatId, opts) {
|
|
56
|
+
const url = _appendPaginationParams(
|
|
57
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/messages/${encodeURIComponent(chatId)}`, opts
|
|
58
|
+
);
|
|
59
|
+
const res = await fetch(url);
|
|
46
60
|
return res.json();
|
|
47
61
|
}
|
|
48
62
|
|
|
49
|
-
export async function fetchSingleMessages(sessionId) {
|
|
50
|
-
const
|
|
63
|
+
export async function fetchSingleMessages(sessionId, opts) {
|
|
64
|
+
const url = _appendPaginationParams(
|
|
65
|
+
`/api/sessions/${encodeURIComponent(sessionId)}/messages-single`, opts
|
|
66
|
+
);
|
|
67
|
+
const res = await fetch(url);
|
|
51
68
|
return res.json();
|
|
52
69
|
}
|
|
53
70
|
|
package/public/js/core/ws.js
CHANGED
|
@@ -3,6 +3,12 @@ import { $ } from './dom.js';
|
|
|
3
3
|
import { getState, setState } from './store.js';
|
|
4
4
|
import { emit } from './events.js';
|
|
5
5
|
|
|
6
|
+
export function subscribeToSession(sessionId) {
|
|
7
|
+
const ws = getState("ws");
|
|
8
|
+
if (!ws || ws.readyState !== 1 || !sessionId) return;
|
|
9
|
+
ws.send(JSON.stringify({ type: "subscribe", sessionId }));
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
let backoffAttempt = 0;
|
|
7
13
|
let hasConnectedBefore = false;
|
|
8
14
|
|
|
@@ -37,6 +43,12 @@ export function connectWebSocket() {
|
|
|
37
43
|
hasConnectedBefore = true;
|
|
38
44
|
emit("ws:connected");
|
|
39
45
|
}
|
|
46
|
+
|
|
47
|
+
// Subscribe to current session for multi-client broadcast
|
|
48
|
+
const currentSession = getState("sessionId");
|
|
49
|
+
if (currentSession) {
|
|
50
|
+
ws.send(JSON.stringify({ type: "subscribe", sessionId: currentSession }));
|
|
51
|
+
}
|
|
40
52
|
};
|
|
41
53
|
|
|
42
54
|
ws.onmessage = (event) => {
|
|
@@ -5,8 +5,12 @@ import { CHAT_IDS } from '../core/constants.js';
|
|
|
5
5
|
import { escapeHtml } from '../core/utils.js';
|
|
6
6
|
import * as api from '../core/api.js';
|
|
7
7
|
import { panes, enterParallelMode, exitParallelMode } from '../ui/parallel.js';
|
|
8
|
-
import { renderMessagesIntoPane, showWhalyPlaceholder } from '../ui/messages.js';
|
|
8
|
+
import { renderMessagesIntoPane, prependOlderMessages, showWhalyPlaceholder, showLoadingIndicator, hideLoadingIndicator } from '../ui/messages.js';
|
|
9
9
|
import { loadContextGauge } from '../ui/context-gauge.js';
|
|
10
|
+
import { subscribeToSession } from '../core/ws.js';
|
|
11
|
+
|
|
12
|
+
const MESSAGE_PAGE_SIZE = 30;
|
|
13
|
+
const SCROLL_LOAD_THRESHOLD = 150; // px from top to trigger load more
|
|
10
14
|
|
|
11
15
|
const SESSION_STORAGE_KEY = "claudeck-session-id";
|
|
12
16
|
|
|
@@ -14,6 +18,7 @@ const SESSION_STORAGE_KEY = "claudeck-session-id";
|
|
|
14
18
|
onState("sessionId", (val) => {
|
|
15
19
|
if (val) {
|
|
16
20
|
localStorage.setItem(SESSION_STORAGE_KEY, val);
|
|
21
|
+
subscribeToSession(val);
|
|
17
22
|
} else {
|
|
18
23
|
localStorage.removeItem(SESSION_STORAGE_KEY);
|
|
19
24
|
}
|
|
@@ -228,16 +233,16 @@ export async function deleteSession(id) {
|
|
|
228
233
|
|
|
229
234
|
export async function loadMessages(sid) {
|
|
230
235
|
if (getState("parallelMode")) {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
236
|
+
// Load all panes concurrently instead of sequentially
|
|
237
|
+
await Promise.all(CHAT_IDS.map(chatId => loadPaneMessages(sid, chatId)));
|
|
234
238
|
return;
|
|
235
239
|
}
|
|
236
240
|
|
|
237
241
|
const pane = panes.get(null);
|
|
238
242
|
try {
|
|
239
|
-
const messages = await api.fetchSingleMessages(sid);
|
|
243
|
+
const messages = await api.fetchSingleMessages(sid, { limit: MESSAGE_PAGE_SIZE });
|
|
240
244
|
renderMessagesIntoPane(messages, pane);
|
|
245
|
+
_initPanePagination(pane, messages, "single");
|
|
241
246
|
loadContextGauge(sid);
|
|
242
247
|
} catch (err) {
|
|
243
248
|
console.error("Failed to load messages:", err);
|
|
@@ -248,22 +253,109 @@ export async function loadPaneMessages(sid, chatId) {
|
|
|
248
253
|
const pane = panes.get(chatId);
|
|
249
254
|
if (!pane) return;
|
|
250
255
|
try {
|
|
251
|
-
let messages
|
|
252
|
-
|
|
256
|
+
let messages;
|
|
253
257
|
// For Chat 1 (chat-0): also load single-mode messages as fallback
|
|
254
258
|
if (chatId === CHAT_IDS[0]) {
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
259
|
+
const [chatMsgs, singleMsgs] = await Promise.all([
|
|
260
|
+
api.fetchMessagesByChatId(sid, chatId, { limit: MESSAGE_PAGE_SIZE }),
|
|
261
|
+
api.fetchSingleMessages(sid, { limit: MESSAGE_PAGE_SIZE }),
|
|
262
|
+
]);
|
|
263
|
+
if (singleMsgs.length > 0) {
|
|
264
|
+
messages = [...singleMsgs, ...chatMsgs].sort((a, b) => a.id - b.id);
|
|
265
|
+
} else {
|
|
266
|
+
messages = chatMsgs;
|
|
258
267
|
}
|
|
268
|
+
} else {
|
|
269
|
+
messages = await api.fetchMessagesByChatId(sid, chatId, { limit: MESSAGE_PAGE_SIZE });
|
|
259
270
|
}
|
|
260
271
|
|
|
261
272
|
renderMessagesIntoPane(messages, pane);
|
|
273
|
+
_initPanePagination(pane, messages, chatId === CHAT_IDS[0] ? "chat0" : "chat");
|
|
262
274
|
} catch (err) {
|
|
263
275
|
console.error(`Failed to load messages for ${chatId}:`, err);
|
|
264
276
|
}
|
|
265
277
|
}
|
|
266
278
|
|
|
279
|
+
// ── Lazy-load pagination ────────────────────────────────
|
|
280
|
+
|
|
281
|
+
function _initPanePagination(pane, messages, mode) {
|
|
282
|
+
pane._hasMore = messages.length >= MESSAGE_PAGE_SIZE;
|
|
283
|
+
pane._oldestMessageId = messages.length > 0 ? messages[0].id : null;
|
|
284
|
+
pane._loadingMore = false;
|
|
285
|
+
pane._paginationMode = mode; // "single" | "chat" | "chat0"
|
|
286
|
+
|
|
287
|
+
// Remove any existing scroll listener
|
|
288
|
+
if (pane._scrollHandler) {
|
|
289
|
+
pane.messagesDiv.removeEventListener("scroll", pane._scrollHandler);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (pane._hasMore) {
|
|
293
|
+
pane._scrollHandler = () => _onPaneScroll(pane);
|
|
294
|
+
pane.messagesDiv.addEventListener("scroll", pane._scrollHandler, { passive: true });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function _onPaneScroll(pane) {
|
|
299
|
+
if (
|
|
300
|
+
pane.messagesDiv.scrollTop < SCROLL_LOAD_THRESHOLD &&
|
|
301
|
+
pane._hasMore &&
|
|
302
|
+
!pane._loadingMore
|
|
303
|
+
) {
|
|
304
|
+
_loadMoreMessages(pane);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function _loadMoreMessages(pane) {
|
|
309
|
+
pane._loadingMore = true;
|
|
310
|
+
showLoadingIndicator(pane);
|
|
311
|
+
|
|
312
|
+
const sid = getState("sessionId");
|
|
313
|
+
const before = pane._oldestMessageId;
|
|
314
|
+
const opts = { limit: MESSAGE_PAGE_SIZE, before };
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
let olderMessages;
|
|
318
|
+
|
|
319
|
+
switch (pane._paginationMode) {
|
|
320
|
+
case "single":
|
|
321
|
+
olderMessages = await api.fetchSingleMessages(sid, opts);
|
|
322
|
+
break;
|
|
323
|
+
case "chat0": {
|
|
324
|
+
// Chat 1 merges chatId + single messages
|
|
325
|
+
const [chatMsgs, singleMsgs] = await Promise.all([
|
|
326
|
+
api.fetchMessagesByChatId(sid, pane.chatId, opts),
|
|
327
|
+
api.fetchSingleMessages(sid, opts),
|
|
328
|
+
]);
|
|
329
|
+
olderMessages = singleMsgs.length > 0
|
|
330
|
+
? [...singleMsgs, ...chatMsgs].sort((a, b) => a.id - b.id)
|
|
331
|
+
: chatMsgs;
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
default:
|
|
335
|
+
olderMessages = await api.fetchMessagesByChatId(sid, pane.chatId, opts);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (olderMessages.length === 0) {
|
|
339
|
+
pane._hasMore = false;
|
|
340
|
+
} else {
|
|
341
|
+
pane._oldestMessageId = olderMessages[0].id;
|
|
342
|
+
pane._hasMore = olderMessages.length >= MESSAGE_PAGE_SIZE;
|
|
343
|
+
prependOlderMessages(olderMessages, pane);
|
|
344
|
+
}
|
|
345
|
+
} catch (err) {
|
|
346
|
+
console.error("Failed to load more messages:", err);
|
|
347
|
+
} finally {
|
|
348
|
+
hideLoadingIndicator(pane);
|
|
349
|
+
pane._loadingMore = false;
|
|
350
|
+
|
|
351
|
+
// Detach scroll listener if no more messages
|
|
352
|
+
if (!pane._hasMore && pane._scrollHandler) {
|
|
353
|
+
pane.messagesDiv.removeEventListener("scroll", pane._scrollHandler);
|
|
354
|
+
pane._scrollHandler = null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
267
359
|
// ── Session Context Menu ────────────────────────────────
|
|
268
360
|
let sessionCtxMenu = null;
|
|
269
361
|
|
package/public/js/ui/messages.js
CHANGED
|
@@ -443,3 +443,45 @@ function addForkButton(msgEl, messageId) {
|
|
|
443
443
|
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>`;
|
|
444
444
|
msgEl.appendChild(btn);
|
|
445
445
|
}
|
|
446
|
+
|
|
447
|
+
// ── Lazy-loading helpers ────────────────────────────────
|
|
448
|
+
|
|
449
|
+
export function prependOlderMessages(messages, pane) {
|
|
450
|
+
if (!messages || messages.length === 0) return;
|
|
451
|
+
|
|
452
|
+
// Render older messages into a detached container using the same rendering logic
|
|
453
|
+
const tempContainer = document.createElement("div");
|
|
454
|
+
const tempPane = { messagesDiv: tempContainer, currentAssistantMsg: null };
|
|
455
|
+
renderMessagesIntoPane(messages, tempPane);
|
|
456
|
+
|
|
457
|
+
// Capture scroll position before DOM mutation
|
|
458
|
+
const scrollHeightBefore = pane.messagesDiv.scrollHeight;
|
|
459
|
+
|
|
460
|
+
// Move all rendered nodes into the real pane
|
|
461
|
+
const fragment = document.createDocumentFragment();
|
|
462
|
+
while (tempContainer.firstChild) {
|
|
463
|
+
fragment.appendChild(tempContainer.firstChild);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Insert after loading indicator (if present) or at the top
|
|
467
|
+
const indicator = pane.messagesDiv.querySelector(".load-more-indicator");
|
|
468
|
+
const insertRef = indicator ? indicator.nextSibling : pane.messagesDiv.firstChild;
|
|
469
|
+
pane.messagesDiv.insertBefore(fragment, insertRef);
|
|
470
|
+
|
|
471
|
+
// Restore scroll position so the user's view doesn't jump
|
|
472
|
+
const scrollHeightAfter = pane.messagesDiv.scrollHeight;
|
|
473
|
+
pane.messagesDiv.scrollTop += (scrollHeightAfter - scrollHeightBefore);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function showLoadingIndicator(pane) {
|
|
477
|
+
if (pane.messagesDiv.querySelector(".load-more-indicator")) return;
|
|
478
|
+
const el = document.createElement("div");
|
|
479
|
+
el.className = "load-more-indicator";
|
|
480
|
+
el.innerHTML = '<span class="load-more-spinner"></span> Loading older messages\u2026';
|
|
481
|
+
pane.messagesDiv.prepend(el);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function hideLoadingIndicator(pane) {
|
|
485
|
+
const el = pane.messagesDiv.querySelector(".load-more-indicator");
|
|
486
|
+
if (el) el.remove();
|
|
487
|
+
}
|
package/public/js/ui/parallel.js
CHANGED
|
@@ -138,11 +138,9 @@ export function enterParallelMode() {
|
|
|
138
138
|
|
|
139
139
|
const sessionId = getState("sessionId");
|
|
140
140
|
if (sessionId) {
|
|
141
|
-
// Lazy import to avoid circular dependency
|
|
141
|
+
// Lazy import to avoid circular dependency — load all panes concurrently
|
|
142
142
|
import('../features/sessions.js').then(({ loadPaneMessages }) => {
|
|
143
|
-
|
|
144
|
-
loadPaneMessages(sessionId, chatId);
|
|
145
|
-
}
|
|
143
|
+
Promise.all(CHAT_IDS.map(chatId => loadPaneMessages(sessionId, chatId)));
|
|
146
144
|
});
|
|
147
145
|
}
|
|
148
146
|
}
|
package/server/agent-loop.js
CHANGED
|
@@ -40,7 +40,7 @@ import { logNotification } from "./notification-logger.js";
|
|
|
40
40
|
* Build the agent system prompt that instructs Claude to work autonomously
|
|
41
41
|
* toward the given goal.
|
|
42
42
|
*/
|
|
43
|
-
function buildAgentPrompt(agentDef, userContext, sharedContext, cwd) {
|
|
43
|
+
async function buildAgentPrompt(agentDef, userContext, sharedContext, cwd) {
|
|
44
44
|
let prompt = `You are an autonomous AI agent. Work toward the following goal step by step, using any tools available to you.\n\n`;
|
|
45
45
|
prompt += `## Goal\n${agentDef.goal}\n\n`;
|
|
46
46
|
if (userContext) {
|
|
@@ -48,7 +48,7 @@ function buildAgentPrompt(agentDef, userContext, sharedContext, cwd) {
|
|
|
48
48
|
}
|
|
49
49
|
// Inject persistent memories from previous sessions
|
|
50
50
|
if (cwd) {
|
|
51
|
-
const memoryPrompt = buildAgentMemoryPrompt(cwd, 8);
|
|
51
|
+
const memoryPrompt = await buildAgentMemoryPrompt(cwd, 8);
|
|
52
52
|
if (memoryPrompt) {
|
|
53
53
|
prompt += memoryPrompt + '\n\n';
|
|
54
54
|
console.log(`\n══════ AGENT MEMORY INJECTION ══════`);
|
|
@@ -105,7 +105,7 @@ export async function runAgent({
|
|
|
105
105
|
const monitorRunId = runId || `single-${Date.now()}`;
|
|
106
106
|
const effectiveRunType = runType || 'single';
|
|
107
107
|
try {
|
|
108
|
-
recordAgentRunStart(monitorRunId, agentId, agentDef.title, effectiveRunType, parentRunId);
|
|
108
|
+
await recordAgentRunStart(monitorRunId, agentId, agentDef.title, effectiveRunType, parentRunId);
|
|
109
109
|
} catch (e) { /* ignore duplicates */ }
|
|
110
110
|
|
|
111
111
|
function agentSend(payload) {
|
|
@@ -145,6 +145,7 @@ export async function runAgent({
|
|
|
145
145
|
abortController,
|
|
146
146
|
maxTurns,
|
|
147
147
|
executable: execPath,
|
|
148
|
+
settingSources: ["user", "project", "local"],
|
|
148
149
|
};
|
|
149
150
|
|
|
150
151
|
if (!useBypass && !usePlan) {
|
|
@@ -160,8 +161,8 @@ export async function runAgent({
|
|
|
160
161
|
if (resumeId) opts.resume = resumeId;
|
|
161
162
|
|
|
162
163
|
// Load shared context from previous agents in this run
|
|
163
|
-
const sharedContext = runId ? getAllAgentContext(runId) : [];
|
|
164
|
-
const prompt = buildAgentPrompt(agentDef, userContext, sharedContext, cwd);
|
|
164
|
+
const sharedContext = runId ? await getAllAgentContext(runId) : [];
|
|
165
|
+
const prompt = await buildAgentPrompt(agentDef, userContext, sharedContext, cwd);
|
|
165
166
|
let resolvedSid = clientSid;
|
|
166
167
|
let claudeSessionId = null;
|
|
167
168
|
let sessionModel = null;
|
|
@@ -184,15 +185,15 @@ export async function runAgent({
|
|
|
184
185
|
|
|
185
186
|
sessionIds.set(ourSid, claudeSessionId);
|
|
186
187
|
|
|
187
|
-
if (!getSession(ourSid)) {
|
|
188
|
-
createSession(ourSid, claudeSessionId, projectName || "Agent Session", cwd || "");
|
|
189
|
-
updateSessionTitle(ourSid, `Agent: ${agentDef.title}`);
|
|
188
|
+
if (!await getSession(ourSid)) {
|
|
189
|
+
await createSession(ourSid, claudeSessionId, projectName || "Agent Session", cwd || "");
|
|
190
|
+
await updateSessionTitle(ourSid, `Agent: ${agentDef.title}`);
|
|
190
191
|
} else {
|
|
191
|
-
updateClaudeSessionId(ourSid, claudeSessionId);
|
|
192
|
+
await updateClaudeSessionId(ourSid, claudeSessionId);
|
|
192
193
|
}
|
|
193
194
|
|
|
194
195
|
agentSend({ type: "session", sessionId: ourSid });
|
|
195
|
-
addMessage(resolvedSid, "user", JSON.stringify({ text: `[Agent: ${agentDef.title}] ${agentDef.goal}` }), null);
|
|
196
|
+
await addMessage(resolvedSid, "user", JSON.stringify({ text: `[Agent: ${agentDef.title}] ${agentDef.goal}` }), null);
|
|
196
197
|
continue;
|
|
197
198
|
}
|
|
198
199
|
|
|
@@ -203,7 +204,7 @@ export async function runAgent({
|
|
|
203
204
|
lastAssistantText += (lastAssistantText ? "\n\n" : "") + block.text;
|
|
204
205
|
agentSend({ type: "text", text: block.text });
|
|
205
206
|
if (resolvedSid) {
|
|
206
|
-
addMessage(resolvedSid, "assistant", JSON.stringify({ text: block.text }), null);
|
|
207
|
+
await addMessage(resolvedSid, "assistant", JSON.stringify({ text: block.text }), null);
|
|
207
208
|
}
|
|
208
209
|
} else if (block.type === "tool_use") {
|
|
209
210
|
turnCount++;
|
|
@@ -219,7 +220,7 @@ export async function runAgent({
|
|
|
219
220
|
: "",
|
|
220
221
|
});
|
|
221
222
|
if (resolvedSid) {
|
|
222
|
-
addMessage(resolvedSid, "tool", JSON.stringify({ id: block.id, name: block.name, input: block.input }), null);
|
|
223
|
+
await addMessage(resolvedSid, "tool", JSON.stringify({ id: block.id, name: block.name, input: block.input }), null);
|
|
223
224
|
}
|
|
224
225
|
}
|
|
225
226
|
}
|
|
@@ -241,7 +242,7 @@ export async function runAgent({
|
|
|
241
242
|
isError: block.is_error || false,
|
|
242
243
|
});
|
|
243
244
|
if (resolvedSid) {
|
|
244
|
-
addMessage(resolvedSid, "tool_result", JSON.stringify({
|
|
245
|
+
await addMessage(resolvedSid, "tool_result", JSON.stringify({
|
|
245
246
|
toolUseId: block.tool_use_id,
|
|
246
247
|
content: text.slice(0, 10000),
|
|
247
248
|
isError: block.is_error || false,
|
|
@@ -265,7 +266,7 @@ export async function runAgent({
|
|
|
265
266
|
const resultModel = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
|
|
266
267
|
|
|
267
268
|
if (resolvedSid) {
|
|
268
|
-
addCost(resolvedSid, costUsd, durationMs, numTurns, inputTokens, outputTokens, {
|
|
269
|
+
await addCost(resolvedSid, costUsd, durationMs, numTurns, inputTokens, outputTokens, {
|
|
269
270
|
model: resultModel,
|
|
270
271
|
stopReason: sdkMsg.subtype,
|
|
271
272
|
isError: 0,
|
|
@@ -279,7 +280,7 @@ export async function runAgent({
|
|
|
279
280
|
duration_ms: durationMs,
|
|
280
281
|
num_turns: numTurns,
|
|
281
282
|
cost_usd: costUsd,
|
|
282
|
-
totalCost: getTotalCost(),
|
|
283
|
+
totalCost: await getTotalCost(),
|
|
283
284
|
input_tokens: inputTokens,
|
|
284
285
|
output_tokens: outputTokens,
|
|
285
286
|
cache_read_tokens: cacheReadTokens,
|
|
@@ -300,11 +301,11 @@ export async function runAgent({
|
|
|
300
301
|
|
|
301
302
|
// Record completion for monitoring
|
|
302
303
|
try {
|
|
303
|
-
recordAgentRunComplete(monitorRunId, agentId, 'completed', numTurns, costUsd, durationMs, inputTokens, outputTokens);
|
|
304
|
+
await recordAgentRunComplete(monitorRunId, agentId, 'completed', numTurns, costUsd, durationMs, inputTokens, outputTokens);
|
|
304
305
|
} catch (e) { /* ignore */ }
|
|
305
306
|
|
|
306
307
|
// Log notification
|
|
307
|
-
logNotification('agent', `Agent "${agentDef.title}" completed`,
|
|
308
|
+
await logNotification('agent', `Agent "${agentDef.title}" completed`,
|
|
308
309
|
`${numTurns} turns · $${costUsd.toFixed(4)} · ${(durationMs / 1000).toFixed(1)}s`,
|
|
309
310
|
JSON.stringify({ costUsd, durationMs, inputTokens, outputTokens, turns: numTurns }),
|
|
310
311
|
resolvedSid, agentId);
|
|
@@ -314,7 +315,7 @@ export async function runAgent({
|
|
|
314
315
|
const summary = lastAssistantText.length > 4000
|
|
315
316
|
? lastAssistantText.slice(0, 4000) + "\n\n[truncated]"
|
|
316
317
|
: lastAssistantText;
|
|
317
|
-
setAgentContext(runId, agentId, "output", summary);
|
|
318
|
+
await setAgentContext(runId, agentId, "output", summary);
|
|
318
319
|
}
|
|
319
320
|
} else if (sdkMsg.subtype?.startsWith("error")) {
|
|
320
321
|
const errMsg = sdkMsg.errors?.join(", ") || "Unknown error";
|
|
@@ -328,14 +329,14 @@ export async function runAgent({
|
|
|
328
329
|
const resultModel = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
|
|
329
330
|
|
|
330
331
|
if (resolvedSid) {
|
|
331
|
-
addCost(resolvedSid, costUsd, durationMs, numTurns, inputTokens, outputTokens, {
|
|
332
|
+
await addCost(resolvedSid, costUsd, durationMs, numTurns, inputTokens, outputTokens, {
|
|
332
333
|
model: resultModel,
|
|
333
334
|
stopReason: sdkMsg.subtype,
|
|
334
335
|
isError: 1,
|
|
335
336
|
cacheReadTokens,
|
|
336
337
|
cacheCreationTokens,
|
|
337
338
|
});
|
|
338
|
-
addMessage(resolvedSid, "error", JSON.stringify({ error: errMsg, subtype: sdkMsg.subtype }), null);
|
|
339
|
+
await addMessage(resolvedSid, "error", JSON.stringify({ error: errMsg, subtype: sdkMsg.subtype }), null);
|
|
339
340
|
}
|
|
340
341
|
|
|
341
342
|
lastAgentMetrics = { durationMs, costUsd, inputTokens, outputTokens, model: resultModel, turns: numTurns, isError: true, error: errMsg };
|
|
@@ -344,11 +345,11 @@ export async function runAgent({
|
|
|
344
345
|
|
|
345
346
|
// Record error for monitoring
|
|
346
347
|
try {
|
|
347
|
-
recordAgentRunComplete(monitorRunId, agentId, 'error', numTurns, costUsd, durationMs, inputTokens, outputTokens, errMsg);
|
|
348
|
+
await recordAgentRunComplete(monitorRunId, agentId, 'error', numTurns, costUsd, durationMs, inputTokens, outputTokens, errMsg);
|
|
348
349
|
} catch (e) { /* ignore */ }
|
|
349
350
|
|
|
350
351
|
// Log error notification
|
|
351
|
-
logNotification('error', `Agent "${agentDef.title}" failed`,
|
|
352
|
+
await logNotification('error', `Agent "${agentDef.title}" failed`,
|
|
352
353
|
errMsg.slice(0, 200),
|
|
353
354
|
JSON.stringify({ costUsd, durationMs, error: errMsg }),
|
|
354
355
|
resolvedSid, agentId);
|
|
@@ -360,11 +361,11 @@ export async function runAgent({
|
|
|
360
361
|
if (err.name === "AbortError") {
|
|
361
362
|
agentSend({ type: "agent_aborted", agentId, turn: turnCount });
|
|
362
363
|
agentSend({ type: "aborted" });
|
|
363
|
-
try { recordAgentRunComplete(monitorRunId, agentId, 'aborted', turnCount, 0, 0, 0, 0, 'Aborted'); } catch (e) { /* ignore */ }
|
|
364
|
+
try { await recordAgentRunComplete(monitorRunId, agentId, 'aborted', turnCount, 0, 0, 0, 0, 'Aborted'); } catch (e) { /* ignore */ }
|
|
364
365
|
} else {
|
|
365
366
|
agentSend({ type: "agent_error", agentId, error: err.message, turn: turnCount });
|
|
366
367
|
agentSend({ type: "error", error: err.message });
|
|
367
|
-
try { recordAgentRunComplete(monitorRunId, agentId, 'error', turnCount, 0, 0, 0, 0, err.message); } catch (e) { /* ignore */ }
|
|
368
|
+
try { await recordAgentRunComplete(monitorRunId, agentId, 'error', turnCount, 0, 0, 0, 0, err.message); } catch (e) { /* ignore */ }
|
|
368
369
|
}
|
|
369
370
|
throw err; // Re-throw so callers (chains, DAGs) know the agent failed
|
|
370
371
|
} finally {
|
|
@@ -412,8 +413,8 @@ export async function runAgent({
|
|
|
412
413
|
// Auto-capture memories from agent output
|
|
413
414
|
if (cwd && lastAssistantText) {
|
|
414
415
|
try {
|
|
415
|
-
const explicitCount = saveExplicitMemories(cwd, lastAssistantText, resolvedSid);
|
|
416
|
-
const autoCount = captureMemories(cwd, lastAssistantText, resolvedSid, agentId);
|
|
416
|
+
const explicitCount = await saveExplicitMemories(cwd, lastAssistantText, resolvedSid);
|
|
417
|
+
const autoCount = await captureMemories(cwd, lastAssistantText, resolvedSid, agentId);
|
|
417
418
|
const totalCaptured = explicitCount + autoCount;
|
|
418
419
|
if (totalCaptured > 0) {
|
|
419
420
|
console.log(`Captured ${totalCaptured} memories (${explicitCount} explicit, ${autoCount} auto) from agent ${agentId}`);
|