claudeck 1.3.1 → 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 +26 -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 +15 -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 +151 -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) {
|
|
@@ -161,8 +161,8 @@ export async function runAgent({
|
|
|
161
161
|
if (resumeId) opts.resume = resumeId;
|
|
162
162
|
|
|
163
163
|
// Load shared context from previous agents in this run
|
|
164
|
-
const sharedContext = runId ? getAllAgentContext(runId) : [];
|
|
165
|
-
const prompt = buildAgentPrompt(agentDef, userContext, sharedContext, cwd);
|
|
164
|
+
const sharedContext = runId ? await getAllAgentContext(runId) : [];
|
|
165
|
+
const prompt = await buildAgentPrompt(agentDef, userContext, sharedContext, cwd);
|
|
166
166
|
let resolvedSid = clientSid;
|
|
167
167
|
let claudeSessionId = null;
|
|
168
168
|
let sessionModel = null;
|
|
@@ -185,15 +185,15 @@ export async function runAgent({
|
|
|
185
185
|
|
|
186
186
|
sessionIds.set(ourSid, claudeSessionId);
|
|
187
187
|
|
|
188
|
-
if (!getSession(ourSid)) {
|
|
189
|
-
createSession(ourSid, claudeSessionId, projectName || "Agent Session", cwd || "");
|
|
190
|
-
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}`);
|
|
191
191
|
} else {
|
|
192
|
-
updateClaudeSessionId(ourSid, claudeSessionId);
|
|
192
|
+
await updateClaudeSessionId(ourSid, claudeSessionId);
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
agentSend({ type: "session", sessionId: ourSid });
|
|
196
|
-
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);
|
|
197
197
|
continue;
|
|
198
198
|
}
|
|
199
199
|
|
|
@@ -204,7 +204,7 @@ export async function runAgent({
|
|
|
204
204
|
lastAssistantText += (lastAssistantText ? "\n\n" : "") + block.text;
|
|
205
205
|
agentSend({ type: "text", text: block.text });
|
|
206
206
|
if (resolvedSid) {
|
|
207
|
-
addMessage(resolvedSid, "assistant", JSON.stringify({ text: block.text }), null);
|
|
207
|
+
await addMessage(resolvedSid, "assistant", JSON.stringify({ text: block.text }), null);
|
|
208
208
|
}
|
|
209
209
|
} else if (block.type === "tool_use") {
|
|
210
210
|
turnCount++;
|
|
@@ -220,7 +220,7 @@ export async function runAgent({
|
|
|
220
220
|
: "",
|
|
221
221
|
});
|
|
222
222
|
if (resolvedSid) {
|
|
223
|
-
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);
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
}
|
|
@@ -242,7 +242,7 @@ export async function runAgent({
|
|
|
242
242
|
isError: block.is_error || false,
|
|
243
243
|
});
|
|
244
244
|
if (resolvedSid) {
|
|
245
|
-
addMessage(resolvedSid, "tool_result", JSON.stringify({
|
|
245
|
+
await addMessage(resolvedSid, "tool_result", JSON.stringify({
|
|
246
246
|
toolUseId: block.tool_use_id,
|
|
247
247
|
content: text.slice(0, 10000),
|
|
248
248
|
isError: block.is_error || false,
|
|
@@ -266,7 +266,7 @@ export async function runAgent({
|
|
|
266
266
|
const resultModel = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
|
|
267
267
|
|
|
268
268
|
if (resolvedSid) {
|
|
269
|
-
addCost(resolvedSid, costUsd, durationMs, numTurns, inputTokens, outputTokens, {
|
|
269
|
+
await addCost(resolvedSid, costUsd, durationMs, numTurns, inputTokens, outputTokens, {
|
|
270
270
|
model: resultModel,
|
|
271
271
|
stopReason: sdkMsg.subtype,
|
|
272
272
|
isError: 0,
|
|
@@ -280,7 +280,7 @@ export async function runAgent({
|
|
|
280
280
|
duration_ms: durationMs,
|
|
281
281
|
num_turns: numTurns,
|
|
282
282
|
cost_usd: costUsd,
|
|
283
|
-
totalCost: getTotalCost(),
|
|
283
|
+
totalCost: await getTotalCost(),
|
|
284
284
|
input_tokens: inputTokens,
|
|
285
285
|
output_tokens: outputTokens,
|
|
286
286
|
cache_read_tokens: cacheReadTokens,
|
|
@@ -301,11 +301,11 @@ export async function runAgent({
|
|
|
301
301
|
|
|
302
302
|
// Record completion for monitoring
|
|
303
303
|
try {
|
|
304
|
-
recordAgentRunComplete(monitorRunId, agentId, 'completed', numTurns, costUsd, durationMs, inputTokens, outputTokens);
|
|
304
|
+
await recordAgentRunComplete(monitorRunId, agentId, 'completed', numTurns, costUsd, durationMs, inputTokens, outputTokens);
|
|
305
305
|
} catch (e) { /* ignore */ }
|
|
306
306
|
|
|
307
307
|
// Log notification
|
|
308
|
-
logNotification('agent', `Agent "${agentDef.title}" completed`,
|
|
308
|
+
await logNotification('agent', `Agent "${agentDef.title}" completed`,
|
|
309
309
|
`${numTurns} turns · $${costUsd.toFixed(4)} · ${(durationMs / 1000).toFixed(1)}s`,
|
|
310
310
|
JSON.stringify({ costUsd, durationMs, inputTokens, outputTokens, turns: numTurns }),
|
|
311
311
|
resolvedSid, agentId);
|
|
@@ -315,7 +315,7 @@ export async function runAgent({
|
|
|
315
315
|
const summary = lastAssistantText.length > 4000
|
|
316
316
|
? lastAssistantText.slice(0, 4000) + "\n\n[truncated]"
|
|
317
317
|
: lastAssistantText;
|
|
318
|
-
setAgentContext(runId, agentId, "output", summary);
|
|
318
|
+
await setAgentContext(runId, agentId, "output", summary);
|
|
319
319
|
}
|
|
320
320
|
} else if (sdkMsg.subtype?.startsWith("error")) {
|
|
321
321
|
const errMsg = sdkMsg.errors?.join(", ") || "Unknown error";
|
|
@@ -329,14 +329,14 @@ export async function runAgent({
|
|
|
329
329
|
const resultModel = Object.keys(sdkMsg.modelUsage || {})[0] || sessionModel;
|
|
330
330
|
|
|
331
331
|
if (resolvedSid) {
|
|
332
|
-
addCost(resolvedSid, costUsd, durationMs, numTurns, inputTokens, outputTokens, {
|
|
332
|
+
await addCost(resolvedSid, costUsd, durationMs, numTurns, inputTokens, outputTokens, {
|
|
333
333
|
model: resultModel,
|
|
334
334
|
stopReason: sdkMsg.subtype,
|
|
335
335
|
isError: 1,
|
|
336
336
|
cacheReadTokens,
|
|
337
337
|
cacheCreationTokens,
|
|
338
338
|
});
|
|
339
|
-
addMessage(resolvedSid, "error", JSON.stringify({ error: errMsg, subtype: sdkMsg.subtype }), null);
|
|
339
|
+
await addMessage(resolvedSid, "error", JSON.stringify({ error: errMsg, subtype: sdkMsg.subtype }), null);
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
lastAgentMetrics = { durationMs, costUsd, inputTokens, outputTokens, model: resultModel, turns: numTurns, isError: true, error: errMsg };
|
|
@@ -345,11 +345,11 @@ export async function runAgent({
|
|
|
345
345
|
|
|
346
346
|
// Record error for monitoring
|
|
347
347
|
try {
|
|
348
|
-
recordAgentRunComplete(monitorRunId, agentId, 'error', numTurns, costUsd, durationMs, inputTokens, outputTokens, errMsg);
|
|
348
|
+
await recordAgentRunComplete(monitorRunId, agentId, 'error', numTurns, costUsd, durationMs, inputTokens, outputTokens, errMsg);
|
|
349
349
|
} catch (e) { /* ignore */ }
|
|
350
350
|
|
|
351
351
|
// Log error notification
|
|
352
|
-
logNotification('error', `Agent "${agentDef.title}" failed`,
|
|
352
|
+
await logNotification('error', `Agent "${agentDef.title}" failed`,
|
|
353
353
|
errMsg.slice(0, 200),
|
|
354
354
|
JSON.stringify({ costUsd, durationMs, error: errMsg }),
|
|
355
355
|
resolvedSid, agentId);
|
|
@@ -361,11 +361,11 @@ export async function runAgent({
|
|
|
361
361
|
if (err.name === "AbortError") {
|
|
362
362
|
agentSend({ type: "agent_aborted", agentId, turn: turnCount });
|
|
363
363
|
agentSend({ type: "aborted" });
|
|
364
|
-
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 */ }
|
|
365
365
|
} else {
|
|
366
366
|
agentSend({ type: "agent_error", agentId, error: err.message, turn: turnCount });
|
|
367
367
|
agentSend({ type: "error", error: err.message });
|
|
368
|
-
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 */ }
|
|
369
369
|
}
|
|
370
370
|
throw err; // Re-throw so callers (chains, DAGs) know the agent failed
|
|
371
371
|
} finally {
|
|
@@ -413,8 +413,8 @@ export async function runAgent({
|
|
|
413
413
|
// Auto-capture memories from agent output
|
|
414
414
|
if (cwd && lastAssistantText) {
|
|
415
415
|
try {
|
|
416
|
-
const explicitCount = saveExplicitMemories(cwd, lastAssistantText, resolvedSid);
|
|
417
|
-
const autoCount = captureMemories(cwd, lastAssistantText, resolvedSid, agentId);
|
|
416
|
+
const explicitCount = await saveExplicitMemories(cwd, lastAssistantText, resolvedSid);
|
|
417
|
+
const autoCount = await captureMemories(cwd, lastAssistantText, resolvedSid, agentId);
|
|
418
418
|
const totalCaptured = explicitCount + autoCount;
|
|
419
419
|
if (totalCaptured > 0) {
|
|
420
420
|
console.log(`Captured ${totalCaptured} memories (${explicitCount} explicit, ${autoCount} auto) from agent ${agentId}`);
|