@ai-setting/roy-plugin-task-show 2.5.0 → 2.5.2

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.
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @fileoverview Chat panel HTML — extracted v2.5.1 for cross-page reuse.
3
+ *
4
+ * ## Why this module exists (v2.5.1 REQ-2 fix + v2.5 REQ-3 extension)
5
+ *
6
+ * v2.5.0 introduced the home-page chat panel via `renderChatPanel()`
7
+ * defined inside `src/server.ts`. Only `renderIndexPage` (the legacy
8
+ * home page, hit when no `TaskSessionStore` is configured) actually
9
+ * called it. The plugin always wires a `TaskSessionStore`, so the
10
+ * production home page (`renderSessionForestPage` in
11
+ * `src/task-session-store.ts`) shipped **without** the chat panel.
12
+ *
13
+ * The pre-existing `home-chat-v25.test.ts` constructed a server
14
+ * WITHOUT `sessionStore`, so the green test exercised the legacy
15
+ * path and the v2.5.0 bug slipped through.
16
+ *
17
+ * The v2.5.1 fix:
18
+ * 1. Extract `renderChatPanel()` into this shared module.
19
+ * 2. Have BOTH `renderIndexPage` (legacy) AND
20
+ * `_renderSessionForestPageImpl` (production home) import it.
21
+ * 3. Both pages also need `<script src="/static/chat-panel.js"
22
+ * defer></script>` for the JS to actually wire up the form.
23
+ *
24
+ * v2.5 REQ-3 extends `renderChatPanel()` to accept an `opts` bag
25
+ * with `scope` ("home" | "task") and `taskId` (when scope=task).
26
+ * The HTML carries `data-chat-scope` and `data-task-id` hooks that
27
+ * the chat-panel.js factory picks up to route to `/api/chat/...`
28
+ * vs `/api/task-chat/...` and to select the right localStorage key.
29
+ *
30
+ * Extracting to a dedicated module avoids a circular import between
31
+ * `server.ts` ↔ `task-session-store.ts` (server already imports from
32
+ * task-session-store, and task-session-store previously imported
33
+ * nothing from server, but future plugin evolution may want
34
+ * session-store to depend on other server-side types — keeping
35
+ * chat-panel-html a leaf module is a safer contract).
36
+ *
37
+ * The asset contract pinned by `test/home-chat-v25.test.ts`,
38
+ * `test/chat-panel-session-forest-v251.test.ts` and
39
+ * `test/task-chat-v25.test.ts`:
40
+ * • `data-chat-shell` — the section root
41
+ * • `data-chat-scope` — "home" | "task" (read by the JS factory)
42
+ * • `data-task-id` — task id when scope=task
43
+ * • `data-chat-input` — the text input
44
+ * • `data-chat-send` — the submit button
45
+ * • `data-chat-form` — the `<form>` (used by the JS to bind Enter)
46
+ * • `data-chat-messages` — the message log container
47
+ * • `<script src="/static/chat-panel.js" defer></script>` — must
48
+ * follow the markup on the same page (defer is fine because the
49
+ * JS module polls for the DOM on load).
50
+ *
51
+ * Disabled gracefully: when the host didn't pass
52
+ * `chatOptions.enabled` we render the empty section so SSR layout
53
+ * stays predictable; the JS short-circuits and the `/api/chat/*`
54
+ * endpoints return 503.
55
+ */
56
+ /**
57
+ * Render the home-page chat panel markup.
58
+ *
59
+ * Markup is intentionally minimal — the heavy lifting (EventSource
60
+ * handling, message rendering, sessionId persistence) lives in
61
+ * `public/chat-panel.js`. The HTML below carries `data-*` hooks that
62
+ * the JS module targets; the contracts are pinned by
63
+ * `test/home-chat-v25.test.ts`,
64
+ * `test/chat-panel-session-forest-v251.test.ts` and
65
+ * `test/task-chat-v25.test.ts`.
66
+ *
67
+ * @param opts.scope "home" (default) | "task" — controls the
68
+ * API endpoint the JS targets and the UI badge.
69
+ * @param opts.taskId Required when scope === "task". Used to label
70
+ * the panel and bind `data-task-id`.
71
+ */
72
+ export declare function renderChatPanel(opts?: {
73
+ scope?: "home" | "task";
74
+ taskId?: number;
75
+ }): string;
76
+ //# sourceMappingURL=chat-panel-html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-panel-html.d.ts","sourceRoot":"","sources":["../src/chat-panel-html.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,MAAM,CA6C/F"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @fileoverview Chat panel HTML — extracted v2.5.1 for cross-page reuse.
3
+ *
4
+ * ## Why this module exists (v2.5.1 REQ-2 fix + v2.5 REQ-3 extension)
5
+ *
6
+ * v2.5.0 introduced the home-page chat panel via `renderChatPanel()`
7
+ * defined inside `src/server.ts`. Only `renderIndexPage` (the legacy
8
+ * home page, hit when no `TaskSessionStore` is configured) actually
9
+ * called it. The plugin always wires a `TaskSessionStore`, so the
10
+ * production home page (`renderSessionForestPage` in
11
+ * `src/task-session-store.ts`) shipped **without** the chat panel.
12
+ *
13
+ * The pre-existing `home-chat-v25.test.ts` constructed a server
14
+ * WITHOUT `sessionStore`, so the green test exercised the legacy
15
+ * path and the v2.5.0 bug slipped through.
16
+ *
17
+ * The v2.5.1 fix:
18
+ * 1. Extract `renderChatPanel()` into this shared module.
19
+ * 2. Have BOTH `renderIndexPage` (legacy) AND
20
+ * `_renderSessionForestPageImpl` (production home) import it.
21
+ * 3. Both pages also need `<script src="/static/chat-panel.js"
22
+ * defer></script>` for the JS to actually wire up the form.
23
+ *
24
+ * v2.5 REQ-3 extends `renderChatPanel()` to accept an `opts` bag
25
+ * with `scope` ("home" | "task") and `taskId` (when scope=task).
26
+ * The HTML carries `data-chat-scope` and `data-task-id` hooks that
27
+ * the chat-panel.js factory picks up to route to `/api/chat/...`
28
+ * vs `/api/task-chat/...` and to select the right localStorage key.
29
+ *
30
+ * Extracting to a dedicated module avoids a circular import between
31
+ * `server.ts` ↔ `task-session-store.ts` (server already imports from
32
+ * task-session-store, and task-session-store previously imported
33
+ * nothing from server, but future plugin evolution may want
34
+ * session-store to depend on other server-side types — keeping
35
+ * chat-panel-html a leaf module is a safer contract).
36
+ *
37
+ * The asset contract pinned by `test/home-chat-v25.test.ts`,
38
+ * `test/chat-panel-session-forest-v251.test.ts` and
39
+ * `test/task-chat-v25.test.ts`:
40
+ * • `data-chat-shell` — the section root
41
+ * • `data-chat-scope` — "home" | "task" (read by the JS factory)
42
+ * • `data-task-id` — task id when scope=task
43
+ * • `data-chat-input` — the text input
44
+ * • `data-chat-send` — the submit button
45
+ * • `data-chat-form` — the `<form>` (used by the JS to bind Enter)
46
+ * • `data-chat-messages` — the message log container
47
+ * • `<script src="/static/chat-panel.js" defer></script>` — must
48
+ * follow the markup on the same page (defer is fine because the
49
+ * JS module polls for the DOM on load).
50
+ *
51
+ * Disabled gracefully: when the host didn't pass
52
+ * `chatOptions.enabled` we render the empty section so SSR layout
53
+ * stays predictable; the JS short-circuits and the `/api/chat/*`
54
+ * endpoints return 503.
55
+ */
56
+ /**
57
+ * Render the home-page chat panel markup.
58
+ *
59
+ * Markup is intentionally minimal — the heavy lifting (EventSource
60
+ * handling, message rendering, sessionId persistence) lives in
61
+ * `public/chat-panel.js`. The HTML below carries `data-*` hooks that
62
+ * the JS module targets; the contracts are pinned by
63
+ * `test/home-chat-v25.test.ts`,
64
+ * `test/chat-panel-session-forest-v251.test.ts` and
65
+ * `test/task-chat-v25.test.ts`.
66
+ *
67
+ * @param opts.scope "home" (default) | "task" — controls the
68
+ * API endpoint the JS targets and the UI badge.
69
+ * @param opts.taskId Required when scope === "task". Used to label
70
+ * the panel and bind `data-task-id`.
71
+ */
72
+ export function renderChatPanel(opts = {}) {
73
+ const scope = opts.scope ?? "home";
74
+ const taskId = opts.taskId;
75
+ const isTask = scope === "task" && typeof taskId === "number";
76
+ const ariaLabel = isTask ? `Task #${taskId} chat panel` : "Home chat panel";
77
+ const heading = isTask ? "💬 Task chat" : "💬 roy-agent chat";
78
+ const badge = isTask
79
+ ? `<span class="chat-scope-badge" data-chat-scope-badge>Connected to task #${taskId}</span>`
80
+ : "";
81
+ const subline = isTask
82
+ ? `Multi-turn conversation powered by <code>roy-agent act -s task-${taskId}</code>. The task context (title, status, recent operations) is auto-injected into every message.`
83
+ : `Multi-turn conversation powered by <code>roy-agent act -s &lt;sessionId&gt;</code>.`;
84
+ const placeholder = isTask ? `Ask anything about task #${taskId}…` : "Type a message and press Enter…";
85
+ // Scope + taskId hooks the JS factory reads to pick the right
86
+ // endpoint and the right localStorage key.
87
+ const scopeAttrs = isTask
88
+ ? ` data-chat-scope="task" data-task-id="${taskId}"`
89
+ : ` data-chat-scope="home"`;
90
+ const extraClass = isTask ? " chat-panel--task" : "";
91
+ return `
92
+ <section class="panel chat-panel${extraClass}"${scopeAttrs} data-chat-shell aria-label="${ariaLabel}">
93
+ <header class="chat-panel-header">
94
+ <h2>${heading} ${badge}</h2>
95
+ <p class="meta chat-panel-meta">${subline}</p>
96
+ </header>
97
+ <div class="chat-messages" data-chat-messages role="log" aria-live="polite" aria-relevant="additions">
98
+ <p class="chat-empty">No messages yet — ask away!</p>
99
+ </div>
100
+ <form class="chat-form" data-chat-form autocomplete="off">
101
+ <label class="visually-hidden" for="chat-input">Message</label>
102
+ <input
103
+ id="chat-input"
104
+ type="text"
105
+ class="chat-input"
106
+ data-chat-input
107
+ placeholder="${placeholder}"
108
+ autocomplete="off"
109
+ />
110
+ <button type="submit" class="chat-send" data-chat-send aria-label="Send message">
111
+ Send
112
+ </button>
113
+ </form>
114
+ </section>
115
+ `;
116
+ }
117
+ //# sourceMappingURL=chat-panel-html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-panel-html.js","sourceRoot":"","sources":["../src/chat-panel-html.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAAC,OAAqD,EAAE;IACrF,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC;IACnC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC3B,MAAM,MAAM,GAAG,KAAK,KAAK,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC;IAC9D,MAAM,SAAS,GAAG,MAAM,CAAC,CAAC,CAAC,SAAS,MAAM,aAAa,CAAC,CAAC,CAAC,iBAAiB,CAAC;IAC5E,MAAM,OAAO,GAAG,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,mBAAmB,CAAC;IAC9D,MAAM,KAAK,GAAG,MAAM;QAClB,CAAC,CAAC,2EAA2E,MAAM,SAAS;QAC5F,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,OAAO,GAAG,MAAM;QACpB,CAAC,CAAC,kEAAkE,MAAM,mGAAmG;QAC7K,CAAC,CAAC,qFAAqF,CAAC;IAC1F,MAAM,WAAW,GAAG,MAAM,CAAC,CAAC,CAAC,4BAA4B,MAAM,GAAG,CAAC,CAAC,CAAC,iCAAiC,CAAC;IACvG,8DAA8D;IAC9D,2CAA2C;IAC3C,MAAM,UAAU,GAAG,MAAM;QACvB,CAAC,CAAC,yCAAyC,MAAM,GAAG;QACpD,CAAC,CAAC,yBAAyB,CAAC;IAC9B,MAAM,UAAU,GAAG,MAAM,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,EAAE,CAAC;IAErD,OAAO;oCAC2B,UAAU,IAAI,UAAU,gCAAgC,SAAS;;YAEzF,OAAO,IAAI,KAAK;wCACY,OAAO;;;;;;;;;;;;uBAYxB,WAAW;;;;;;;;CAQjC,CAAC;AACF,CAAC"}
package/dist/server.d.ts CHANGED
@@ -202,6 +202,30 @@ export declare class TaskShowServer {
202
202
  private handleChatMessage;
203
203
  /** Abort an in-flight chat query for the given session. */
204
204
  private handleChatAbort;
205
+ /**
206
+ * v2.5.0+ REQ-3: task-aware chat endpoint.
207
+ *
208
+ * Resolves the task by id (from the collector session OR the
209
+ * operations cache), builds an additionInfo block from the task
210
+ * context, wraps the user's message, and forwards it to the same
211
+ * ChatProcessManager REQ-2 already plumbs. The sessionId is the
212
+ * deterministic `task-${taskId}` so reloads land on the same
213
+ * SQLite-backed conversation.
214
+ *
215
+ * Returns 404 when the task is unknown — we deliberately do NOT
216
+ * answer off-topic questions for tasks that don't exist (which is
217
+ * the same behaviour REQ-2 already shows for unknown chat
218
+ * sessions, just gated by a task id instead of a random uuid).
219
+ */
220
+ private handleTaskChatMessage;
221
+ /**
222
+ * Resolve the task context for a chat turn. Returns the
223
+ * TaskChatContextBuilder input or null when the task is unknown.
224
+ * Prefers the live collector session (rich `context.worktree` +
225
+ * current status); falls back to the operations cache envelope
226
+ * for tasks that have already been finalized.
227
+ */
228
+ private resolveTaskChatContext;
205
229
  private handleFileContent;
206
230
  /**
207
231
  * v2.0.0: GET /api/task/:id/file-tree — returns the git-tracked
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
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;AAcjC,sEAAsE;AACtE,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;IACpB,wEAAwE;IACxE,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,6EAA6E;AAC7E,eAAO,MAAM,YAAY,QAAQ,CAAC;AAElC,0EAA0E;AAC1E,eAAO,MAAM,YAAY,OAAO,CAAC;AAIjC,4DAA4D;AAC5D,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1B,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1B,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5B,CAAC;AAEF,sCAAsC;AACtC,MAAM,WAAW,iBAAiB;IAChC,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,aAAa,EAAE,OAAO,CAAC;CACxB;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACtC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAkI7B;AAkCD,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAiB;IACrC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAoB;IAC9C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAW;IACpC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuF;IAC9G;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAmB;IACjD,8EAA8E;IAC9E,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyB;IACzD,oEAAoE;IACpE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAwB;IACvD;6EACyE;IACzE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA0B;IACvD,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4B;gBAE5C,IAAI,EAAE;QAChB,GAAG,EAAE,cAAc,CAAC;QACpB,SAAS,EAAE,iBAAiB,CAAC;QAC7B,0EAA0E;QAC1E,QAAQ,CAAC,EAAE,QAAQ,CAAC;QACpB,yEAAyE;QACzE,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE;YAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;YAAC,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;YAAC,KAAK,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;SAAE,CAAC;QAC9F,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,6FAA6F;QAC7F,eAAe,CAAC,EAAE,eAAe,CAAC;QAClC,kFAAkF;QAClF,cAAc,CAAC,EAAE,cAAc,CAAC;QAChC,gFAAgF;QAChF,YAAY,CAAC,EAAE,gBAAgB,CAAC;QAChC,uEAAuE;QACvE,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB;;;;;;WAMG;QACH,WAAW,CAAC,EAAE;YACZ,OAAO,EAAE,MAAM,CAAC;YAChB,aAAa,CAAC,EAAE,MAAM,CAAC;YACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;YAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;SACnB,CAAC;KACH;IAwBD;;OAEG;IACH,WAAW,IAAI,QAAQ;IAIvB;;;;;;OAMG;IACH,YAAY,IAAI,iBAAiB;IAIjC;;;;;;;;;OASG;IACH,KAAK,IAAI,OAAO,CAAC,UAAU,CAAC;IA8D5B;;;OAGG;IACH,OAAO,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAW7D;;OAEG;IACH,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YA4BP,MAAM;IAsKpB;;;;;OAKG;IACH;;;;;;;;;;;OAWG;IAUH;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAqBvB;;;;;OAKG;YACW,iBAAiB;IA+E/B,2DAA2D;IAC3D,OAAO,CAAC,eAAe;IASvB;;;;;;;;;;;;;;OAcG;YACW,qBAAqB;IAgGnC;;;;;;OAMG;YACW,sBAAsB;IAyFpC,OAAO,CAAC,iBAAiB;IA+DzB;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,SAAS;IA4BjB;;;;OAIG;YAEW,SAAS;IA2BvB;;;;OAIG;YACW,gBAAgB;IAqE9B;;;;;;;;;;;;OAYG;YACW,eAAe;IAoE7B;;OAEG;IACH,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,WAAW;IAuBnB,OAAO,CAAC,IAAI;IAQZ,OAAO,CAAC,IAAI;IAMZ,OAAO,CAAC,QAAQ;IAOhB;;;;;;;;;OASG;IACH,OAAO,CAAC,YAAY;CAmBrB;AAmOD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAkL3D"}
package/dist/server.js CHANGED
@@ -37,6 +37,8 @@ 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
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";
40
42
  /** Upper bound of the TCP user port range. Probing never wraps past this. */
41
43
  export const MAX_TCP_PORT = 65535;
42
44
  /** Smallest legal TCP user port (avoid binding to well-known 0..1023). */
@@ -408,6 +410,24 @@ export class TaskShowServer {
408
410
  this.handleChatAbort(sid, res);
409
411
  return;
410
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
+ }
411
431
  // All remaining routes are GET/HEAD only.
412
432
  if (method !== "GET" && method !== "HEAD") {
413
433
  this.text(res, 405, "Method Not Allowed");
@@ -639,6 +659,184 @@ export class TaskShowServer {
639
659
  this.chatManager.abort(sessionId);
640
660
  this.json(res, 200, { sessionId, aborted: true });
641
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
+ }
642
840
  handleFileContent(url, req, res) {
643
841
  const wantsHtml = prefersHtml(req);
644
842
  const raw = url.searchParams.get("path") ?? "";
@@ -1368,6 +1566,8 @@ export function renderTaskPage(session) {
1368
1566
  <pre class="json">${htmlEscape(JSON.stringify(session, null, 2))}</pre>
1369
1567
  </details>
1370
1568
  </section>
1569
+
1570
+ ${renderChatPanel({ scope: "task", taskId: session.taskId })}
1371
1571
  </div>
1372
1572
  </div>
1373
1573
 
@@ -1376,6 +1576,7 @@ export function renderTaskPage(session) {
1376
1576
  <script src="/static/task-operations.js"></script>
1377
1577
  <script src="/static/file-tree.js"></script>
1378
1578
  <script src="/static/tool-call-detail.js"></script>
1579
+ <script src="/static/chat-panel.js" defer></script>
1379
1580
  <script>
1380
1581
  if (window.mermaid) {
1381
1582
  window.mermaid.initialize({
@@ -1515,49 +1716,18 @@ function walkTreeFlat(node, depth, out, affectedSet) {
1515
1716
  walkTreeFlat(child, depth + 1, out, affectedSet);
1516
1717
  }
1517
1718
  }
1518
- /**
1519
- * v2.5.0+: render the home-page chat panel.
1520
- *
1521
- * Markup is intentionally minimal the heavy lifting (EventSource
1522
- * handling, message rendering, sessionId persistence) lives in
1523
- * `public/chat-panel.js`. The HTML below carries `data-*` hooks that
1524
- * the JS module targets; the contracts are pinned by
1525
- * `test/home-chat-v25.test.ts`.
1526
- *
1527
- * Disabled gracefully: when `chatEnabled` is false (the host didn't
1528
- * pass `chatOptions.enabled`) we render an empty section so SSR layout
1529
- * stays predictable, but the JS module short-circuits and the API
1530
- * endpoints return 503.
1531
- */
1532
- function renderChatPanel() {
1533
- return `
1534
- <section class="panel chat-panel" data-chat-shell aria-label="Home chat panel">
1535
- <header class="chat-panel-header">
1536
- <h2>💬 roy-agent chat</h2>
1537
- <p class="meta chat-panel-meta">
1538
- Multi-turn conversation powered by <code>roy-agent act -s &lt;sessionId&gt;</code>.
1539
- </p>
1540
- </header>
1541
- <div class="chat-messages" data-chat-messages role="log" aria-live="polite" aria-relevant="additions">
1542
- <p class="chat-empty">No messages yet — ask away!</p>
1543
- </div>
1544
- <form class="chat-form" data-chat-form autocomplete="off">
1545
- <label class="visually-hidden" for="chat-input">Message</label>
1546
- <input
1547
- id="chat-input"
1548
- type="text"
1549
- class="chat-input"
1550
- data-chat-input
1551
- placeholder="Type a message and press Enter…"
1552
- autocomplete="off"
1553
- />
1554
- <button type="submit" class="chat-send" data-chat-send aria-label="Send message">
1555
- Send
1556
- </button>
1557
- </form>
1558
- </section>
1559
- `;
1560
- }
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.
1561
1731
  function renderIndexPage(sessions) {
1562
1732
  // SSR hint — the tree container is always rendered; if /api/tasks/tree
1563
1733
  // returns 503 the JS shows a graceful empty state with a hint to
@@ -1672,6 +1842,16 @@ function renderIndexPage(sessions) {
1672
1842
  <script src="/static/app.js"></script>
1673
1843
  <script src="/static/mermaid-renderer.js"></script>
1674
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>
1675
1855
  <script src="/static/chat-panel.js" defer></script>
1676
1856
  </body>
1677
1857
  </html>`;