@ai-setting/roy-plugin-task-show 2.4.2 → 2.5.1
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/dist/chat-output-parser.d.ts +26 -0
- package/dist/chat-output-parser.d.ts.map +1 -0
- package/dist/chat-output-parser.js +36 -0
- package/dist/chat-output-parser.js.map +1 -0
- package/dist/chat-panel-html.d.ts +76 -0
- package/dist/chat-panel-html.d.ts.map +1 -0
- package/dist/chat-panel-html.js +117 -0
- package/dist/chat-panel-html.js.map +1 -0
- package/dist/chat-process-manager.d.ts +93 -0
- package/dist/chat-process-manager.d.ts.map +1 -0
- package/dist/chat-process-manager.js +197 -0
- package/dist/chat-process-manager.js.map +1 -0
- package/dist/server.d.ts +55 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +408 -4
- package/dist/server.js.map +1 -1
- package/dist/task-chat-context-builder.d.ts +94 -0
- package/dist/task-chat-context-builder.d.ts.map +1 -0
- package/dist/task-chat-context-builder.js +130 -0
- package/dist/task-chat-context-builder.js.map +1 -0
- package/dist/task-session-store.d.ts.map +1 -1
- package/dist/task-session-store.js +5 -0
- package/dist/task-session-store.js.map +1 -1
- package/package.json +4 -2
- package/plugin.json +1 -1
- package/public/chat-panel.js +565 -0
- package/public/dompurify.min.js +3 -0
- package/public/index.html +11 -0
- package/public/markdown-renderer.js +176 -0
- package/public/marked.min.js +6 -0
- package/public/style.css +316 -0
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAGlC,OAAO,KAAK,EACV,WAAW,EACX,cAAc,EACf,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,OAAO,EACL,eAAe,EAChB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,cAAc,EACf,MAAM,uBAAuB,CAAC;AAkB/B,OAAO,EACL,gBAAgB,EAEjB,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/dist/server.js
CHANGED
|
@@ -36,6 +36,9 @@ import { TracedAs } from "./tracing/decorator.js";
|
|
|
36
36
|
import { buildSessionFileTree, } from "./file-tree.js";
|
|
37
37
|
import { renderToolCallDetail, } from "./tool-call-detail.js";
|
|
38
38
|
import { renderSessionForestPage, } from "./task-session-store.js";
|
|
39
|
+
import { ChatProcessManager } from "./chat-process-manager.js";
|
|
40
|
+
import { renderChatPanel } from "./chat-panel-html.js";
|
|
41
|
+
import { TaskChatContextBuilder } from "./task-chat-context-builder.js";
|
|
39
42
|
/** Upper bound of the TCP user port range. Probing never wraps past this. */
|
|
40
43
|
export const MAX_TCP_PORT = 65535;
|
|
41
44
|
/** Smallest legal TCP user port (avoid binding to well-known 0..1023). */
|
|
@@ -177,6 +180,29 @@ function getDirname(metaUrl) {
|
|
|
177
180
|
return path.dirname(fileURLToPath(metaUrl));
|
|
178
181
|
return typeof __dirname !== "undefined" ? __dirname : process.cwd();
|
|
179
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Read the request body and parse it as JSON. Returns an empty object
|
|
185
|
+
* when the body is empty or non-JSON. Used by the chat routes to
|
|
186
|
+
* gracefully handle curl probes that omit Content-Type.
|
|
187
|
+
*/
|
|
188
|
+
async function readJsonBody(req) {
|
|
189
|
+
return new Promise((resolve) => {
|
|
190
|
+
const chunks = [];
|
|
191
|
+
req.on("data", (c) => chunks.push(c));
|
|
192
|
+
req.on("end", () => {
|
|
193
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
194
|
+
if (!raw)
|
|
195
|
+
return resolve({});
|
|
196
|
+
try {
|
|
197
|
+
resolve(JSON.parse(raw));
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
resolve({});
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
req.on("error", () => resolve({}));
|
|
204
|
+
});
|
|
205
|
+
}
|
|
180
206
|
// ============================================================================
|
|
181
207
|
// TaskShowServer
|
|
182
208
|
// ============================================================================
|
|
@@ -202,6 +228,8 @@ export class TaskShowServer {
|
|
|
202
228
|
/** v0.9.0+: optional session-scoped task store. When set, GET / renders the
|
|
203
229
|
* session forest; otherwise the legacy cross-host tree view is used. */
|
|
204
230
|
sessionStore;
|
|
231
|
+
/** v2.5.0+: ChatProcessManager singleton (null when chat is disabled). */
|
|
232
|
+
chatManager;
|
|
205
233
|
constructor(opts) {
|
|
206
234
|
this.cfg = opts.cfg;
|
|
207
235
|
this.collector = opts.collector;
|
|
@@ -212,6 +240,18 @@ export class TaskShowServer {
|
|
|
212
240
|
this.tasksTreeCache = opts.tasksTreeCache ?? null;
|
|
213
241
|
this.sessionStore = opts.sessionStore ?? null;
|
|
214
242
|
this.pluginVersion = opts.pluginVersion ?? "1.1.0";
|
|
243
|
+
// v2.5.0+: ChatProcessManager is a per-server singleton. It owns the
|
|
244
|
+
// subprocess lifecycle and is shared across all /api/chat/* routes.
|
|
245
|
+
if (opts.chatOptions && opts.chatOptions.enabled !== false && opts.chatOptions.cliPath) {
|
|
246
|
+
this.chatManager = new ChatProcessManager({
|
|
247
|
+
cliPath: opts.chatOptions.cliPath,
|
|
248
|
+
maxConcurrent: opts.chatOptions.maxConcurrent ?? 4,
|
|
249
|
+
defaultTimeoutMs: opts.chatOptions.defaultTimeoutMs ?? 300_000,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
this.chatManager = null;
|
|
254
|
+
}
|
|
215
255
|
}
|
|
216
256
|
/**
|
|
217
257
|
* Get the SSE event bus (for tests / health checks).
|
|
@@ -342,16 +382,57 @@ export class TaskShowServer {
|
|
|
342
382
|
async handle(req, res) {
|
|
343
383
|
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
344
384
|
const method = (req.method ?? "GET").toUpperCase();
|
|
345
|
-
if (method !== "GET" && method !== "HEAD") {
|
|
346
|
-
this.text(res, 405, "Method Not Allowed");
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
385
|
const pathname = url.pathname;
|
|
350
386
|
// /api/events — SSE stream
|
|
351
387
|
if (pathname === "/api/events") {
|
|
388
|
+
if (method !== "GET") {
|
|
389
|
+
this.text(res, 405, "Method Not Allowed");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
352
392
|
this.handleSSE(req, res);
|
|
353
393
|
return;
|
|
354
394
|
}
|
|
395
|
+
// v2.5.0+: /api/chat/* — home-page chat. The /start endpoint also
|
|
396
|
+
// accepts GET so plain curl/EventSource probes work without a body.
|
|
397
|
+
if (pathname === "/api/chat/start") {
|
|
398
|
+
await this.handleChatStart(req, res);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const chatMsgMatch = pathname.match(/^\/api\/chat\/([^/]+)\/message$/);
|
|
402
|
+
if (chatMsgMatch) {
|
|
403
|
+
const sid = decodeURIComponent(chatMsgMatch[1]);
|
|
404
|
+
await this.handleChatMessage(sid, req, res);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const chatAbortMatch = pathname.match(/^\/api\/chat\/([^/]+)\/abort$/);
|
|
408
|
+
if (chatAbortMatch) {
|
|
409
|
+
const sid = decodeURIComponent(chatAbortMatch[1]);
|
|
410
|
+
this.handleChatAbort(sid, res);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
// v2.5.0+ REQ-3: /api/task-chat/:taskId/message — task-aware chat.
|
|
414
|
+
// Same SSE protocol as /api/chat/:id/message but the subprocess
|
|
415
|
+
// sees a `task-${taskId}` sessionId (deterministic, long-lived)
|
|
416
|
+
// and the user message is wrapped with an additionInfo block
|
|
417
|
+
// describing the task context. The taskId itself must exist in
|
|
418
|
+
// either the collector OR the operations cache — otherwise we
|
|
419
|
+
// 404 instead of silently answering off-topic.
|
|
420
|
+
const taskChatMatch = pathname.match(/^\/api\/task-chat\/([^/]+)\/message$/);
|
|
421
|
+
if (taskChatMatch) {
|
|
422
|
+
const rawId = decodeURIComponent(taskChatMatch[1]);
|
|
423
|
+
const numericId = Number(rawId);
|
|
424
|
+
if (!Number.isFinite(numericId)) {
|
|
425
|
+
this.text(res, 400, `Invalid task id: ${rawId}`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
await this.handleTaskChatMessage(numericId, req, res);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
// All remaining routes are GET/HEAD only.
|
|
432
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
433
|
+
this.text(res, 405, "Method Not Allowed");
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
355
436
|
// /api/sessions
|
|
356
437
|
if (pathname === "/api/sessions") {
|
|
357
438
|
this.json(res, 200, this.collector.listSessions());
|
|
@@ -461,6 +542,301 @@ export class TaskShowServer {
|
|
|
461
542
|
* directories (defaults to cwd + $HOME; configurable via
|
|
462
543
|
* `ts.config.fileSandboxPaths`).
|
|
463
544
|
*/
|
|
545
|
+
// -------------------------------------------------------------------------
|
|
546
|
+
// v2.5.0+ chat handlers — POST/GET /api/chat/start,
|
|
547
|
+
// POST /api/chat/:id/message (SSE), POST /api/chat/:id/abort.
|
|
548
|
+
//
|
|
549
|
+
// The actual subprocess orchestration lives in `ChatProcessManager`
|
|
550
|
+
// (src/chat-process-manager.ts). These handlers translate HTTP + SSE
|
|
551
|
+
// concerns into chunk callbacks.
|
|
552
|
+
// -------------------------------------------------------------------------
|
|
553
|
+
/**
|
|
554
|
+
* Allocate a fresh chat sessionId. The id is just a string — the
|
|
555
|
+
* underlying SQLite store lives inside the `roy-agent act` subprocess
|
|
556
|
+
* (we never persist anything ourselves; on the next `act -s <id>` call
|
|
557
|
+
* the CLI resurrects the session from disk if it still exists).
|
|
558
|
+
*/
|
|
559
|
+
handleChatStart(req, res) {
|
|
560
|
+
if (!this.chatManager) {
|
|
561
|
+
this.json(res, 503, { error: "chat_disabled", message: "Chat feature is not enabled on this server." });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
565
|
+
if (method !== "POST" && method !== "GET" && method !== "HEAD") {
|
|
566
|
+
this.text(res, 405, "Method Not Allowed");
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
// Use crypto.randomUUID when available; fall back to time + random.
|
|
570
|
+
const id = typeof globalThis.crypto?.randomUUID === "function"
|
|
571
|
+
? globalThis.crypto.randomUUID()
|
|
572
|
+
: `sess_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
573
|
+
const sessionId = `session_${id}`;
|
|
574
|
+
this.json(res, 200, {
|
|
575
|
+
sessionId,
|
|
576
|
+
createdAt: new Date().toISOString(),
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* SSE endpoint. Accepts `application/json` POST body and emits a series
|
|
581
|
+
* of SSE frames: `event: chunk` for every ChatChunk, then
|
|
582
|
+
* `event: done` once the subprocess exits (or `event: error` on
|
|
583
|
+
* failure).
|
|
584
|
+
*/
|
|
585
|
+
async handleChatMessage(sessionId, req, res) {
|
|
586
|
+
if (!this.chatManager) {
|
|
587
|
+
this.json(res, 503, { error: "chat_disabled" });
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
591
|
+
if (method !== "POST" && method !== "GET") {
|
|
592
|
+
this.text(res, 405, "Method Not Allowed");
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
let message = "";
|
|
596
|
+
if (method === "POST") {
|
|
597
|
+
const body = await readJsonBody(req);
|
|
598
|
+
if (body && typeof body === "object" && typeof body.message === "string") {
|
|
599
|
+
message = body.message;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (!message && method === "GET") {
|
|
603
|
+
message = "";
|
|
604
|
+
}
|
|
605
|
+
if (typeof message !== "string" || message.length === 0) {
|
|
606
|
+
this.json(res, 400, { error: "bad_request", message: "Missing `message` field in JSON body." });
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
// SSE headers.
|
|
610
|
+
res.statusCode = 200;
|
|
611
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
612
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
613
|
+
res.setHeader("Connection", "keep-alive");
|
|
614
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
615
|
+
res.flushHeaders?.();
|
|
616
|
+
let chunkIndex = 0;
|
|
617
|
+
const sendFrame = (event, payload) => {
|
|
618
|
+
if (res.writableEnded || res.destroyed)
|
|
619
|
+
return;
|
|
620
|
+
const id = ++chunkIndex;
|
|
621
|
+
try {
|
|
622
|
+
res.write(`id: ${id}\nevent: ${event}\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
/* socket closed mid-write */
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
// Initial heartbeat so the client knows the connection is live before
|
|
629
|
+
// the subprocess warms up.
|
|
630
|
+
sendFrame("ready", { sessionId });
|
|
631
|
+
try {
|
|
632
|
+
const result = await this.chatManager.sendStream(sessionId, message, (chunk) => {
|
|
633
|
+
sendFrame("chunk", chunk);
|
|
634
|
+
if (chunk.type === "done" || chunk.type === "error") {
|
|
635
|
+
// Mirror the terminal event on its own line so EventSource
|
|
636
|
+
// listeners can register `addEventListener("done", ...)`.
|
|
637
|
+
sendFrame(chunk.type, chunk);
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
sendFrame("done", { sessionId, exitCode: result.exitCode, durationMs: result.durationMs });
|
|
641
|
+
}
|
|
642
|
+
catch (err) {
|
|
643
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
644
|
+
sendFrame("error", { sessionId, error: { message: msg } });
|
|
645
|
+
}
|
|
646
|
+
try {
|
|
647
|
+
res.end();
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
/* ignore */
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/** Abort an in-flight chat query for the given session. */
|
|
654
|
+
handleChatAbort(sessionId, res) {
|
|
655
|
+
if (!this.chatManager) {
|
|
656
|
+
this.json(res, 503, { error: "chat_disabled" });
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
this.chatManager.abort(sessionId);
|
|
660
|
+
this.json(res, 200, { sessionId, aborted: true });
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* v2.5.0+ REQ-3: task-aware chat endpoint.
|
|
664
|
+
*
|
|
665
|
+
* Resolves the task by id (from the collector session OR the
|
|
666
|
+
* operations cache), builds an additionInfo block from the task
|
|
667
|
+
* context, wraps the user's message, and forwards it to the same
|
|
668
|
+
* ChatProcessManager REQ-2 already plumbs. The sessionId is the
|
|
669
|
+
* deterministic `task-${taskId}` so reloads land on the same
|
|
670
|
+
* SQLite-backed conversation.
|
|
671
|
+
*
|
|
672
|
+
* Returns 404 when the task is unknown — we deliberately do NOT
|
|
673
|
+
* answer off-topic questions for tasks that don't exist (which is
|
|
674
|
+
* the same behaviour REQ-2 already shows for unknown chat
|
|
675
|
+
* sessions, just gated by a task id instead of a random uuid).
|
|
676
|
+
*/
|
|
677
|
+
async handleTaskChatMessage(taskId, req, res) {
|
|
678
|
+
if (!this.chatManager) {
|
|
679
|
+
this.json(res, 503, { error: "chat_disabled" });
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
683
|
+
if (method !== "POST" && method !== "GET") {
|
|
684
|
+
this.text(res, 405, "Method Not Allowed");
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
let message = "";
|
|
688
|
+
if (method === "POST") {
|
|
689
|
+
const body = await readJsonBody(req);
|
|
690
|
+
if (body && typeof body === "object" && typeof body.message === "string") {
|
|
691
|
+
message = body.message;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (typeof message !== "string" || message.length === 0) {
|
|
695
|
+
this.json(res, 400, { error: "bad_request", message: "Missing `message` field in JSON body." });
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
// Resolve the task context. Collector sessions are the primary
|
|
699
|
+
// source for in-progress tasks (live `title` / `status` /
|
|
700
|
+
// `context.worktree`); the operations cache is the fallback for
|
|
701
|
+
// tasks the user is asking about retroactively (session is gone
|
|
702
|
+
// but the CLI still has the description / operations). If neither
|
|
703
|
+
// knows the task, return 404 — answering without context would
|
|
704
|
+
// produce a misleading answer.
|
|
705
|
+
const ctx = await this.resolveTaskChatContext(taskId);
|
|
706
|
+
if (!ctx) {
|
|
707
|
+
this.json(res, 404, { error: "not_found", id: taskId });
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
const builder = new TaskChatContextBuilder(ctx);
|
|
711
|
+
const sessionId = builder.sessionId();
|
|
712
|
+
const wrapped = builder.wrapMessage(message);
|
|
713
|
+
// SSE headers (mirror /api/chat/:id/message so the frontend SSE
|
|
714
|
+
// parser can stay identical).
|
|
715
|
+
res.statusCode = 200;
|
|
716
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
717
|
+
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
718
|
+
res.setHeader("Connection", "keep-alive");
|
|
719
|
+
res.setHeader("X-Accel-Buffering", "no");
|
|
720
|
+
res.flushHeaders?.();
|
|
721
|
+
let chunkIndex = 0;
|
|
722
|
+
const sendFrame = (event, payload) => {
|
|
723
|
+
if (res.writableEnded || res.destroyed)
|
|
724
|
+
return;
|
|
725
|
+
const id = ++chunkIndex;
|
|
726
|
+
try {
|
|
727
|
+
res.write(`id: ${id}\nevent: ${event}\ndata: ${JSON.stringify(payload)}\n\n`);
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
/* socket closed mid-write */
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
// Heartbeat so the client knows the connection is live.
|
|
734
|
+
sendFrame("ready", { sessionId, taskId, truncated: builder.truncated });
|
|
735
|
+
try {
|
|
736
|
+
const result = await this.chatManager.sendStream(sessionId, wrapped, (chunk) => {
|
|
737
|
+
sendFrame("chunk", chunk);
|
|
738
|
+
if (chunk.type === "done" || chunk.type === "error") {
|
|
739
|
+
sendFrame(chunk.type, chunk);
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
sendFrame("done", {
|
|
743
|
+
sessionId,
|
|
744
|
+
taskId,
|
|
745
|
+
exitCode: result.exitCode,
|
|
746
|
+
durationMs: result.durationMs,
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
catch (err) {
|
|
750
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
751
|
+
sendFrame("error", { sessionId, taskId, error: { message: msg } });
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
res.end();
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
/* ignore */
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Resolve the task context for a chat turn. Returns the
|
|
762
|
+
* TaskChatContextBuilder input or null when the task is unknown.
|
|
763
|
+
* Prefers the live collector session (rich `context.worktree` +
|
|
764
|
+
* current status); falls back to the operations cache envelope
|
|
765
|
+
* for tasks that have already been finalized.
|
|
766
|
+
*/
|
|
767
|
+
async resolveTaskChatContext(taskId) {
|
|
768
|
+
const live = this.collector.getSession(taskId);
|
|
769
|
+
if (live) {
|
|
770
|
+
// Best-effort: pull description / priority / operations from
|
|
771
|
+
// the operations cache if available (the CLI is the canonical
|
|
772
|
+
// source for those — collector only tracks tool calls).
|
|
773
|
+
let description;
|
|
774
|
+
let priority = live.status; // sensible fallback
|
|
775
|
+
let recentOperations = [];
|
|
776
|
+
if (this.operationsCache) {
|
|
777
|
+
try {
|
|
778
|
+
const cached = await this.operationsCache.get(taskId, { allowStale: true });
|
|
779
|
+
description = cached.task.context && typeof cached.task.context === "object"
|
|
780
|
+
? JSON.stringify(cached.task.context)
|
|
781
|
+
: undefined;
|
|
782
|
+
if (cached.task.goals_and_expected_deliverables && !description) {
|
|
783
|
+
description = cached.task.goals_and_expected_deliverables;
|
|
784
|
+
}
|
|
785
|
+
priority = cached.task.priority ?? priority;
|
|
786
|
+
recentOperations = cached.operations.slice(0, 10).map((op) => ({
|
|
787
|
+
id: op.id,
|
|
788
|
+
sequence: op.sequence,
|
|
789
|
+
milestoneType: op.milestoneType,
|
|
790
|
+
title: op.title,
|
|
791
|
+
description: op.description,
|
|
792
|
+
processDescription: op.processDescription,
|
|
793
|
+
timestamp: op.timestamp,
|
|
794
|
+
sessionShort: op.sessionShort,
|
|
795
|
+
}));
|
|
796
|
+
}
|
|
797
|
+
catch {
|
|
798
|
+
/* fall through with defaults — we still answer */
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return {
|
|
802
|
+
taskId: live.taskId,
|
|
803
|
+
title: live.title,
|
|
804
|
+
description,
|
|
805
|
+
status: live.status,
|
|
806
|
+
priority,
|
|
807
|
+
worktree: live.context?.worktree,
|
|
808
|
+
recentOperations,
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
if (this.operationsCache) {
|
|
812
|
+
try {
|
|
813
|
+
const cached = await this.operationsCache.get(taskId, { allowStale: true });
|
|
814
|
+
const ctx = cached.task.context;
|
|
815
|
+
return {
|
|
816
|
+
taskId: cached.task.id,
|
|
817
|
+
title: cached.task.title,
|
|
818
|
+
description: ctx && typeof ctx === "object" ? JSON.stringify(ctx) : cached.task.goals_and_expected_deliverables,
|
|
819
|
+
status: cached.task.status,
|
|
820
|
+
priority: cached.task.priority,
|
|
821
|
+
worktree: cached.task.projectPath,
|
|
822
|
+
recentOperations: cached.operations.slice(0, 10).map((op) => ({
|
|
823
|
+
id: op.id,
|
|
824
|
+
sequence: op.sequence,
|
|
825
|
+
milestoneType: op.milestoneType,
|
|
826
|
+
title: op.title,
|
|
827
|
+
description: op.description,
|
|
828
|
+
processDescription: op.processDescription,
|
|
829
|
+
timestamp: op.timestamp,
|
|
830
|
+
sessionShort: op.sessionShort,
|
|
831
|
+
})),
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
464
840
|
handleFileContent(url, req, res) {
|
|
465
841
|
const wantsHtml = prefersHtml(req);
|
|
466
842
|
const raw = url.searchParams.get("path") ?? "";
|
|
@@ -1190,6 +1566,8 @@ export function renderTaskPage(session) {
|
|
|
1190
1566
|
<pre class="json">${htmlEscape(JSON.stringify(session, null, 2))}</pre>
|
|
1191
1567
|
</details>
|
|
1192
1568
|
</section>
|
|
1569
|
+
|
|
1570
|
+
${renderChatPanel({ scope: "task", taskId: session.taskId })}
|
|
1193
1571
|
</div>
|
|
1194
1572
|
</div>
|
|
1195
1573
|
|
|
@@ -1198,6 +1576,7 @@ export function renderTaskPage(session) {
|
|
|
1198
1576
|
<script src="/static/task-operations.js"></script>
|
|
1199
1577
|
<script src="/static/file-tree.js"></script>
|
|
1200
1578
|
<script src="/static/tool-call-detail.js"></script>
|
|
1579
|
+
<script src="/static/chat-panel.js" defer></script>
|
|
1201
1580
|
<script>
|
|
1202
1581
|
if (window.mermaid) {
|
|
1203
1582
|
window.mermaid.initialize({
|
|
@@ -1337,6 +1716,18 @@ function walkTreeFlat(node, depth, out, affectedSet) {
|
|
|
1337
1716
|
walkTreeFlat(child, depth + 1, out, affectedSet);
|
|
1338
1717
|
}
|
|
1339
1718
|
}
|
|
1719
|
+
// v2.5.1+ REQ-2 fix: `renderChatPanel` now lives in `./chat-panel-html.ts`
|
|
1720
|
+
// so BOTH `renderIndexPage` (legacy home, server.ts) and
|
|
1721
|
+
// `_renderSessionForestPageImpl` (production home, task-session-store.ts)
|
|
1722
|
+
// can embed the same markup. See chat-panel-html.ts for the asset
|
|
1723
|
+
// contract that home-chat-v25.test.ts and
|
|
1724
|
+
// chat-panel-session-forest-v251.test.ts pin.
|
|
1725
|
+
//
|
|
1726
|
+
// v2.5 REQ-3: `renderChatPanel()` accepts `{scope, taskId}` so the
|
|
1727
|
+
// task-detail page can embed a task-aware panel (with
|
|
1728
|
+
// `data-chat-scope="task" data-task-id="N"` hooks the JS factory reads
|
|
1729
|
+
// to route to /api/task-chat/<N>/message). The contract is pinned by
|
|
1730
|
+
// test/task-chat-v25.test.ts.
|
|
1340
1731
|
function renderIndexPage(sessions) {
|
|
1341
1732
|
// SSR hint — the tree container is always rendered; if /api/tasks/tree
|
|
1342
1733
|
// returns 503 the JS shows a graceful empty state with a hint to
|
|
@@ -1361,6 +1752,8 @@ function renderIndexPage(sessions) {
|
|
|
1361
1752
|
</p>
|
|
1362
1753
|
</header>
|
|
1363
1754
|
|
|
1755
|
+
${renderChatPanel()}
|
|
1756
|
+
|
|
1364
1757
|
<section class="panel tree-controls">
|
|
1365
1758
|
<div class="tree-controls-row">
|
|
1366
1759
|
<label class="tree-search">
|
|
@@ -1449,6 +1842,17 @@ function renderIndexPage(sessions) {
|
|
|
1449
1842
|
<script src="/static/app.js"></script>
|
|
1450
1843
|
<script src="/static/mermaid-renderer.js"></script>
|
|
1451
1844
|
<script src="/static/tasks-tree.js"></script>
|
|
1845
|
+
<!--
|
|
1846
|
+
v2.5.x REQ-4: vendor markdown rendering for the home chat panel.
|
|
1847
|
+
Load order matters - marked must register globals before DOMPurify
|
|
1848
|
+
is constructed, and both must register before markdown-renderer.js
|
|
1849
|
+
resolves them at import time. chat-panel.js only depends on
|
|
1850
|
+
window.markdownRenderer, which is set by markdown-renderer.js.
|
|
1851
|
+
-->
|
|
1852
|
+
<script src="/static/marked.min.js" defer></script>
|
|
1853
|
+
<script src="/static/dompurify.min.js" defer></script>
|
|
1854
|
+
<script src="/static/markdown-renderer.js" defer></script>
|
|
1855
|
+
<script src="/static/chat-panel.js" defer></script>
|
|
1452
1856
|
</body>
|
|
1453
1857
|
</html>`;
|
|
1454
1858
|
}
|