bazilion 0.14.2 → 0.16.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.
Files changed (122) hide show
  1. package/dist/cli.js +1752 -825
  2. package/dist/cli.js.map +1 -1
  3. package/dist/daemon.js +16624 -9803
  4. package/dist/daemon.js.map +1 -1
  5. package/dist/migrations/0001_init.sql +218 -3
  6. package/dist/web/client/assets/ChatPane-CQZzxOir.js +76 -0
  7. package/dist/web/client/assets/{ConfirmDialog-_bmG-wVG.js → ConfirmDialog-MicTTwdy.js} +1 -1
  8. package/dist/web/client/assets/{FieldRow-Do1cBkRc.js → FieldRow-DJ2Gx4Du.js} +1 -1
  9. package/dist/web/client/assets/{ProductionTeamPolicyEditor-XP-QJul4.js → ProductionTeamPolicyEditor-DqUnN_ce.js} +1 -1
  10. package/dist/web/client/assets/{ProfileCommunicationEditor-CwdFPdpk.js → ProfileCommunicationEditor-DArXub0b.js} +1 -1
  11. package/dist/web/client/assets/ResultCard-Cps_lXVN.js +1 -0
  12. package/dist/web/client/assets/ResultSourceConversation-CtzJ8QA-.js +1 -0
  13. package/dist/web/client/assets/TeamTabs-DbIk5vvT.js +1 -0
  14. package/dist/web/client/assets/{TemplatesTabs-D4WHvs6b.js → TemplatesTabs-CatJIjGi.js} +1 -1
  15. package/dist/web/client/assets/{UnsavedChangesGuard-B1aLqiK5.js → UnsavedChangesGuard-BCsLw-mU.js} +1 -1
  16. package/dist/web/client/assets/_id-B5Ov7cdO.js +1 -0
  17. package/dist/web/client/assets/{_id-BH3v_vI_.js → _id-BEDmIXVf.js} +1 -1
  18. package/dist/web/client/assets/_id-BFM_tn_s.js +1 -0
  19. package/dist/web/client/assets/_id-CrnEVcw8.js +1 -0
  20. package/dist/web/client/assets/{_id-3w4PFfma.js → _id-DxopIW-y.js} +1 -1
  21. package/dist/web/client/assets/{activity-A_HrG1Tv.js → activity-CrG8Bg60.js} +1 -1
  22. package/dist/web/client/assets/{agents-C8OVKq3N.js → agents-CMPOFcs-.js} +1 -1
  23. package/dist/web/client/assets/{agents-BeeFCt2c.js → agents-O4soKZND.js} +1 -1
  24. package/dist/web/client/assets/{approvals-BIdZ9BSn.js → approvals-CwDu_lrA.js} +1 -1
  25. package/dist/web/client/assets/{attention-B9bUrxiU.js → attention-BY8MMuM_.js} +1 -1
  26. package/dist/web/client/assets/chevron-right-Cf6vKeLq.js +1 -0
  27. package/dist/web/client/assets/{config-By86S7k-.js → config-Cn4iBJFz.js} +1 -1
  28. package/dist/web/client/assets/{inbox-DlXQCJLe.js → inbox-CWXql0K3.js} +1 -1
  29. package/dist/web/client/assets/{index-CLdmsmiN.js → index-Bq2zrG2_.js} +2 -2
  30. package/dist/web/client/assets/{learning-BqZIw2nA.js → learning-DAm4dyyc.js} +1 -1
  31. package/dist/web/client/assets/login-BsABI8Ux.js +1 -0
  32. package/dist/web/client/assets/{mcp-DKSY2yVa.js → mcp-loLB0wuh.js} +1 -1
  33. package/dist/web/client/assets/{members-9GuOiq6J.js → members-BJ0ikkTl.js} +1 -1
  34. package/dist/web/client/assets/{memory-DiGMI6FC.js → memory-Drn9Gv53.js} +1 -1
  35. package/dist/web/client/assets/{policy-TBGQffw5.js → policy-pQadZyk0.js} +1 -1
  36. package/dist/web/client/assets/results-9exU6f7m.js +1 -0
  37. package/dist/web/client/assets/routes-jw1PuqiR.js +1 -0
  38. package/dist/web/client/assets/{services-1Z398CES.js → services-BLso1BhA.js} +1 -1
  39. package/dist/web/client/assets/{skills-BP2gciFt.js → skills-BodqX3OI.js} +1 -1
  40. package/dist/web/client/assets/styles-d1yjBpud.css +2 -0
  41. package/dist/web/client/assets/{teams-DBIivGT7.js → teams-ByQedXZi.js} +1 -1
  42. package/dist/web/client/assets/{teams-tNoMDJ_M.js → teams-SYsF1bOR.js} +1 -1
  43. package/dist/web/client/assets/telegram-DC9PLFbB.js +1 -0
  44. package/dist/web/client/assets/{tokens-eDWCGQQ2.js → tokens-DGy8Ijpo.js} +1 -1
  45. package/dist/web/client/assets/{triggers-DZME3IcK.js → triggers-BXpVmHDG.js} +1 -1
  46. package/dist/web/client/assets/{welcome-B49sh_uf.js → welcome-BKsnsCxJ.js} +1 -1
  47. package/dist/web/client/assets/wire-constants-CBD-lTaa.js +1 -0
  48. package/dist/web/client/assets/{x-CHc7_-TH.js → x-BK4xNCzS.js} +1 -1
  49. package/dist/web/server/assets/ChatPane-CS5oI7ef.js +2666 -0
  50. package/dist/web/server/assets/ResultCard-B3zD9N0t.js +143 -0
  51. package/dist/web/server/assets/ResultSourceConversation-BpVWBRYq.js +64 -0
  52. package/dist/web/server/assets/{TeamTabs-Co3n2exj.js → TeamTabs-TCKjCvMA.js} +4 -0
  53. package/dist/web/server/assets/{_id-sNu5aXZt.js → _id-5pgKoid8.js} +2 -1
  54. package/dist/web/server/assets/_id-BDEpe6k0.js +6 -0
  55. package/dist/web/server/assets/{_id-DxCnEFpm.js → _id-BG2nkGxa.js} +3 -4
  56. package/dist/web/server/assets/{_id-BqWTB8Wl.js → _id-B_-CDHOi2.js} +1 -1
  57. package/dist/web/server/assets/{_id-BeKhPKw3.js → _id-BnqodC1c2.js} +7 -3
  58. package/dist/web/server/assets/{_id-DvlDkk04.js → _id-CeyHEsCZ.js} +1 -1
  59. package/dist/web/server/assets/{_id-BYKpT2Tu.js → _id-DFHB9JFQ.js} +1 -1
  60. package/dist/web/server/assets/{_id-DYU6uIBx.js → _id-DOnInzLb.js} +1 -1
  61. package/dist/web/server/assets/{_id-HMy7jM3P.js → _id-DrB9uR1b.js} +1 -1
  62. package/dist/web/server/assets/{_id-BAA9KJBU.js → _id-DtIYuJJX.js} +1 -1
  63. package/dist/web/server/assets/_id-DwZXhLlL.js +779 -0
  64. package/dist/web/server/assets/_id-wyjrf0JG.js +17 -0
  65. package/dist/web/server/assets/{_tanstack-start-manifest_v-Duhf0z7H.js → _tanstack-start-manifest_v-D7gKv6m6.js} +97 -70
  66. package/dist/web/server/assets/{activity-9VP6QDTH.js → activity-0VUkStOu.js} +1 -1
  67. package/dist/web/server/assets/{activity-BWv1goK6.js → activity-5fWKPDxj.js} +2 -2
  68. package/dist/web/server/assets/{activity-LjpblbDL.js → activity-C88qrNqv.js} +1 -1
  69. package/dist/web/server/assets/{agents-DjyQk94u.js → agents-C86K6HvS.js} +1 -1
  70. package/dist/web/server/assets/{agents-BJAv919x.js → agents-CSXLaM2W.js} +1 -1
  71. package/dist/web/server/assets/{approvals-BYi84gwl.js → approvals-C3MysI6l.js} +1 -1
  72. package/dist/web/server/assets/{approvals-7JOoBchn.js → approvals-DTdrltyk.js} +1 -1
  73. package/dist/web/server/assets/{approvals-Bz123liE.js → approvals-DXoN-T57.js} +15 -1
  74. package/dist/web/server/assets/{attention-UhqPK_Ro.js → attention-DLeLCKah.js} +1 -1
  75. package/dist/web/server/assets/{auth-7BHTH-KS.js → auth-CDZp9aEk.js} +1 -1
  76. package/dist/web/server/assets/{config-BMiajTZP.js → config-BVTHvljR.js} +1 -1
  77. package/dist/web/server/assets/{daemon-client-CI5NHwNC.js → daemon-client-xWyYKAcx.js} +59 -0
  78. package/dist/web/server/assets/{inbox-Dv55rGYq.js → inbox-PQTrjtOw.js} +1 -1
  79. package/dist/web/server/assets/{learning-C2upa1CE.js → learning-K0cmDsZR.js} +1 -1
  80. package/dist/web/server/assets/{login-BNj3U6ok.js → login-B_-9L8TU.js} +7 -2
  81. package/dist/web/server/assets/{login-Be8eP4Jp.js → login-rDox7PRD.js} +5 -2
  82. package/dist/web/server/assets/{mcp-BvnvlVFd.js → mcp-Rtraochg.js} +1 -1
  83. package/dist/web/server/assets/{members-DiYtr-zm.js → members-CD3GiTfF.js} +1 -1
  84. package/dist/web/server/assets/{members-DHYTVgwb.js → members-cc5SeEaW.js} +2 -2
  85. package/dist/web/server/assets/{members-BP3UTqu1.js → members-grAKFWgv.js} +1 -1
  86. package/dist/web/server/assets/{memory-CkXUgf5s.js → memory-C5i_GxlY.js} +2 -2
  87. package/dist/web/server/assets/{memory-DTFEmfF3.js → memory-CVqnyUaM.js} +1 -1
  88. package/dist/web/server/assets/{memory-BqV-Afy1.js → memory-egFpBD1g.js} +1 -1
  89. package/dist/web/server/assets/{policy-Bvvav8fh.js → policy-Dgjb0_gK.js} +2 -2
  90. package/dist/web/server/assets/{policy-BddK4xom.js → policy-bLKwvnja.js} +1 -1
  91. package/dist/web/server/assets/{policy-18fdTFO4.js → policy-ooSBgiDS.js} +1 -1
  92. package/dist/web/server/assets/results-CfNsFncJ.js +173 -0
  93. package/dist/web/server/assets/results-DXdYtHaq.js +12 -0
  94. package/dist/web/server/assets/results-N-kP4iFT.js +12 -0
  95. package/dist/web/server/assets/{router-CY_pCgP1.js → router-B6g_s7W3.js} +60 -43
  96. package/dist/web/server/assets/{routes-COtkyZG1.js → routes-BqSaZvlu.js} +3 -3
  97. package/dist/web/server/assets/{routes-BgHisAxH.js → routes-Dsh8F_7i.js} +2 -2
  98. package/dist/web/server/assets/{routes-DAU2g7Uz.js → routes-pcRy_Sf8.js} +1 -1
  99. package/dist/web/server/assets/{services-DozuJtzX.js → services-DlZA8fRL.js} +1 -1
  100. package/dist/web/server/assets/{skills-DfGSJTCB.js → skills-i2duvgW-.js} +1 -1
  101. package/dist/web/server/assets/{teams-be05ci17.js → teams-6RYIj1Xm.js} +1 -1
  102. package/dist/web/server/assets/{teams-DPnAX4wD.js → teams-B1AI3NfN.js} +1 -1
  103. package/dist/web/server/assets/{telegram-1AWL6lFw.js → telegram-C4q0YTVn.js} +1 -1
  104. package/dist/web/server/assets/{telegram-Dj4Wwhd6.js → telegram-CoNeOgqZ.js} +465 -1
  105. package/dist/web/server/assets/{telegram-DbOFpTFm.js → telegram-Zd1rIZxg.js} +1 -1
  106. package/dist/web/server/assets/{tokens-DjM8GsgS.js → tokens-RFquA2Bx.js} +1 -1
  107. package/dist/web/server/assets/{triggers-Nezfw-Bd.js → triggers-DVD-iE-N.js} +1 -1
  108. package/dist/web/server/server.js +31 -27
  109. package/dist/worker.js +1765 -710
  110. package/dist/worker.js.map +1 -1
  111. package/package.json +3 -3
  112. package/dist/web/client/assets/TeamTabs-DIRiKcqx.js +0 -1
  113. package/dist/web/client/assets/_id-Cx7o4Zmf.js +0 -1
  114. package/dist/web/client/assets/_id-DIiVVxBl.js +0 -1
  115. package/dist/web/client/assets/chevron-right-VWrk_dTX.js +0 -1
  116. package/dist/web/client/assets/login-CCMPXiny.js +0 -1
  117. package/dist/web/client/assets/routes-g3yUOhCb.js +0 -1
  118. package/dist/web/client/assets/styles-B5vcgXln.css +0 -2
  119. package/dist/web/client/assets/telegram-CJG_DFdI.js +0 -1
  120. package/dist/web/client/assets/wire-constants-zZ7Wjb9C.js +0 -74
  121. package/dist/web/server/assets/ChatPane-CgSe7tsy.js +0 -1429
  122. package/dist/web/server/assets/_id-ndxFHx0t.js +0 -381
@@ -0,0 +1,2666 @@
1
+ import { t as Button } from "./Button-Dsnw22OF.js";
2
+ import { t as ResultCard } from "./ResultCard-B3zD9N0t.js";
3
+ import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
+ import DOMPurify from "dompurify";
6
+ import { marked } from "marked";
7
+ //#region src/lib/coding-presentation.ts
8
+ /** Presentation only: excerpts are never used for authorization or receipt validity. */
9
+ function codingFailureSummary(diagnostic) {
10
+ const error = diagnostic.split("\n").map((line) => line.trim()).find((line) => /^(?:[A-Za-z]*Error(?: \[[A-Z_]+\])?:|ERR_[A-Z_]+\b)/.test(line));
11
+ if (!error) return null;
12
+ return error.length > 240 ? `${error.slice(0, 237)}…` : error;
13
+ }
14
+ function inboxUpdateLabel(body) {
15
+ const header = body.split("\n").find((line) => line.startsWith("--- from "));
16
+ if (!header) return "Teammate update received";
17
+ const match = /^--- from (.+?) \(message [0-9a-f-]+(?:, reply to [0-9a-f-]+)?\) ---$/.exec(header);
18
+ if (!match) return "Teammate update received";
19
+ const name = match[1]?.replace(/ \([0-9a-f-]{36}\)$/, "").trim();
20
+ if (!name || /^[0-9a-f-]{36}$/.test(name)) return "Teammate update received";
21
+ return `Update from ${name.slice(0, 100)}`;
22
+ }
23
+ //#endregion
24
+ //#region src/components/CodingToolResult.tsx
25
+ /** Presents only already-authorized chat text; never fetches private receipt records. */
26
+ function CodingToolResult({ name, body, pending = false }) {
27
+ if (!pending && body === "Coding context or command evidence retained privately. Captured transport output follows its communication approval; ask the Agent for the relevant result.") return /* @__PURE__ */ jsxs("details", {
28
+ className: "py-1 text-xs font-sans",
29
+ "aria-label": "Private command details",
30
+ children: [/* @__PURE__ */ jsx("summary", {
31
+ className: "cursor-pointer",
32
+ children: "Detailed output isn’t included in this history view"
33
+ }), /* @__PURE__ */ jsx("p", {
34
+ className: "mt-1",
35
+ children: "The Agent’s summary is shown in the conversation. Ask the Agent to summarize its retained command result if you need more detail."
36
+ })]
37
+ });
38
+ let parsed;
39
+ try {
40
+ parsed = JSON.parse(body);
41
+ } catch {
42
+ return /* @__PURE__ */ jsx("span", { children: body });
43
+ }
44
+ if (!parsed || typeof parsed !== "object") return /* @__PURE__ */ jsx("span", { children: body });
45
+ if (pending) {
46
+ const input = parsed;
47
+ return /* @__PURE__ */ jsxs("p", {
48
+ className: "py-1 break-words",
49
+ children: [
50
+ name === "coding_command" ? "Running" : "Inspecting",
51
+ ":",
52
+ " ",
53
+ typeof input.command === "string" ? input.command : typeof input.target === "string" ? input.target : "command evidence"
54
+ ]
55
+ });
56
+ }
57
+ if (name === "repository_context") {
58
+ const report = parsed;
59
+ if (!Array.isArray(report.instructions?.files)) return /* @__PURE__ */ jsx("span", { children: body });
60
+ return /* @__PURE__ */ jsxs("details", {
61
+ className: "py-1",
62
+ children: [
63
+ /* @__PURE__ */ jsxs("summary", {
64
+ className: "cursor-pointer",
65
+ children: [
66
+ "Repository: ",
67
+ report.target,
68
+ " ·",
69
+ " ",
70
+ report.instructions.state === "complete" ? `${report.instructions.files.length} instruction files` : "instructions unavailable"
71
+ ]
72
+ }),
73
+ report.instructions.files.map((file) => /* @__PURE__ */ jsx("p", { children: file.path }, file.path)),
74
+ /* @__PURE__ */ jsxs("p", { children: [
75
+ report.commands?.candidates?.length ?? 0,
76
+ " suggested commands ·",
77
+ " ",
78
+ report.git?.state === "available" ? report.git.branch : "Git unavailable"
79
+ ] }),
80
+ /* @__PURE__ */ jsx("pre", {
81
+ className: "max-h-64 overflow-auto whitespace-pre-wrap break-words",
82
+ children: body
83
+ })
84
+ ]
85
+ });
86
+ }
87
+ if (name === "coding_environment") {
88
+ const value = parsed;
89
+ if (typeof value.posture !== "string") return /* @__PURE__ */ jsx("span", { children: body });
90
+ return /* @__PURE__ */ jsxs("details", {
91
+ className: "py-1",
92
+ children: [
93
+ /* @__PURE__ */ jsxs("summary", {
94
+ className: "cursor-pointer",
95
+ children: [
96
+ "Environment: ",
97
+ value.posture,
98
+ " · ",
99
+ value.cwd
100
+ ]
101
+ }),
102
+ /* @__PURE__ */ jsx("p", {
103
+ className: "break-all",
104
+ children: value.imageId ?? "Host runtime"
105
+ }),
106
+ Array.isArray(value.restrictions) && value.restrictions.filter((x) => typeof x === "string").map((text) => /* @__PURE__ */ jsx("p", { children: text }, text))
107
+ ]
108
+ });
109
+ }
110
+ const view = parsed;
111
+ const receipt = name === "coding_receipt" ? view.receipt : parsed;
112
+ if (!receipt || typeof receipt.state !== "string" || typeof receipt.input?.command !== "string" || typeof receipt.input?.cwd !== "string") return /* @__PURE__ */ jsx("span", { children: body });
113
+ const failure = receipt.state === "failed" && typeof receipt.diagnostic === "string" ? codingFailureSummary(receipt.diagnostic) : null;
114
+ return /* @__PURE__ */ jsxs("div", {
115
+ className: "py-2",
116
+ "aria-label": "Coding command result",
117
+ children: [
118
+ /* @__PURE__ */ jsxs("p", {
119
+ className: "font-sans",
120
+ children: [
121
+ /* @__PURE__ */ jsx("strong", {
122
+ className: receipt.state === "succeeded" ? "text-sapphire-deep" : "text-mocha",
123
+ children: receipt.state.replaceAll("_", " ")
124
+ }),
125
+ " ",
126
+ "· ",
127
+ receipt.input.purpose,
128
+ typeof receipt.exitCode === "number" ? ` · exit ${receipt.exitCode}` : "",
129
+ name === "coding_receipt" ? ` · ${view.applicability}` : ""
130
+ ]
131
+ }),
132
+ /* @__PURE__ */ jsx("p", {
133
+ className: "whitespace-pre-wrap break-words",
134
+ children: receipt.input.command
135
+ }),
136
+ /* @__PURE__ */ jsxs("p", {
137
+ className: "text-xs",
138
+ children: [
139
+ receipt.input.cwd,
140
+ " · ",
141
+ receipt.environment?.posture,
142
+ " · result at execution time"
143
+ ]
144
+ }),
145
+ failure && /* @__PURE__ */ jsx("p", {
146
+ className: "mt-1 font-sans text-rose-baziu",
147
+ "aria-label": "Command failure summary",
148
+ children: failure
149
+ }),
150
+ typeof receipt.reason === "string" && /* @__PURE__ */ jsx("p", { children: receipt.reason }),
151
+ /* @__PURE__ */ jsxs("details", { children: [
152
+ /* @__PURE__ */ jsx("summary", {
153
+ className: "cursor-pointer",
154
+ children: "Output and evidence"
155
+ }),
156
+ /* @__PURE__ */ jsx("pre", {
157
+ className: "max-h-64 overflow-auto whitespace-pre-wrap break-words",
158
+ children: receipt.diagnostic
159
+ }),
160
+ receipt.truncated && /* @__PURE__ */ jsx("p", { children: "Output truncated." }),
161
+ /* @__PURE__ */ jsxs("p", {
162
+ className: "break-all",
163
+ children: ["coding-receipt:", receipt.id]
164
+ }),
165
+ receipt.environment?.imageId && /* @__PURE__ */ jsx("p", {
166
+ className: "break-all",
167
+ children: receipt.environment.imageId
168
+ })
169
+ ] })
170
+ ]
171
+ });
172
+ }
173
+ //#endregion
174
+ //#region src/lib/md.ts
175
+ var installed = false;
176
+ function install() {
177
+ if (installed) return;
178
+ marked.setOptions({
179
+ breaks: true,
180
+ gfm: true
181
+ });
182
+ DOMPurify.addHook("afterSanitizeAttributes", (node) => {
183
+ if (node.nodeName === "A") {
184
+ node.setAttribute("target", "_blank");
185
+ node.setAttribute("rel", "noopener noreferrer");
186
+ }
187
+ });
188
+ installed = true;
189
+ }
190
+ function renderMd(text, domReady) {
191
+ if (!text) return "";
192
+ if (!domReady || typeof window === "undefined") return escapeHtml(text);
193
+ try {
194
+ install();
195
+ const html = marked.parse(text, { async: false });
196
+ return DOMPurify.sanitize(html);
197
+ } catch {
198
+ return escapeHtml(text);
199
+ }
200
+ }
201
+ function escapeHtml(s) {
202
+ return String(s).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
203
+ }
204
+ //#endregion
205
+ //#region src/components/ConversationLibrary.tsx
206
+ function ConversationLibrary({ agentId, turnBusy, onCreated, renderHistory, initialConversationId }) {
207
+ const [library, setLibrary] = useState(null);
208
+ const [offset, setOffset] = useState(0);
209
+ const [refresh, setRefresh] = useState(0);
210
+ const [selected, setSelected] = useState(null);
211
+ const [history, setHistory] = useState(null);
212
+ const [title, setTitle] = useState("");
213
+ const [error, setError] = useState("");
214
+ const [historyError, setHistoryError] = useState("");
215
+ const [busy, setBusy] = useState(false);
216
+ const pendingKey = `bazilion:conversation-create:${agentId}`;
217
+ const pendingCreate = useRef(null);
218
+ useEffect(() => {
219
+ try {
220
+ pendingCreate.current = JSON.parse(sessionStorage.getItem(pendingKey) ?? "null");
221
+ } catch {
222
+ pendingCreate.current = null;
223
+ }
224
+ }, [pendingKey]);
225
+ function clearPending() {
226
+ pendingCreate.current = null;
227
+ sessionStorage.removeItem(pendingKey);
228
+ }
229
+ const base = `/api/agents/${encodeURIComponent(agentId)}/conversations`;
230
+ useEffect(() => {
231
+ if (!initialConversationId) return;
232
+ const controller = new AbortController();
233
+ fetch(`${base}/${encodeURIComponent(initialConversationId)}`, { signal: controller.signal }).then(async (response) => {
234
+ if (!response.ok) throw new Error("Retained conversation is unavailable");
235
+ const body = await response.json();
236
+ if (!controller.signal.aborted) {
237
+ setSelected(body.conversation);
238
+ setTitle(body.conversation.title);
239
+ }
240
+ }).catch((error) => {
241
+ if (!controller.signal.aborted) setError(error.message);
242
+ });
243
+ return () => controller.abort();
244
+ }, [base, initialConversationId]);
245
+ useEffect(() => {
246
+ const controller = new AbortController();
247
+ setLibrary(null);
248
+ fetch(`${base}?limit=20&offset=${offset}`, { signal: controller.signal }).then(async (response) => {
249
+ if (!response.ok) throw new Error("Could not load conversations.");
250
+ const body = await response.json();
251
+ if (!controller.signal.aborted) setLibrary(body);
252
+ }).catch((error) => {
253
+ if (!controller.signal.aborted) setError(error.message);
254
+ });
255
+ return () => controller.abort();
256
+ }, [
257
+ base,
258
+ offset,
259
+ refresh
260
+ ]);
261
+ useEffect(() => {
262
+ if (!selected) return;
263
+ const controller = new AbortController();
264
+ setHistory(null);
265
+ setHistoryError("");
266
+ fetch(`${base}/${encodeURIComponent(selected.id)}`, { signal: controller.signal }).then(async (response) => {
267
+ if (!response.ok) throw new Error("This conversation history is unavailable. You can still start a new conversation.");
268
+ const body = await response.json();
269
+ if (!controller.signal.aborted) setHistory(body.messages);
270
+ }).catch((error) => {
271
+ if (!controller.signal.aborted) setHistoryError(error.message);
272
+ });
273
+ return () => controller.abort();
274
+ }, [base, selected?.id]);
275
+ async function create() {
276
+ if (!library || busy || turnBusy) return;
277
+ setBusy(true);
278
+ setError("");
279
+ const request = pendingCreate.current ?? {
280
+ requestId: crypto.randomUUID(),
281
+ expectedSelection: library.selection
282
+ };
283
+ pendingCreate.current = request;
284
+ try {
285
+ sessionStorage.setItem(pendingKey, JSON.stringify(request));
286
+ const response = await fetch(base, {
287
+ method: "POST",
288
+ headers: { "content-type": "application/json" },
289
+ body: JSON.stringify(request)
290
+ });
291
+ const body = await response.json();
292
+ if (!response.ok) {
293
+ if (response.status === 409 || response.status === 400) clearPending();
294
+ throw new Error(body.error ?? "Could not create conversation.");
295
+ }
296
+ clearPending();
297
+ setSelected(null);
298
+ setOffset(0);
299
+ setRefresh((value) => value + 1);
300
+ await onCreated();
301
+ } catch (error) {
302
+ setError(error instanceof Error ? error.message : "Could not create conversation.");
303
+ } finally {
304
+ setBusy(false);
305
+ }
306
+ }
307
+ async function rename() {
308
+ if (!selected || busy) return;
309
+ setBusy(true);
310
+ setError("");
311
+ try {
312
+ const response = await fetch(`${base}/${encodeURIComponent(selected.id)}`, {
313
+ method: "PATCH",
314
+ headers: { "content-type": "application/json" },
315
+ body: JSON.stringify({
316
+ title,
317
+ expectedTitleRevision: selected.titleRevision
318
+ })
319
+ });
320
+ const body = await response.json();
321
+ if (!response.ok) throw new Error(body.error ?? "Could not rename conversation.");
322
+ setSelected(body.conversation);
323
+ setRefresh((value) => value + 1);
324
+ } catch (error) {
325
+ setError(error instanceof Error ? error.message : "Could not rename conversation.");
326
+ } finally {
327
+ setBusy(false);
328
+ }
329
+ }
330
+ return /* @__PURE__ */ jsxs("section", {
331
+ "aria-live": "off",
332
+ className: "my-3 min-w-0 space-y-4 rounded-lg border border-fawn bg-ivory p-4",
333
+ "aria-label": "Conversation library",
334
+ children: [
335
+ /* @__PURE__ */ jsxs("div", {
336
+ className: "flex flex-col items-start gap-3 sm:flex-row sm:items-center sm:justify-between",
337
+ children: [/* @__PURE__ */ jsx("h2", {
338
+ className: "m-0 text-lg",
339
+ children: "Conversations"
340
+ }), /* @__PURE__ */ jsx(Button, {
341
+ variant: "primary",
342
+ disabled: !library || busy || turnBusy,
343
+ onClick: () => void create(),
344
+ children: "New conversation"
345
+ })]
346
+ }),
347
+ /* @__PURE__ */ jsx("p", {
348
+ className: "m-0 text-sm",
349
+ children: "Starting a new conversation keeps previous history and saved files. Viewing history does not change the active conversation."
350
+ }),
351
+ turnBusy && /* @__PURE__ */ jsx("p", {
352
+ role: "status",
353
+ children: "Wait for the current turn to finish before starting a new conversation."
354
+ }),
355
+ error && /* @__PURE__ */ jsxs("p", {
356
+ role: "alert",
357
+ children: [
358
+ error,
359
+ " ",
360
+ /* @__PURE__ */ jsx(Button, {
361
+ variant: "ghost",
362
+ disabled: busy,
363
+ onClick: () => {
364
+ setError("");
365
+ setRefresh((value) => value + 1);
366
+ },
367
+ children: "Refresh library"
368
+ })
369
+ ]
370
+ }),
371
+ !library ? /* @__PURE__ */ jsx("p", {
372
+ role: "status",
373
+ children: "Loading conversations…"
374
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
375
+ !library.total && /* @__PURE__ */ jsx("p", { children: "No conversations yet." }),
376
+ /* @__PURE__ */ jsx("ul", {
377
+ className: "mt-4 grid list-none gap-2 p-0",
378
+ children: library.conversations.map((item) => /* @__PURE__ */ jsxs("li", {
379
+ className: "flex min-w-0 flex-col items-start gap-3 rounded border border-fawn p-3 sm:flex-row sm:items-center sm:justify-between",
380
+ children: [/* @__PURE__ */ jsxs("span", {
381
+ className: "flex min-w-0 flex-1 flex-wrap items-center gap-2",
382
+ children: [/* @__PURE__ */ jsx("span", {
383
+ className: "min-w-0 break-words [overflow-wrap:anywhere]",
384
+ children: item.title
385
+ }), item.id === library.selection.conversationId && /* @__PURE__ */ jsx("strong", {
386
+ className: "shrink-0 rounded-full bg-frost px-2 py-0.5 text-xs",
387
+ children: "Active"
388
+ })]
389
+ }), /* @__PURE__ */ jsxs(Button, {
390
+ variant: "ghost",
391
+ className: "max-w-full whitespace-normal break-words text-left sm:max-w-[50%]",
392
+ disabled: busy,
393
+ "aria-pressed": selected?.id === item.id,
394
+ onClick: () => {
395
+ setSelected(item);
396
+ setTitle(item.title);
397
+ },
398
+ children: ["View ", item.title]
399
+ })]
400
+ }, item.id))
401
+ }),
402
+ /* @__PURE__ */ jsxs("div", {
403
+ className: "mt-4 flex flex-wrap items-center gap-2",
404
+ children: [
405
+ /* @__PURE__ */ jsx(Button, {
406
+ variant: "ghost",
407
+ disabled: busy || offset === 0,
408
+ onClick: () => setOffset(Math.max(0, offset - 20)),
409
+ children: "Previous"
410
+ }),
411
+ /* @__PURE__ */ jsx(Button, {
412
+ variant: "ghost",
413
+ disabled: busy || offset + library.conversations.length >= library.total,
414
+ onClick: () => setOffset(offset + 20),
415
+ children: "Next"
416
+ }),
417
+ /* @__PURE__ */ jsxs("span", {
418
+ className: "text-sm",
419
+ children: [library.total, " conversations"]
420
+ })
421
+ ]
422
+ })
423
+ ] }),
424
+ selected && /* @__PURE__ */ jsxs("section", {
425
+ className: "space-y-4 border-t border-fawn pt-4",
426
+ "aria-label": "Retained conversation",
427
+ children: [
428
+ /* @__PURE__ */ jsxs("div", {
429
+ className: "grid min-w-0 grid-cols-2 items-end gap-2 sm:grid-cols-[minmax(0,1fr)_auto_auto]",
430
+ children: [
431
+ /* @__PURE__ */ jsxs("label", {
432
+ className: "col-span-2 m-0 grid min-w-0 gap-2 sm:col-span-1",
433
+ children: ["Conversation title", /* @__PURE__ */ jsx("input", {
434
+ className: "h-11",
435
+ value: title,
436
+ maxLength: 200,
437
+ onChange: (event) => setTitle(event.target.value)
438
+ })]
439
+ }),
440
+ /* @__PURE__ */ jsx(Button, {
441
+ variant: "ghost",
442
+ className: "h-11",
443
+ disabled: busy || !title.trim(),
444
+ onClick: () => void rename(),
445
+ children: "Rename"
446
+ }),
447
+ /* @__PURE__ */ jsx(Button, {
448
+ variant: "ghost",
449
+ className: "h-11",
450
+ disabled: busy,
451
+ onClick: () => setSelected(null),
452
+ children: "Close history"
453
+ })
454
+ ]
455
+ }),
456
+ /* @__PURE__ */ jsx("p", {
457
+ className: "m-0 text-sm",
458
+ children: "Viewing retained history. New messages still go to the active conversation."
459
+ }),
460
+ historyError ? /* @__PURE__ */ jsx("p", {
461
+ role: "alert",
462
+ children: historyError
463
+ }) : history === null ? /* @__PURE__ */ jsx("p", {
464
+ role: "status",
465
+ children: "Loading history…"
466
+ }) : history.length ? renderHistory(history) : /* @__PURE__ */ jsx("p", { children: "This conversation is empty." })
467
+ ]
468
+ })
469
+ ]
470
+ });
471
+ }
472
+ //#endregion
473
+ //#region src/lib/queue-journal.ts
474
+ /** Store bytes before submission so a reload can retry the exact same attempt. */
475
+ async function queueJournal(agentId, value, clearRequestId) {
476
+ return new Promise((resolve, reject) => {
477
+ const opening = indexedDB.open("bazilion-queue-attempts", 1);
478
+ opening.onupgradeneeded = () => {
479
+ opening.result.createObjectStore("requests", { keyPath: "input.requestId" }).createIndex("agentId", "agentId");
480
+ };
481
+ opening.onerror = () => reject(/* @__PURE__ */ new Error("Local queue recovery storage is unavailable"));
482
+ opening.onblocked = () => reject(/* @__PURE__ */ new Error("Close other Bazilion tabs to open queue recovery storage"));
483
+ opening.onsuccess = () => {
484
+ const db = opening.result;
485
+ const tx = db.transaction("requests", value === void 0 ? "readonly" : "readwrite");
486
+ const store = tx.objectStore("requests");
487
+ if (value === null && !clearRequestId) {
488
+ db.close();
489
+ reject(/* @__PURE__ */ new Error("Queue request identity is required to clear recovery state"));
490
+ return;
491
+ }
492
+ const request = value === void 0 ? store.index("agentId").get(agentId) : value === null ? store.delete(clearRequestId) : store.put(value);
493
+ tx.oncomplete = () => {
494
+ db.close();
495
+ resolve(value === void 0 ? request.result ?? null : value);
496
+ };
497
+ tx.onerror = tx.onabort = () => {
498
+ db.close();
499
+ reject(/* @__PURE__ */ new Error("Unable to retain queue request locally; it was not acknowledged"));
500
+ };
501
+ };
502
+ });
503
+ }
504
+ //#endregion
505
+ //#region src/components/UserQueuePanel.tsx
506
+ var UserQueuePanel = forwardRef(function UserQueuePanel({ agentId, selection, onQueueMode, onViewConversation }, ref) {
507
+ const base = `/api/agents/${encodeURIComponent(agentId)}/queue`;
508
+ const [state, setState] = useState(null);
509
+ const [error, setError] = useState("");
510
+ const [notice, setNotice] = useState("");
511
+ const [busy, setBusy] = useState(false);
512
+ const [ready, setReady] = useState(false);
513
+ const readyRef = useRef(false);
514
+ const [pending, setPending] = useState(null);
515
+ const pendingRef = useRef(null);
516
+ const locked = useRef(false);
517
+ const mounted = useRef(true);
518
+ const refreshGeneration = useRef(0);
519
+ useEffect(() => {
520
+ mounted.current = true;
521
+ return () => {
522
+ mounted.current = false;
523
+ };
524
+ }, []);
525
+ const [editing, setEditing] = useState(null);
526
+ const [text, setText] = useState("");
527
+ const [editFiles, setEditFiles] = useState([]);
528
+ const [history, setHistory] = useState(false);
529
+ const [offset, setOffset] = useState(0);
530
+ const modeRef = useRef(onQueueMode);
531
+ modeRef.current = onQueueMode;
532
+ async function refresh() {
533
+ const generation = ++refreshGeneration.current;
534
+ const response = await fetch(`${base}?all=${history ? 1 : 0}&offset=${offset}`);
535
+ if (!response.ok) throw new Error("Queue could not be loaded");
536
+ const body = await response.json();
537
+ const activeResponse = history ? await fetch(base) : null;
538
+ const active = activeResponse?.ok ? await activeResponse.json() : body;
539
+ if (mounted.current && generation === refreshGeneration.current) {
540
+ setState(body);
541
+ if (readyRef.current) modeRef.current(active.total > 0 || active.control.paused || !!pendingRef.current);
542
+ }
543
+ return body;
544
+ }
545
+ useEffect(() => {
546
+ let live = true;
547
+ queueJournal(agentId).then((value) => {
548
+ if (live) {
549
+ pendingRef.current = value;
550
+ setPending(value);
551
+ readyRef.current = true;
552
+ setReady(true);
553
+ refresh().catch((e) => setError(e.message));
554
+ }
555
+ }).catch((e) => {
556
+ if (live) setError(e.message);
557
+ });
558
+ return () => {
559
+ live = false;
560
+ };
561
+ }, [agentId]);
562
+ useEffect(() => {
563
+ refresh().catch((e) => setError(e.message));
564
+ const timer = setInterval(() => {
565
+ refresh().catch((e) => setError(e.message));
566
+ }, 2e3);
567
+ return () => clearInterval(timer);
568
+ }, [
569
+ agentId,
570
+ history,
571
+ offset
572
+ ]);
573
+ async function clearPending(record) {
574
+ await queueJournal(agentId, null, record.input.requestId);
575
+ const remaining = await queueJournal(agentId);
576
+ pendingRef.current = remaining;
577
+ setPending(remaining);
578
+ }
579
+ async function submit(record, retry = false) {
580
+ if (locked.current || !ready) return false;
581
+ if (pendingRef.current && !retry) {
582
+ setError("Resolve the unacknowledged queue request before sending another.");
583
+ return false;
584
+ }
585
+ locked.current = true;
586
+ setBusy(true);
587
+ setError("");
588
+ setNotice("");
589
+ try {
590
+ await queueJournal(agentId, record);
591
+ pendingRef.current = record;
592
+ setPending(record);
593
+ const response = await fetch(record.replacementId ? `${base}/${encodeURIComponent(record.replacementId)}` : base, {
594
+ method: record.replacementId ? "PATCH" : "POST",
595
+ headers: { "content-type": "application/json" },
596
+ body: JSON.stringify(record.input)
597
+ });
598
+ if (!response.ok) {
599
+ const detail = await response.json().catch(() => ({}));
600
+ if (response.status >= 400 && response.status < 500) await clearPending(record);
601
+ throw new Error(detail.error ?? "Queue acknowledgement unavailable; retry this same request.");
602
+ }
603
+ const item = await response.json();
604
+ await clearPending(record);
605
+ setNotice(`Follow-up ${item.status}.`);
606
+ setEditing(null);
607
+ await refresh().catch(() => setError("Input acknowledged; queue display could not refresh."));
608
+ return true;
609
+ } catch (e) {
610
+ setError(e instanceof Error ? e.message : "Queue request failed");
611
+ return false;
612
+ } finally {
613
+ locked.current = false;
614
+ setBusy(false);
615
+ }
616
+ }
617
+ async function action(path, body, method = "POST") {
618
+ if (locked.current) return false;
619
+ locked.current = true;
620
+ setBusy(true);
621
+ setError("");
622
+ try {
623
+ const response = await fetch(`${base}${path}`, {
624
+ method,
625
+ headers: { "content-type": "application/json" },
626
+ body: JSON.stringify(body)
627
+ });
628
+ if (!response.ok) {
629
+ const detail = await response.json();
630
+ throw new Error(detail.error ?? "Queue changed; refresh and try again");
631
+ }
632
+ await refresh();
633
+ return true;
634
+ } catch (e) {
635
+ setError(e instanceof Error ? e.message : "Queue action failed");
636
+ await refresh().catch(() => {});
637
+ return false;
638
+ } finally {
639
+ locked.current = false;
640
+ setBusy(false);
641
+ }
642
+ }
643
+ useImperativeHandle(ref, () => ({
644
+ async enqueue(message, attachments, expectedSelection) {
645
+ if (!expectedSelection) {
646
+ setError("Refresh conversation history before queuing input.");
647
+ return false;
648
+ }
649
+ return submit({
650
+ agentId,
651
+ input: {
652
+ requestId: crypto.randomUUID(),
653
+ message,
654
+ attachments,
655
+ expectedSelection
656
+ }
657
+ });
658
+ },
659
+ async stop() {
660
+ if (!state) {
661
+ setError("Load queue controls before stopping.");
662
+ return false;
663
+ }
664
+ return action("/stop", { expectedRevision: state.control.revision });
665
+ }
666
+ }));
667
+ async function edit(item) {
668
+ try {
669
+ const response = await fetch(`${base}/${encodeURIComponent(item.id)}/input`);
670
+ if (!response.ok) throw new Error("Retained input is unavailable");
671
+ const input = await response.json();
672
+ setEditing(item);
673
+ setText(input.message);
674
+ setEditFiles(input.attachments);
675
+ } catch (e) {
676
+ setError(e instanceof Error ? e.message : "Could not edit");
677
+ }
678
+ }
679
+ return /* @__PURE__ */ jsxs("section", {
680
+ "aria-label": "Follow-up queue",
681
+ className: "max-h-[45%] min-w-0 shrink-0 space-y-3 overflow-y-auto border-t border-frost px-5 py-4 text-sm",
682
+ children: [
683
+ /* @__PURE__ */ jsxs("div", {
684
+ className: "flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",
685
+ children: [/* @__PURE__ */ jsxs("strong", {
686
+ className: "shrink-0",
687
+ children: ["Follow-ups ", state?.control.paused ? "· Paused" : ""]
688
+ }), /* @__PURE__ */ jsxs("div", {
689
+ className: "flex flex-wrap items-center gap-2",
690
+ children: [
691
+ state && /* @__PURE__ */ jsx(Button, {
692
+ variant: "ghost",
693
+ disabled: busy,
694
+ onClick: () => void action("/control", {
695
+ paused: !state.control.paused,
696
+ expectedRevision: state.control.revision
697
+ }),
698
+ children: state.control.paused ? "Resume queue" : "Pause queue"
699
+ }),
700
+ state && /* @__PURE__ */ jsx(Button, {
701
+ variant: "danger",
702
+ disabled: busy,
703
+ onClick: () => void action("/stop", { expectedRevision: state.control.revision }),
704
+ children: "Stop and pause"
705
+ }),
706
+ /* @__PURE__ */ jsx(Button, {
707
+ variant: "ghost",
708
+ onClick: () => {
709
+ setHistory(!history);
710
+ setOffset(0);
711
+ },
712
+ children: history ? "Pending" : "History"
713
+ })
714
+ ]
715
+ })]
716
+ }),
717
+ error && /* @__PURE__ */ jsx("p", {
718
+ role: "alert",
719
+ className: "m-0 break-words text-danger",
720
+ children: error
721
+ }),
722
+ notice && /* @__PURE__ */ jsx("p", {
723
+ role: "status",
724
+ className: "m-0 text-mocha",
725
+ children: notice
726
+ }),
727
+ pending && /* @__PURE__ */ jsxs("div", {
728
+ role: "status",
729
+ className: "flex min-w-0 flex-col gap-3 rounded-lg border border-frost bg-ivory p-3",
730
+ children: [
731
+ /* @__PURE__ */ jsx("p", { children: "A queue request has no confirmed acknowledgement. Retrying uses the same request and files." }),
732
+ /* @__PURE__ */ jsx("p", {
733
+ className: "truncate",
734
+ children: pending.input.message
735
+ }),
736
+ /* @__PURE__ */ jsx(Button, {
737
+ variant: "primary",
738
+ disabled: busy,
739
+ onClick: () => void submit(pending, true),
740
+ children: "Retry saved request"
741
+ })
742
+ ]
743
+ }),
744
+ /* @__PURE__ */ jsxs("div", {
745
+ className: "min-w-0 space-y-3",
746
+ children: [state?.items.map((item) => /* @__PURE__ */ jsxs("article", {
747
+ className: "flex min-w-0 flex-col gap-3 rounded-lg border border-frost bg-ivory p-3",
748
+ children: [
749
+ /* @__PURE__ */ jsxs("div", {
750
+ className: "flex flex-wrap items-center justify-between gap-3",
751
+ children: [/* @__PURE__ */ jsxs("div", {
752
+ className: "flex flex-wrap items-center gap-2",
753
+ children: [/* @__PURE__ */ jsx("strong", {
754
+ className: "rounded-full bg-frost px-2 py-1 text-xs capitalize",
755
+ children: item.status
756
+ }), /* @__PURE__ */ jsx("span", {
757
+ className: "text-xs text-mocha",
758
+ children: item.source === "telegram" ? "Telegram" : "Web / API"
759
+ })]
760
+ }), /* @__PURE__ */ jsx(Button, {
761
+ variant: "ghost",
762
+ onClick: () => onViewConversation(item.conversationId),
763
+ children: "View conversation"
764
+ })]
765
+ }),
766
+ /* @__PURE__ */ jsx("p", {
767
+ className: "m-0 whitespace-pre-wrap break-words [overflow-wrap:anywhere]",
768
+ children: item.text ?? "Input retention expired"
769
+ }),
770
+ item.attachments.map((file) => /* @__PURE__ */ jsxs("p", {
771
+ className: "m-0 break-all text-xs text-mocha",
772
+ children: [
773
+ file.name ?? "Attachment",
774
+ " · ",
775
+ file.byteLength,
776
+ " bytes"
777
+ ]
778
+ }, file.id)),
779
+ item.diagnostic && /* @__PURE__ */ jsx("p", {
780
+ className: "m-0 break-words",
781
+ children: item.diagnostic
782
+ }),
783
+ item.approvalId && /* @__PURE__ */ jsx("a", {
784
+ href: `/approvals`,
785
+ children: "Review communication approval"
786
+ }),
787
+ item.status === "pending" && /* @__PURE__ */ jsxs("div", {
788
+ className: "flex flex-wrap items-center gap-2",
789
+ children: [/* @__PURE__ */ jsx(Button, {
790
+ variant: "ghost",
791
+ disabled: busy || !!pending,
792
+ onClick: () => void edit(item),
793
+ children: "Edit"
794
+ }), /* @__PURE__ */ jsx(Button, {
795
+ variant: "danger",
796
+ disabled: busy,
797
+ onClick: () => void action(`/${item.id}`, { expectedRevision: item.revision }, "DELETE"),
798
+ children: "Remove"
799
+ })]
800
+ }),
801
+ item.status === "uncertain" && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", { children: "This input may already have acted. Review its conversation before closing it. Closing never retries it." }), /* @__PURE__ */ jsx(Button, {
802
+ variant: "danger",
803
+ disabled: busy,
804
+ onClick: () => void action(`/${item.id}/reconcile`, {
805
+ expectedRevision: item.revision,
806
+ acknowledged: true
807
+ }),
808
+ children: "Acknowledge and close"
809
+ })] })
810
+ ]
811
+ }, item.id)), editing && /* @__PURE__ */ jsxs("form", {
812
+ className: "flex min-w-0 flex-col gap-3 rounded-lg border border-frost bg-ivory p-3",
813
+ onSubmit: (e) => {
814
+ e.preventDefault();
815
+ const expectedSelection = selection();
816
+ if (expectedSelection) submit({
817
+ agentId,
818
+ replacementId: editing.id,
819
+ input: {
820
+ requestId: crypto.randomUUID(),
821
+ expectedRevision: editing.revision,
822
+ expectedSelection,
823
+ message: text,
824
+ attachments: editFiles
825
+ }
826
+ });
827
+ },
828
+ children: [
829
+ /* @__PURE__ */ jsxs("label", {
830
+ className: "m-0 grid gap-2",
831
+ children: ["Edit queued input", /* @__PURE__ */ jsx("textarea", {
832
+ "aria-label": "Edit queued input",
833
+ rows: 3,
834
+ className: "w-full resize-y",
835
+ value: text,
836
+ onChange: (e) => setText(e.target.value)
837
+ })]
838
+ }),
839
+ editFiles.map((file, index) => /* @__PURE__ */ jsxs("div", { children: [
840
+ file.name ?? "Attachment",
841
+ " ",
842
+ /* @__PURE__ */ jsx(Button, {
843
+ variant: "ghost",
844
+ onClick: () => setEditFiles((files) => files.filter((_, i) => i !== index)),
845
+ children: "Remove attachment"
846
+ })
847
+ ] }, index)),
848
+ /* @__PURE__ */ jsxs("div", {
849
+ className: "flex flex-wrap items-center gap-2",
850
+ children: [/* @__PURE__ */ jsx(Button, {
851
+ variant: "primary",
852
+ type: "submit",
853
+ disabled: busy || !!pending,
854
+ children: "Save replacement"
855
+ }), /* @__PURE__ */ jsx(Button, {
856
+ variant: "ghost",
857
+ onClick: () => setEditing(null),
858
+ children: "Cancel edit"
859
+ })]
860
+ })
861
+ ]
862
+ })]
863
+ }),
864
+ state && (offset > 0 || offset + state.items.length < state.total) && /* @__PURE__ */ jsxs("div", {
865
+ className: "flex flex-wrap items-center gap-2",
866
+ children: [/* @__PURE__ */ jsx(Button, {
867
+ variant: "ghost",
868
+ disabled: offset === 0,
869
+ onClick: () => setOffset(Math.max(0, offset - 20)),
870
+ children: "Previous"
871
+ }), /* @__PURE__ */ jsx(Button, {
872
+ variant: "ghost",
873
+ disabled: offset + state.items.length >= state.total,
874
+ onClick: () => setOffset(offset + 20),
875
+ children: "Next"
876
+ })]
877
+ })
878
+ ]
879
+ });
880
+ });
881
+ //#endregion
882
+ //#region src/components/AgentQuestions.tsx
883
+ function AgentQuestions({ agentId }) {
884
+ const [items, setItems] = useState([]);
885
+ const [error, setError] = useState("");
886
+ useEffect(() => {
887
+ let live = true;
888
+ let running = false;
889
+ const refresh = async () => {
890
+ if (running) return;
891
+ running = true;
892
+ try {
893
+ const response = await fetch(`/api/agents/${encodeURIComponent(agentId)}/questions`, { cache: "no-store" });
894
+ if (!response.ok) throw new Error("Questions could not be refreshed");
895
+ const body = await response.json();
896
+ if (live) {
897
+ setItems(body.questions);
898
+ setError("");
899
+ }
900
+ } catch (e) {
901
+ if (live) {
902
+ setItems([]);
903
+ setError(e instanceof Error ? e.message : "Questions unavailable");
904
+ }
905
+ } finally {
906
+ running = false;
907
+ }
908
+ };
909
+ refresh();
910
+ const timer = setInterval(() => {
911
+ refresh();
912
+ }, 2e3);
913
+ return () => {
914
+ live = false;
915
+ clearInterval(timer);
916
+ };
917
+ }, [agentId]);
918
+ if (!items.length && !error) return null;
919
+ return /* @__PURE__ */ jsxs("section", {
920
+ "aria-label": "Agent questions",
921
+ className: "max-h-[40vh] min-w-0 shrink-0 space-y-3 overflow-y-auto border-t border-frost px-5 py-3",
922
+ children: [
923
+ error && /* @__PURE__ */ jsx("p", {
924
+ role: "status",
925
+ children: error
926
+ }),
927
+ items.filter((item) => item.status === "pending" || item.continuation === "unconfirmed").map((item) => /* @__PURE__ */ jsx(QuestionCard, { item }, item.id)),
928
+ items.some((item) => item.status !== "pending" && item.continuation !== "unconfirmed") && /* @__PURE__ */ jsxs("details", { children: [/* @__PURE__ */ jsx("summary", { children: "Recent question outcomes" }), items.filter((item) => item.status !== "pending" && item.continuation !== "unconfirmed").map((item) => /* @__PURE__ */ jsx(QuestionCard, { item }, item.id))] })
929
+ ]
930
+ });
931
+ }
932
+ function QuestionCard({ item }) {
933
+ const storageKey = `bazilion-question-answer:${item.agentId}:${item.id}`;
934
+ const [choice, setChoice] = useState("");
935
+ const [text, setText] = useState("");
936
+ const [pending, setPending] = useState(null);
937
+ const pendingRef = useRef(null);
938
+ const [ready, setReady] = useState(false);
939
+ const [busy, setBusy] = useState(false);
940
+ const locked = useRef(false);
941
+ const [error, setError] = useState("");
942
+ const [notice, setNotice] = useState("");
943
+ useEffect(() => {
944
+ try {
945
+ const saved = sessionStorage.getItem(storageKey);
946
+ if (saved) {
947
+ const value = JSON.parse(saved);
948
+ pendingRef.current = value;
949
+ setPending(value);
950
+ }
951
+ setReady(true);
952
+ } catch {
953
+ setError("Answer recovery storage is unavailable. Enable session storage before answering.");
954
+ }
955
+ }, [storageKey]);
956
+ useEffect(() => {
957
+ if (item.status === "pending") return;
958
+ try {
959
+ sessionStorage.removeItem(storageKey);
960
+ pendingRef.current = null;
961
+ setPending(null);
962
+ } catch {}
963
+ }, [item.status, storageKey]);
964
+ async function submit(answer) {
965
+ if (locked.current || !ready || item.status !== "pending") return;
966
+ locked.current = true;
967
+ setBusy(true);
968
+ setError("");
969
+ try {
970
+ if (!pendingRef.current && !answer) throw new Error("Choose an answer first");
971
+ const input = pendingRef.current ?? {
972
+ requestId: crypto.randomUUID(),
973
+ conversationId: item.conversationId,
974
+ answer
975
+ };
976
+ sessionStorage.setItem(storageKey, JSON.stringify(input));
977
+ pendingRef.current = input;
978
+ setPending(input);
979
+ const response = await fetch(`/api/agents/${encodeURIComponent(item.agentId)}/questions/${encodeURIComponent(item.id)}/answer`, {
980
+ method: "POST",
981
+ headers: { "content-type": "application/json" },
982
+ body: JSON.stringify(input)
983
+ }).catch(() => {
984
+ throw new Error("Answer acknowledgement unavailable. Retry the same answer.");
985
+ });
986
+ if (!response.ok) {
987
+ if (response.status === 409 || response.status === 404) throw new Error("This question is no longer answerable. Its outcome will refresh shortly.");
988
+ throw new Error("Answer acknowledgement unavailable. Retry the same answer.");
989
+ }
990
+ if ((await response.json()).kind === "held") setNotice("Answer awaits communication approval. The Agent has not received it.");
991
+ else {
992
+ sessionStorage.removeItem(storageKey);
993
+ setNotice("Answer accepted. Waiting for saved conversation evidence that the Agent consumed it.");
994
+ }
995
+ } catch (e) {
996
+ setError(e instanceof Error ? e.message : "Answer failed");
997
+ } finally {
998
+ locked.current = false;
999
+ setBusy(false);
1000
+ }
1001
+ }
1002
+ const disabled = !ready || busy || !!pending || item.status !== "pending" || !!item.answerApprovalId;
1003
+ return /* @__PURE__ */ jsxs("article", {
1004
+ className: "flex min-w-0 flex-col gap-3 rounded-lg border border-frost bg-ivory p-3",
1005
+ children: [
1006
+ /* @__PURE__ */ jsx("p", {
1007
+ className: "m-0 break-words font-medium",
1008
+ children: item.question.prompt
1009
+ }),
1010
+ /* @__PURE__ */ jsxs("details", {
1011
+ className: "text-xs text-mocha",
1012
+ children: [/* @__PURE__ */ jsxs("summary", {
1013
+ className: "cursor-pointer",
1014
+ children: ["Question details · ", item.status]
1015
+ }), /* @__PURE__ */ jsxs("p", {
1016
+ className: "mt-2 break-all",
1017
+ children: [
1018
+ "Conversation ",
1019
+ item.conversationId,
1020
+ " · ",
1021
+ item.continuation
1022
+ ]
1023
+ })]
1024
+ }),
1025
+ item.status === "pending" ? /* @__PURE__ */ jsxs("form", {
1026
+ className: "flex min-w-0 flex-col gap-3",
1027
+ onSubmit: (event) => {
1028
+ event.preventDefault();
1029
+ if (choice === "other") submit({
1030
+ kind: "text",
1031
+ text
1032
+ });
1033
+ else if (choice !== "") submit({
1034
+ kind: "choice",
1035
+ index: Number(choice)
1036
+ });
1037
+ },
1038
+ children: [
1039
+ /* @__PURE__ */ jsxs("fieldset", {
1040
+ disabled,
1041
+ className: "m-0 grid min-w-0 gap-2 border-0 p-0 sm:grid-cols-2",
1042
+ children: [
1043
+ /* @__PURE__ */ jsx("legend", {
1044
+ className: "sr-only",
1045
+ children: "Answer this question"
1046
+ }),
1047
+ item.question.choices.map((option, index) => /* @__PURE__ */ jsxs("label", {
1048
+ className: "m-0 flex min-w-0 cursor-pointer items-center gap-2 rounded-md border border-frost px-3 py-2.5 has-[:checked]:border-sapphire has-[:checked]:bg-sapphire-glow",
1049
+ children: [/* @__PURE__ */ jsx("input", {
1050
+ className: "m-0 h-4 w-4 shrink-0 accent-sapphire",
1051
+ type: "radio",
1052
+ name: `question-${item.id}`,
1053
+ value: index,
1054
+ checked: choice === String(index),
1055
+ onChange: () => setChoice(String(index))
1056
+ }), /* @__PURE__ */ jsxs("span", {
1057
+ className: "min-w-0 break-words [overflow-wrap:anywhere]",
1058
+ children: [
1059
+ option.label,
1060
+ item.question.recommendedIndex === index ? " (Recommended)" : "",
1061
+ option.description && /* @__PURE__ */ jsx("small", {
1062
+ className: "block",
1063
+ children: option.description
1064
+ })
1065
+ ]
1066
+ })]
1067
+ }, index)),
1068
+ /* @__PURE__ */ jsxs("label", {
1069
+ className: "m-0 flex min-w-0 cursor-pointer items-center gap-2 rounded-md border border-frost px-3 py-2.5 has-[:checked]:border-sapphire has-[:checked]:bg-sapphire-glow",
1070
+ children: [/* @__PURE__ */ jsx("input", {
1071
+ className: "m-0 h-4 w-4 shrink-0 accent-sapphire",
1072
+ type: "radio",
1073
+ name: `question-${item.id}`,
1074
+ checked: choice === "other",
1075
+ onChange: () => setChoice("other")
1076
+ }), /* @__PURE__ */ jsx("span", { children: "Other" })]
1077
+ }),
1078
+ choice === "other" && /* @__PURE__ */ jsxs("label", {
1079
+ className: "m-0 grid gap-2 sm:col-span-2",
1080
+ children: ["Your answer", /* @__PURE__ */ jsx("textarea", {
1081
+ rows: 2,
1082
+ className: "w-full resize-y",
1083
+ value: text,
1084
+ maxLength: 4096,
1085
+ onChange: (event) => setText(event.target.value)
1086
+ })]
1087
+ })
1088
+ ]
1089
+ }),
1090
+ pending && /* @__PURE__ */ jsxs("p", {
1091
+ className: "break-words",
1092
+ children: ["Saved answer: ", pending.answer.kind === "choice" ? item.question.choices[pending.answer.index]?.label : pending.answer.kind === "text" ? pending.answer.text : "Skip"]
1093
+ }),
1094
+ /* @__PURE__ */ jsxs("div", {
1095
+ className: "flex flex-wrap gap-2",
1096
+ children: [
1097
+ /* @__PURE__ */ jsx(Button, {
1098
+ variant: "primary",
1099
+ type: "submit",
1100
+ disabled: disabled || !choice || choice === "other" && (!text.trim() || new TextEncoder().encode(text).length > 4096),
1101
+ children: "Send answer"
1102
+ }),
1103
+ /* @__PURE__ */ jsx(Button, {
1104
+ variant: "ghost",
1105
+ disabled,
1106
+ onClick: () => {
1107
+ submit({ kind: "skip" });
1108
+ },
1109
+ children: "Skip"
1110
+ }),
1111
+ pending && !item.answerApprovalId && /* @__PURE__ */ jsx(Button, {
1112
+ variant: "ghost",
1113
+ disabled: busy,
1114
+ onClick: () => {
1115
+ submit();
1116
+ },
1117
+ children: "Retry same answer"
1118
+ })
1119
+ ]
1120
+ }),
1121
+ /* @__PURE__ */ jsxs("p", {
1122
+ className: "m-0 text-xs text-mocha",
1123
+ children: [
1124
+ "Clarification does not grant permission. This question expires at ",
1125
+ new Date(item.expiresAt).toLocaleTimeString(),
1126
+ "."
1127
+ ]
1128
+ }),
1129
+ item.answerApprovalId && /* @__PURE__ */ jsx("a", {
1130
+ href: "/approvals",
1131
+ children: "View communication approvals"
1132
+ })
1133
+ ]
1134
+ }) : /* @__PURE__ */ jsxs("p", { children: [
1135
+ item.answer?.kind === "choice" ? item.question.choices[item.answer.index]?.label : item.answer?.kind === "text" ? item.answer.text : `No answer: ${item.noAnswerReason ?? item.status}`,
1136
+ ". ",
1137
+ item.continuation === "consumed" ? "Saved in the originating conversation; task completion is separate." : item.continuation === "interrupted" ? "The waiting turn ended." : "Consumption has not been confirmed."
1138
+ ] }),
1139
+ notice && /* @__PURE__ */ jsx("p", {
1140
+ role: "status",
1141
+ children: notice
1142
+ }),
1143
+ error && /* @__PURE__ */ jsx("p", {
1144
+ role: "alert",
1145
+ children: error
1146
+ })
1147
+ ]
1148
+ });
1149
+ }
1150
+ //#endregion
1151
+ //#region src/components/ChatPane.tsx
1152
+ var INBOX_WAKE_PREFIX = "[[bazilion:inbox-wake]]\n";
1153
+ var COMPACTION_REPLAY_PREFIX = "[conversation summary]";
1154
+ var AUTOSCROLL_THRESHOLD_PX = 40;
1155
+ var SESSION_POLL_MS = 1e4;
1156
+ var TOOL_GROUP_MAX_HEIGHT_PX = 320;
1157
+ var MAX_INPUT_HEIGHT = 200;
1158
+ var subscribeToHydration = () => () => {};
1159
+ var browserMarkdownReady = () => true;
1160
+ var serverMarkdownReady = () => false;
1161
+ /** Keep one inline card per ephemeral shell approval while preserving its first position. */
1162
+ function upsertCommandApprovalEntry(entries, approval) {
1163
+ const index = entries.findIndex((entry) => entry.type === "command_approval" && entry.approval.id === approval.id);
1164
+ if (index === -1) return [...entries, {
1165
+ type: "command_approval",
1166
+ approval
1167
+ }];
1168
+ const next = [...entries];
1169
+ next[index] = {
1170
+ type: "command_approval",
1171
+ approval
1172
+ };
1173
+ return next;
1174
+ }
1175
+ function interactiveChatRequest(message, attachments) {
1176
+ return {
1177
+ message,
1178
+ attachments,
1179
+ bashApprovalMode: "interactive",
1180
+ questionMode: "web"
1181
+ };
1182
+ }
1183
+ function shellApprovalsUrl(agentId) {
1184
+ return `/api/shell-approvals?agentId=${encodeURIComponent(agentId)}`;
1185
+ }
1186
+ var SLASH_HELP = "slash commands:\n /context — context breakdown (system prompt, tools, skills, history)\n /compact [N] — summarize the head; keep the last N messages verbatim (default 10)\n /new — open the retained conversation library\n /help — show this list";
1187
+ function prettyArgs(raw) {
1188
+ if (!raw) return "";
1189
+ try {
1190
+ return JSON.stringify(JSON.parse(raw), null, 2);
1191
+ } catch {
1192
+ return raw;
1193
+ }
1194
+ }
1195
+ function formatBytes(n) {
1196
+ if (n < 1024) return `${n} B`;
1197
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;
1198
+ return `${(n / (1024 * 1024)).toFixed(2)} MiB`;
1199
+ }
1200
+ /** Read any File into a base64 Attachment (strips the data: URL prefix). */
1201
+ function fileToAttachment(file) {
1202
+ return new Promise((resolve, reject) => {
1203
+ const reader = new FileReader();
1204
+ reader.onload = () => {
1205
+ const result = String(reader.result);
1206
+ const comma = result.indexOf(",");
1207
+ resolve({
1208
+ name: file.name,
1209
+ data: comma >= 0 ? result.slice(comma + 1) : result,
1210
+ mimeType: file.type || "application/octet-stream"
1211
+ });
1212
+ };
1213
+ reader.onerror = () => reject(reader.error);
1214
+ reader.readAsDataURL(file);
1215
+ });
1216
+ }
1217
+ var isImageMime = (m) => m.startsWith("image/");
1218
+ function projectMessages(msgs) {
1219
+ const entries = [];
1220
+ let openTool = null;
1221
+ for (const m of msgs) if (m.role === "user") {
1222
+ openTool = null;
1223
+ entries.push({
1224
+ type: "user",
1225
+ content: m.content,
1226
+ ...m.images && m.images.length > 0 ? { images: m.images } : {}
1227
+ });
1228
+ } else if (m.role === "assistant") {
1229
+ if (m.content) {
1230
+ openTool = null;
1231
+ entries.push({
1232
+ type: "assistant",
1233
+ content: m.content
1234
+ });
1235
+ }
1236
+ if (m.toolCalls && m.toolCalls.length > 0) {
1237
+ if (!openTool) {
1238
+ openTool = {
1239
+ type: "tool",
1240
+ items: []
1241
+ };
1242
+ entries.push(openTool);
1243
+ }
1244
+ for (const tc of m.toolCalls) openTool.items.push({
1245
+ kind: "call",
1246
+ id: tc.id,
1247
+ name: tc.name,
1248
+ body: tc.arguments
1249
+ });
1250
+ }
1251
+ } else if (m.role === "tool") {
1252
+ if (!openTool) {
1253
+ openTool = {
1254
+ type: "tool",
1255
+ items: []
1256
+ };
1257
+ entries.push(openTool);
1258
+ }
1259
+ const isError = m.content.startsWith("Error:") || m.content.startsWith("error:");
1260
+ openTool.items.push({
1261
+ kind: isError ? "error" : "result",
1262
+ id: m.toolCallId ?? "",
1263
+ name: m.toolName ?? "",
1264
+ body: m.content
1265
+ });
1266
+ if (m.result) {
1267
+ entries.push({
1268
+ type: "result",
1269
+ resultId: m.result.resultId
1270
+ });
1271
+ openTool = null;
1272
+ }
1273
+ if (m.images && m.images.length > 0) {
1274
+ entries.push({
1275
+ type: "images",
1276
+ images: m.images
1277
+ });
1278
+ openTool = null;
1279
+ }
1280
+ }
1281
+ return entries;
1282
+ }
1283
+ function ChatPane({ agentId, agentName, initialMessages, initialSessionHead }) {
1284
+ const markdownReady = useSyncExternalStore(subscribeToHydration, browserMarkdownReady, serverMarkdownReady);
1285
+ const [serverMessages, setServerMessages] = useState(initialMessages);
1286
+ const [liveEntries, setLiveEntries] = useState([]);
1287
+ const deliveredCoding = useRef(/* @__PURE__ */ new Map());
1288
+ const [systemBubbles, setSystemBubbles] = useState([]);
1289
+ const [thinking, setThinking] = useState(false);
1290
+ const [streaming, setStreaming] = useState(false);
1291
+ const [editIdx, setEditIdx] = useState(null);
1292
+ const [input, setInput] = useState("");
1293
+ const [attachments, setAttachments] = useState([]);
1294
+ const [dragging, setDragging] = useState(false);
1295
+ const [staleBanner, setStaleBanner] = useState(false);
1296
+ const [recoveredTurn, setRecoveredTurn] = useState(false);
1297
+ const [approvalBusy, setApprovalBusy] = useState({});
1298
+ const [approvalErrors, setApprovalErrors] = useState({});
1299
+ const [libraryOpen, setLibraryOpen] = useState(false);
1300
+ const [queueConversation, setQueueConversation] = useState();
1301
+ const [queueMode, setQueueMode] = useState(false);
1302
+ const [queueReady, setQueueReady] = useState(false);
1303
+ const queueRef = useRef(null);
1304
+ const [historyUnavailable, setHistoryUnavailable] = useState(initialSessionHead?.unavailable ?? false);
1305
+ const messagesRef = useRef(null);
1306
+ const fileInputRef = useRef(null);
1307
+ const inputRef = useRef(null);
1308
+ const currentAbortRef = useRef(null);
1309
+ const currentAgentIdRef = useRef(agentId);
1310
+ const commandApprovalTurnRef = useRef(false);
1311
+ currentAgentIdRef.current = agentId;
1312
+ const systemIdRef = useRef(0);
1313
+ const visibleEntryCountRef = useRef(0);
1314
+ const knownHeadRef = useRef(initialSessionHead ?? {
1315
+ file: null,
1316
+ size: 0
1317
+ });
1318
+ const streamingRef = useRef(false);
1319
+ streamingRef.current = streaming;
1320
+ const turnBusy = streaming || recoveredTurn;
1321
+ useEffect(() => {
1322
+ currentAbortRef.current?.abort();
1323
+ currentAbortRef.current = null;
1324
+ commandApprovalTurnRef.current = false;
1325
+ deliveredCoding.current.clear();
1326
+ setServerMessages(initialMessages);
1327
+ setLiveEntries([]);
1328
+ setSystemBubbles([]);
1329
+ setEditIdx(null);
1330
+ setInput("");
1331
+ setQueueReady(false);
1332
+ setAttachments([]);
1333
+ setHistoryUnavailable(initialSessionHead?.unavailable ?? false);
1334
+ setThinking(false);
1335
+ setStreaming(false);
1336
+ setStaleBanner(false);
1337
+ setRecoveredTurn(false);
1338
+ setApprovalBusy({});
1339
+ setApprovalErrors({});
1340
+ knownHeadRef.current = initialSessionHead ?? {
1341
+ file: null,
1342
+ size: 0
1343
+ };
1344
+ }, [agentId]);
1345
+ useEffect(() => {
1346
+ const controller = new AbortController();
1347
+ let stopped = false;
1348
+ async function recoverPendingCommandApprovals() {
1349
+ try {
1350
+ const response = await fetch(shellApprovalsUrl(agentId), { signal: controller.signal });
1351
+ if (!response.ok) return;
1352
+ const pending = (await response.json()).approvals.filter((approval) => approval.agentId === agentId && approval.status === "pending");
1353
+ if (stopped || pending.length === 0) return;
1354
+ sessionStorage.removeItem(`bz_pending_${agentId}`);
1355
+ commandApprovalTurnRef.current = true;
1356
+ setLiveEntries((entries) => pending.reduce(upsertCommandApprovalEntry, entries));
1357
+ setRecoveredTurn(true);
1358
+ } catch (error) {
1359
+ if (error.name === "AbortError") return;
1360
+ }
1361
+ }
1362
+ recoverPendingCommandApprovals();
1363
+ return () => {
1364
+ stopped = true;
1365
+ controller.abort();
1366
+ };
1367
+ }, [agentId]);
1368
+ const wasNearBottomRef = useRef(true);
1369
+ useLayoutEffect(() => {
1370
+ const el = messagesRef.current;
1371
+ if (!el) return;
1372
+ if (wasNearBottomRef.current) el.scrollTop = el.scrollHeight;
1373
+ });
1374
+ function captureScroll() {
1375
+ const el = messagesRef.current;
1376
+ if (!el) {
1377
+ wasNearBottomRef.current = true;
1378
+ return;
1379
+ }
1380
+ wasNearBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < AUTOSCROLL_THRESHOLD_PX;
1381
+ }
1382
+ useLayoutEffect(() => {
1383
+ const el = inputRef.current;
1384
+ if (!el) return;
1385
+ el.style.height = "auto";
1386
+ el.style.height = `${Math.min(el.scrollHeight, MAX_INPUT_HEIGHT)}px`;
1387
+ }, [input]);
1388
+ useEffect(() => {
1389
+ const key = `bz_pending_${agentId}`;
1390
+ function onVis() {
1391
+ if (document.visibilityState === "visible" && sessionStorage.getItem(key)) {
1392
+ sessionStorage.removeItem(key);
1393
+ setStaleBanner(true);
1394
+ }
1395
+ }
1396
+ document.addEventListener("visibilitychange", onVis);
1397
+ return () => document.removeEventListener("visibilitychange", onVis);
1398
+ }, [agentId]);
1399
+ useEffect(() => {
1400
+ if (!initialSessionHead) return;
1401
+ let stopped = false;
1402
+ let timer = null;
1403
+ async function tick() {
1404
+ if (stopped) return;
1405
+ if (!document.hidden && !streamingRef.current && !staleBanner) try {
1406
+ const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/sessions/head`);
1407
+ if (res.ok) {
1408
+ const body = await res.json();
1409
+ if (typeof body.size === "number") {
1410
+ const known = knownHeadRef.current;
1411
+ if (body.unavailable !== known.unavailable || body.file !== known.file || body.size !== known.size) setStaleBanner(true);
1412
+ }
1413
+ }
1414
+ } catch {}
1415
+ timer = setTimeout(tick, SESSION_POLL_MS);
1416
+ }
1417
+ timer = setTimeout(tick, SESSION_POLL_MS);
1418
+ return () => {
1419
+ stopped = true;
1420
+ if (timer) clearTimeout(timer);
1421
+ };
1422
+ }, [
1423
+ agentId,
1424
+ initialSessionHead,
1425
+ staleBanner
1426
+ ]);
1427
+ async function refreshConversationHistory() {
1428
+ if (streamingRef.current) return;
1429
+ try {
1430
+ const response = await fetch(`/api/agents/${encodeURIComponent(agentId)}/sessions/messages`);
1431
+ if (!response.ok) throw new Error("Conversation history is unavailable. Your draft is preserved.");
1432
+ const body = await response.json();
1433
+ if (currentAgentIdRef.current !== agentId) return;
1434
+ setHistoryUnavailable(body.head.unavailable ?? false);
1435
+ setServerMessages(body.messages);
1436
+ setLiveEntries([]);
1437
+ setEditIdx(null);
1438
+ knownHeadRef.current = body.head;
1439
+ setStaleBanner(false);
1440
+ } catch (error) {
1441
+ pushSystem(error instanceof Error ? error.message : "Could not refresh conversation");
1442
+ }
1443
+ }
1444
+ async function refreshKnownHead() {
1445
+ if (currentAgentIdRef.current !== agentId) return;
1446
+ try {
1447
+ const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/sessions/head`);
1448
+ if (!res.ok) return;
1449
+ const body = await res.json();
1450
+ if (currentAgentIdRef.current === agentId && typeof body.size === "number") {
1451
+ const prior = knownHeadRef.current.selection;
1452
+ if (prior?.conversationId && prior.conversationId !== body.selection?.conversationId) {
1453
+ setStaleBanner(true);
1454
+ return;
1455
+ }
1456
+ setHistoryUnavailable(body.unavailable ?? false);
1457
+ knownHeadRef.current = body;
1458
+ }
1459
+ } catch {}
1460
+ }
1461
+ function pushSystem(text) {
1462
+ captureScroll();
1463
+ setSystemBubbles((prev) => [...prev, {
1464
+ id: systemIdRef.current++,
1465
+ content: text,
1466
+ afterIdx: visibleEntryCountRef.current
1467
+ }]);
1468
+ }
1469
+ async function runContextCommand() {
1470
+ try {
1471
+ const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/chat/context`);
1472
+ if (!res.ok) {
1473
+ pushSystem(`/context failed: ${res.statusText}`);
1474
+ return;
1475
+ }
1476
+ const ctx = await res.json();
1477
+ const fmt = (chars, tokens) => `${chars.toLocaleString()} chars (~${tokens.toLocaleString()} tok)`;
1478
+ const lines = [];
1479
+ lines.push(`context for ${ctx.agentId}`);
1480
+ lines.push(`model: ${ctx.model}`);
1481
+ lines.push("");
1482
+ lines.push(`system prompt: ${fmt(ctx.systemPrompt.chars, ctx.systemPrompt.tokens)}`);
1483
+ for (const f of ctx.systemPrompt.files) lines.push(` - ${f.name}: ${f.chars.toLocaleString()} chars`);
1484
+ lines.push("");
1485
+ lines.push(`tools: ${ctx.tools.count} (${ctx.tools.schemaChars.toLocaleString()} chars of schema)`);
1486
+ for (const t of ctx.tools.entries.slice(0, 5)) lines.push(` - ${t.name}: ${t.schemaChars.toLocaleString()} chars`);
1487
+ if (ctx.skills.count > 0) {
1488
+ lines.push("");
1489
+ lines.push(`skills: ${ctx.skills.count}`);
1490
+ for (const s of ctx.skills.entries.slice(0, 10)) lines.push(` - ${s.name}: ${s.blockChars.toLocaleString()} chars`);
1491
+ }
1492
+ lines.push("");
1493
+ lines.push(`history: ${ctx.history.messageEntries} messages / ${ctx.history.compactionEntries} compactions`);
1494
+ lines.push(` ${fmt(ctx.history.chars, ctx.history.tokensEstimate)}`);
1495
+ lines.push(` bytes on disk: ${formatBytes(ctx.history.bytes)}`);
1496
+ lines.push("");
1497
+ lines.push(`TOTAL: ${fmt(ctx.totals.chars, ctx.totals.tokens)}`);
1498
+ pushSystem(lines.join("\n"));
1499
+ } catch (err) {
1500
+ pushSystem(`/context failed: ${err.message}`);
1501
+ }
1502
+ }
1503
+ async function runCompactCommand(rest) {
1504
+ if (serverMessages.length < 2) {
1505
+ pushSystem("/compact: need ≥2 messages to compact");
1506
+ return;
1507
+ }
1508
+ let keepTail;
1509
+ const argText = rest.trim();
1510
+ if (argText) {
1511
+ const n = Number(argText);
1512
+ if (Number.isFinite(n) && n >= 0) keepTail = Math.floor(n);
1513
+ }
1514
+ exitEditMode();
1515
+ setStreaming(true);
1516
+ setThinking(true);
1517
+ try {
1518
+ const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/chat/compact`, {
1519
+ method: "POST",
1520
+ headers: { "content-type": "application/json" },
1521
+ body: JSON.stringify({
1522
+ keepTail,
1523
+ expectedSelection: knownHeadRef.current.selection
1524
+ })
1525
+ });
1526
+ if (!res.ok) {
1527
+ let msg = res.statusText;
1528
+ try {
1529
+ msg = (await res.json()).error || msg;
1530
+ } catch {}
1531
+ pushSystem(`/compact failed: ${msg}`);
1532
+ return;
1533
+ }
1534
+ const body = await res.json();
1535
+ pushSystem(`/compact: ${body.before} → ${body.after} entries (${body.summarized} summarized, ${body.keptTail} kept verbatim; ~${body.tokensBefore.toLocaleString()} → ~${body.tokensAfter.toLocaleString()} tok). reloading…`);
1536
+ setTimeout(() => window.location.reload(), 400);
1537
+ } catch (err) {
1538
+ pushSystem(`/compact failed: ${err.message}`);
1539
+ } finally {
1540
+ setThinking(false);
1541
+ setStreaming(false);
1542
+ }
1543
+ }
1544
+ async function maybeHandleSlashCommand(text) {
1545
+ const trimmed = text.trim();
1546
+ if (!trimmed.startsWith("/")) return false;
1547
+ const parts = trimmed.split(/\s+/);
1548
+ const cmd = (parts[0] ?? "").toLowerCase();
1549
+ const rest = parts.slice(1).join(" ");
1550
+ switch (cmd) {
1551
+ case "/help":
1552
+ pushSystem(SLASH_HELP);
1553
+ return true;
1554
+ case "/context":
1555
+ await runContextCommand();
1556
+ return true;
1557
+ case "/new":
1558
+ setLibraryOpen(true);
1559
+ return true;
1560
+ case "/compact":
1561
+ await runCompactCommand(rest);
1562
+ return true;
1563
+ default: return false;
1564
+ }
1565
+ }
1566
+ function findLastUserIdx() {
1567
+ for (let i = serverMessages.length - 1; i >= 0; i--) if (serverMessages[i]?.role === "user") return i;
1568
+ return -1;
1569
+ }
1570
+ function enterEditMode() {
1571
+ if (liveEntries.length > 0 || turnBusy) return;
1572
+ const idx = findLastUserIdx();
1573
+ if (idx === -1) return;
1574
+ const userMsg = serverMessages[idx];
1575
+ if (!userMsg) return;
1576
+ setEditIdx(idx);
1577
+ setInput(userMsg.content);
1578
+ inputRef.current?.focus();
1579
+ }
1580
+ function exitEditMode() {
1581
+ setEditIdx(null);
1582
+ }
1583
+ async function addFiles(files) {
1584
+ if (!files) return;
1585
+ const arr = Array.from(files);
1586
+ if (arr.length === 0) return;
1587
+ const encoded = await Promise.all(arr.map(fileToAttachment));
1588
+ setAttachments((prev) => [...prev, ...encoded].slice(0, 16));
1589
+ }
1590
+ function onPaste(e) {
1591
+ const files = Array.from(e.clipboardData.files);
1592
+ if (files.some((f) => f.type.startsWith("image/"))) {
1593
+ e.preventDefault();
1594
+ addFiles(files);
1595
+ }
1596
+ }
1597
+ function onDragOver(e) {
1598
+ if (!Array.from(e.dataTransfer.types).includes("Files")) return;
1599
+ e.preventDefault();
1600
+ setDragging(true);
1601
+ }
1602
+ function onDragLeave(e) {
1603
+ if (e.currentTarget === e.target) setDragging(false);
1604
+ }
1605
+ function onDrop(e) {
1606
+ if (e.dataTransfer.files.length === 0) return;
1607
+ e.preventDefault();
1608
+ setDragging(false);
1609
+ addFiles(e.dataTransfer.files);
1610
+ }
1611
+ const send = useCallback(async (text) => {
1612
+ const atts = attachments;
1613
+ if (!text.trim() && atts.length === 0) return;
1614
+ if (!queueReady) {
1615
+ pushSystem("Queue status is loading. Your draft is preserved.");
1616
+ return;
1617
+ }
1618
+ if (turnBusy || queueMode) {
1619
+ if (await queueRef.current?.enqueue(text, atts, knownHeadRef.current.selection) && currentAgentIdRef.current === agentId) {
1620
+ setInput((current) => current === text ? "" : current);
1621
+ setAttachments((current) => current === atts ? [] : current);
1622
+ }
1623
+ return;
1624
+ }
1625
+ setInput("");
1626
+ if (await maybeHandleSlashCommand(text)) return;
1627
+ setAttachments([]);
1628
+ let truncatedServerMessages = null;
1629
+ if (editIdx !== null) {
1630
+ const keep = editIdx;
1631
+ try {
1632
+ const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/chat/truncate`, {
1633
+ method: "POST",
1634
+ headers: { "content-type": "application/json" },
1635
+ body: JSON.stringify({
1636
+ keepCount: keep,
1637
+ expectedSelection: knownHeadRef.current.selection
1638
+ })
1639
+ });
1640
+ if (!res.ok) {
1641
+ let err = res.statusText;
1642
+ try {
1643
+ err = (await res.json()).error || err;
1644
+ } catch {}
1645
+ setInput(text);
1646
+ pushSystem(`Could not replace the last turn: ${err}`);
1647
+ return;
1648
+ }
1649
+ truncatedServerMessages = serverMessages.slice(0, keep);
1650
+ setServerMessages(truncatedServerMessages);
1651
+ setEditIdx(null);
1652
+ } catch (err) {
1653
+ setInput(text);
1654
+ pushSystem(`Could not replace the last turn: ${err.message}`);
1655
+ return;
1656
+ }
1657
+ }
1658
+ captureScroll();
1659
+ const previewImages = atts.filter((a) => isImageMime(a.mimeType));
1660
+ const previewFiles = atts.filter((a) => !isImageMime(a.mimeType));
1661
+ setLiveEntries([{
1662
+ type: "user",
1663
+ content: text,
1664
+ ...previewImages.length > 0 ? { images: previewImages } : {},
1665
+ ...previewFiles.length > 0 ? { files: previewFiles.map((f) => ({ name: f.name ?? "file" })) } : {}
1666
+ }]);
1667
+ setRecoveredTurn(false);
1668
+ setApprovalBusy({});
1669
+ setApprovalErrors({});
1670
+ commandApprovalTurnRef.current = false;
1671
+ sessionStorage.setItem(`bz_pending_${agentId}`, "1");
1672
+ const abort = new AbortController();
1673
+ currentAbortRef.current = abort;
1674
+ setStreaming(true);
1675
+ setThinking(true);
1676
+ let terminalFrameReceived = false;
1677
+ try {
1678
+ const res = await fetch(`/api/agents/${encodeURIComponent(agentId)}/chat`, {
1679
+ method: "POST",
1680
+ headers: { "content-type": "application/json" },
1681
+ body: JSON.stringify({
1682
+ ...interactiveChatRequest(text, atts),
1683
+ expectedSelection: knownHeadRef.current.selection
1684
+ }),
1685
+ signal: abort.signal
1686
+ });
1687
+ if (res.status === 202) {
1688
+ const pending = await res.json();
1689
+ setLiveEntries((prev) => [...prev, {
1690
+ type: "error",
1691
+ content: `[approval pending] ${pending.approvalId} · expires ${new Date(pending.expiresAt).toLocaleString()} · review in /approvals`
1692
+ }]);
1693
+ sessionStorage.removeItem(`bz_pending_${agentId}`);
1694
+ return;
1695
+ }
1696
+ if (res.status === 409) {
1697
+ setInput(text);
1698
+ setAttachments(atts);
1699
+ setLiveEntries([]);
1700
+ setStaleBanner(true);
1701
+ sessionStorage.removeItem(`bz_pending_${agentId}`);
1702
+ pushSystem("Conversation changed. Refresh history, then review and send your preserved draft.");
1703
+ return;
1704
+ }
1705
+ if (!res.ok || !res.body) {
1706
+ let err = res.statusText;
1707
+ try {
1708
+ err = (await res.json()).error || err;
1709
+ } catch {}
1710
+ setLiveEntries((prev) => [...prev, {
1711
+ type: "error",
1712
+ content: `[error] ${err}`
1713
+ }]);
1714
+ return;
1715
+ }
1716
+ const admittedSelection = res.headers.get("x-bazilion-conversation-selection");
1717
+ if (admittedSelection) try {
1718
+ const selection = JSON.parse(admittedSelection);
1719
+ if (typeof selection.conversationId === "string" && Number.isSafeInteger(selection.revision) && selection.revision >= 0) knownHeadRef.current = {
1720
+ ...knownHeadRef.current,
1721
+ selection
1722
+ };
1723
+ } catch {}
1724
+ const reader = res.body.getReader();
1725
+ const decoder = new TextDecoder();
1726
+ let buffer = "";
1727
+ while (true) {
1728
+ const { done, value } = await reader.read();
1729
+ if (done) break;
1730
+ buffer += decoder.decode(value, { stream: true });
1731
+ const lines = buffer.split("\n");
1732
+ buffer = lines.pop() ?? "";
1733
+ for (const line of lines) {
1734
+ if (!line.trim()) continue;
1735
+ let frame;
1736
+ try {
1737
+ frame = JSON.parse(line);
1738
+ } catch {
1739
+ continue;
1740
+ }
1741
+ if (frame.kind === "done" || frame.kind === "fatal") terminalFrameReceived = true;
1742
+ handleFrame(frame);
1743
+ }
1744
+ }
1745
+ if (buffer.trim()) try {
1746
+ const frame = JSON.parse(buffer);
1747
+ if (frame.kind === "done" || frame.kind === "fatal") terminalFrameReceived = true;
1748
+ handleFrame(frame);
1749
+ } catch {}
1750
+ await refreshKnownHead();
1751
+ sessionStorage.removeItem(`bz_pending_${agentId}`);
1752
+ } catch (err) {
1753
+ if (err.name !== "AbortError") setLiveEntries((prev) => [...prev, {
1754
+ type: "error",
1755
+ content: `[network error] ${err.message}`
1756
+ }]);
1757
+ sessionStorage.removeItem(`bz_pending_${agentId}`);
1758
+ } finally {
1759
+ if (currentAgentIdRef.current === agentId && currentAbortRef.current === abort) {
1760
+ if (!terminalFrameReceived && commandApprovalTurnRef.current) setRecoveredTurn(true);
1761
+ setThinking(false);
1762
+ setStreaming(false);
1763
+ currentAbortRef.current = null;
1764
+ }
1765
+ }
1766
+ }, [
1767
+ agentId,
1768
+ editIdx,
1769
+ serverMessages,
1770
+ turnBusy,
1771
+ queueMode,
1772
+ queueReady,
1773
+ attachments
1774
+ ]);
1775
+ function handleFrame(frame) {
1776
+ if (currentAgentIdRef.current !== agentId) return;
1777
+ if (frame.kind === "fatal") {
1778
+ commandApprovalTurnRef.current = false;
1779
+ setLiveEntries((prev) => [...prev.map((entry) => entry.type === "command_approval" && entry.approval.status === "pending" ? {
1780
+ type: "command_approval",
1781
+ approval: {
1782
+ ...entry.approval,
1783
+ status: "cancelled"
1784
+ }
1785
+ } : entry), {
1786
+ type: "error",
1787
+ content: `[fatal] ${frame.error}`
1788
+ }]);
1789
+ setRecoveredTurn(false);
1790
+ return;
1791
+ }
1792
+ if (frame.kind === "done") {
1793
+ commandApprovalTurnRef.current = false;
1794
+ setServerMessages(frame.messages);
1795
+ setLiveEntries([]);
1796
+ setRecoveredTurn(false);
1797
+ setApprovalBusy({});
1798
+ setApprovalErrors({});
1799
+ return;
1800
+ }
1801
+ if (frame.kind !== "event") return;
1802
+ const ev = frame.event;
1803
+ captureScroll();
1804
+ if (ev.type === "user_message") return;
1805
+ if (ev.type === "assistant_delta") {
1806
+ setThinking(false);
1807
+ setLiveEntries((prev) => {
1808
+ const next = [...prev];
1809
+ const last = next[next.length - 1];
1810
+ if (last?.type === "assistant") next[next.length - 1] = {
1811
+ type: "assistant",
1812
+ content: last.content + ev.delta
1813
+ };
1814
+ else next.push({
1815
+ type: "assistant",
1816
+ content: ev.delta
1817
+ });
1818
+ return next;
1819
+ });
1820
+ return;
1821
+ }
1822
+ if (ev.type === "assistant_message") {
1823
+ setThinking(false);
1824
+ setLiveEntries((prev) => {
1825
+ const next = [...prev];
1826
+ if (next[next.length - 1]?.type === "assistant") next[next.length - 1] = {
1827
+ type: "assistant",
1828
+ content: ev.text
1829
+ };
1830
+ else next.push({
1831
+ type: "assistant",
1832
+ content: ev.text
1833
+ });
1834
+ return next;
1835
+ });
1836
+ return;
1837
+ }
1838
+ if (ev.type === "command_approval") {
1839
+ if (ev.approval.agentId !== agentId) return;
1840
+ commandApprovalTurnRef.current = true;
1841
+ setThinking(ev.approval.status !== "pending" && ev.approval.status !== "cancelled");
1842
+ setLiveEntries((entries) => upsertCommandApprovalEntry(entries, ev.approval));
1843
+ return;
1844
+ }
1845
+ if (ev.type === "tool_call" || ev.type === "tool_result" || ev.type === "tool_error") {
1846
+ setThinking(ev.type !== "tool_call");
1847
+ const item = ev.type === "tool_call" ? {
1848
+ kind: "call",
1849
+ id: ev.id,
1850
+ name: ev.name,
1851
+ body: ev.arguments
1852
+ } : ev.type === "tool_result" ? {
1853
+ kind: "result",
1854
+ id: ev.id,
1855
+ name: ev.name,
1856
+ body: ev.result
1857
+ } : {
1858
+ kind: "error",
1859
+ id: ev.id,
1860
+ name: ev.name,
1861
+ body: ev.error
1862
+ };
1863
+ if (ev.type === "tool_result" && [
1864
+ "repository_context",
1865
+ "coding_environment",
1866
+ "coding_command",
1867
+ "coding_receipt"
1868
+ ].includes(ev.name)) {
1869
+ deliveredCoding.current.set(`${ev.name}:${ev.id}`, ev.result);
1870
+ while (deliveredCoding.current.size > 20) deliveredCoding.current.delete(deliveredCoding.current.keys().next().value);
1871
+ }
1872
+ const images = ev.type === "tool_result" ? ev.images : void 0;
1873
+ setLiveEntries((prev) => {
1874
+ const next = [...prev];
1875
+ const last = next[next.length - 1];
1876
+ if (last?.type === "tool") next[next.length - 1] = {
1877
+ type: "tool",
1878
+ items: [...last.items, item]
1879
+ };
1880
+ else next.push({
1881
+ type: "tool",
1882
+ items: [item]
1883
+ });
1884
+ if (images && images.length > 0) next.push({
1885
+ type: "images",
1886
+ images
1887
+ });
1888
+ return next;
1889
+ });
1890
+ return;
1891
+ }
1892
+ if (ev.type === "file") {
1893
+ if (ev.result) {
1894
+ const resultId = ev.result.resultId;
1895
+ setLiveEntries((prev) => [...prev, {
1896
+ type: "result",
1897
+ resultId
1898
+ }]);
1899
+ return;
1900
+ }
1901
+ setLiveEntries((prev) => [...prev, {
1902
+ type: "file",
1903
+ name: ev.name,
1904
+ mimeType: ev.mimeType,
1905
+ data: ev.data
1906
+ }]);
1907
+ return;
1908
+ }
1909
+ if (ev.type === "error") setLiveEntries((prev) => [...prev, {
1910
+ type: "error",
1911
+ content: `[error] ${ev.error}`
1912
+ }]);
1913
+ }
1914
+ async function reconcileCommandApproval(approvalId) {
1915
+ if (currentAgentIdRef.current !== agentId) return null;
1916
+ try {
1917
+ const response = await fetch(shellApprovalsUrl(agentId));
1918
+ if (!response.ok) return null;
1919
+ const body = await response.json();
1920
+ if (currentAgentIdRef.current !== agentId) return null;
1921
+ const approval = body.approvals.find((item) => item.id === approvalId && item.agentId === agentId);
1922
+ if (approval) setLiveEntries((entries) => upsertCommandApprovalEntry(entries, approval));
1923
+ return approval ?? null;
1924
+ } catch {
1925
+ return null;
1926
+ }
1927
+ }
1928
+ async function decideCommandApproval(approval, decision) {
1929
+ if (approval.agentId !== agentId || approval.status !== "pending") return;
1930
+ setApprovalBusy((current) => ({
1931
+ ...current,
1932
+ [approval.id]: true
1933
+ }));
1934
+ setApprovalErrors((current) => {
1935
+ const next = { ...current };
1936
+ delete next[approval.id];
1937
+ return next;
1938
+ });
1939
+ try {
1940
+ const response = await fetch(`/api/shell-approvals/${encodeURIComponent(approval.id)}`, {
1941
+ method: "POST",
1942
+ headers: { "content-type": "application/json" },
1943
+ body: JSON.stringify({ decision })
1944
+ });
1945
+ const body = await response.json().catch(() => null);
1946
+ if (currentAgentIdRef.current !== agentId) return;
1947
+ if (body?.approval?.agentId === agentId) setLiveEntries((entries) => upsertCommandApprovalEntry(entries, body.approval));
1948
+ if (!response.ok) {
1949
+ const message = body?.error ?? `Decision failed (${response.status})`;
1950
+ const terminalStatus = commandApprovalStatusFromConflict(message);
1951
+ if (terminalStatus) {
1952
+ setLiveEntries((entries) => upsertCommandApprovalEntry(entries, {
1953
+ ...approval,
1954
+ status: terminalStatus
1955
+ }));
1956
+ return;
1957
+ }
1958
+ throw new Error(message);
1959
+ }
1960
+ } catch (error) {
1961
+ const reconciled = await reconcileCommandApproval(approval.id);
1962
+ if (currentAgentIdRef.current !== agentId) return;
1963
+ if (!reconciled || reconciled.status === "pending") setApprovalErrors((current) => ({
1964
+ ...current,
1965
+ [approval.id]: error instanceof Error ? error.message : String(error)
1966
+ }));
1967
+ } finally {
1968
+ if (currentAgentIdRef.current === agentId) setApprovalBusy((current) => ({
1969
+ ...current,
1970
+ [approval.id]: false
1971
+ }));
1972
+ }
1973
+ }
1974
+ async function cancel() {
1975
+ if (!await queueRef.current?.stop() && currentAgentIdRef.current === agentId) pushSystem("Stop was not confirmed. Check queue controls and try again; the active response may still be running.");
1976
+ }
1977
+ function onKeyDown(e) {
1978
+ if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
1979
+ e.preventDefault();
1980
+ send(input);
1981
+ }
1982
+ }
1983
+ const baseEntries = projectMessages(serverMessages.map((message) => {
1984
+ if (message.role !== "tool" || !message.toolCallId || !message.toolName) return message;
1985
+ const body = deliveredCoding.current.get(`${message.toolName}:${message.toolCallId}`);
1986
+ return body === void 0 ? message : {
1987
+ ...message,
1988
+ content: body
1989
+ };
1990
+ }));
1991
+ visibleEntryCountRef.current = baseEntries.length + liveEntries.length;
1992
+ const lastUserIdx = (() => {
1993
+ if (liveEntries.length > 0 || turnBusy) return -1;
1994
+ for (let i = baseEntries.length - 1; i >= 0; i--) if (baseEntries[i]?.type === "user") return i;
1995
+ return -1;
1996
+ })();
1997
+ const willDropFromIdx = (() => {
1998
+ if (editIdx === null) return -1;
1999
+ let userCount = 0;
2000
+ for (let i = 0; i < baseEntries.length; i++) if (baseEntries[i]?.type === "user") {
2001
+ if (userCount === serverMessages.slice(0, editIdx + 1).filter((m) => m.role === "user").length - 1) return i;
2002
+ userCount++;
2003
+ }
2004
+ return lastUserIdx;
2005
+ })();
2006
+ return /* @__PURE__ */ jsxs("div", {
2007
+ className: "relative flex h-full flex-col overflow-hidden rounded-[16px] border border-frost bg-snow shadow-baziu-sm",
2008
+ onDragOver,
2009
+ onDragLeave,
2010
+ onDrop,
2011
+ children: [
2012
+ dragging && /* @__PURE__ */ jsx("div", {
2013
+ className: "pointer-events-none absolute inset-0 z-10 flex items-center justify-center rounded-[16px] border-2 border-dashed border-sapphire bg-sapphire-glow/80 text-[0.95em] font-medium text-sapphire-deep",
2014
+ children: "drop images or files to attach"
2015
+ }),
2016
+ /* @__PURE__ */ jsxs("div", {
2017
+ className: "flex items-center justify-between gap-3 border-b border-frost px-4 py-2.5",
2018
+ children: [/* @__PURE__ */ jsxs("div", {
2019
+ className: "flex min-w-0 items-center gap-2",
2020
+ children: [/* @__PURE__ */ jsx("a", {
2021
+ href: "/",
2022
+ className: "inline-flex h-7 items-center rounded-md border border-frost px-2 text-xs font-semibold text-mocha lg:hidden",
2023
+ children: "← agents"
2024
+ }), /* @__PURE__ */ jsx("h1", {
2025
+ className: "truncate font-display text-[1.2rem] text-charcoal",
2026
+ children: agentName
2027
+ })]
2028
+ }), /* @__PURE__ */ jsx("a", {
2029
+ href: `/agents/${agentId}?mode=settings`,
2030
+ className: "shrink-0 text-xs text-mocha-light hover:text-sapphire",
2031
+ children: "Manage agent →"
2032
+ })]
2033
+ }),
2034
+ /* @__PURE__ */ jsx("div", {
2035
+ className: "mx-5 mt-2",
2036
+ children: /* @__PURE__ */ jsx(Button, {
2037
+ variant: "ghost",
2038
+ "aria-expanded": libraryOpen,
2039
+ onClick: () => setLibraryOpen((value) => !value),
2040
+ children: libraryOpen ? "Close conversations" : "Conversations"
2041
+ })
2042
+ }),
2043
+ historyUnavailable && /* @__PURE__ */ jsx("p", {
2044
+ role: "alert",
2045
+ className: "mx-5 mt-2 text-sm",
2046
+ children: "Selected conversation history is unavailable. Open Conversations to start a new one; retained history and files stay listed."
2047
+ }),
2048
+ staleBanner && /* @__PURE__ */ jsxs("div", {
2049
+ role: "status",
2050
+ "aria-live": "polite",
2051
+ "aria-atomic": "true",
2052
+ className: "mx-5 mt-2 flex items-center gap-2 rounded-md border border-sapphire bg-frost px-4 py-2 text-[0.86em] text-mocha",
2053
+ children: [
2054
+ /* @__PURE__ */ jsx("span", {
2055
+ className: "h-2 w-2 flex-none rounded-full bg-sapphire",
2056
+ "aria-hidden": "true"
2057
+ }),
2058
+ /* @__PURE__ */ jsx("span", {
2059
+ className: "flex-1",
2060
+ children: recoveredTurn ? "the recovered turn has new activity — reload to see it" : "new activity from another source — reload to see it"
2061
+ }),
2062
+ /* @__PURE__ */ jsx("button", {
2063
+ type: "button",
2064
+ onClick: () => void refreshConversationHistory(),
2065
+ className: "rounded-sm bg-sapphire px-3 py-1 text-[0.92em] text-snow hover:opacity-90",
2066
+ children: "reload"
2067
+ }),
2068
+ /* @__PURE__ */ jsx("button", {
2069
+ type: "button",
2070
+ onClick: () => setStaleBanner(false),
2071
+ className: "px-1 text-mocha-light hover:text-mocha",
2072
+ "aria-label": "dismiss",
2073
+ children: "×"
2074
+ })
2075
+ ]
2076
+ }),
2077
+ /* @__PURE__ */ jsxs("div", {
2078
+ ref: messagesRef,
2079
+ onScroll: captureScroll,
2080
+ role: "log",
2081
+ "aria-label": `Conversation with ${agentName}`,
2082
+ "aria-live": "polite",
2083
+ "aria-relevant": "additions",
2084
+ "aria-busy": turnBusy,
2085
+ className: `min-h-0 flex-1 overflow-y-auto px-5 py-5 ${editIdx !== null ? "is-editing" : ""}`,
2086
+ children: [
2087
+ libraryOpen && /* @__PURE__ */ jsx(ConversationLibrary, {
2088
+ initialConversationId: queueConversation,
2089
+ agentId,
2090
+ turnBusy,
2091
+ onCreated: refreshConversationHistory,
2092
+ renderHistory: (messages) => /* @__PURE__ */ jsx(ResultTranscript, { messages })
2093
+ }, agentId),
2094
+ !historyUnavailable && baseEntries.length === 0 && liveEntries.length === 0 && systemBubbles.length === 0 && /* @__PURE__ */ jsx("p", {
2095
+ className: "py-12 text-center italic text-mocha-light",
2096
+ children: "Start a conversation…"
2097
+ }),
2098
+ (() => {
2099
+ const out = [];
2100
+ let sysIdx = 0;
2101
+ const flushSysUpTo = (idx) => {
2102
+ while (sysIdx < systemBubbles.length && systemBubbles[sysIdx].afterIdx <= idx) {
2103
+ const sb = systemBubbles[sysIdx];
2104
+ out.push(/* @__PURE__ */ jsx(Bubble, {
2105
+ entry: {
2106
+ type: "system",
2107
+ content: sb.content
2108
+ },
2109
+ markdownReady,
2110
+ onCommandApprovalDecision: decideCommandApproval
2111
+ }, `y-${sb.id}`));
2112
+ sysIdx++;
2113
+ }
2114
+ };
2115
+ flushSysUpTo(0);
2116
+ for (let i = 0; i < baseEntries.length; i++) {
2117
+ const entry = baseEntries[i];
2118
+ out.push(/* @__PURE__ */ jsx(Bubble, {
2119
+ entry,
2120
+ markdownReady,
2121
+ isLastUser: i === lastUserIdx && editIdx === null,
2122
+ isWillDrop: willDropFromIdx !== -1 && i >= willDropFromIdx,
2123
+ onEdit: enterEditMode,
2124
+ onCommandApprovalDecision: decideCommandApproval,
2125
+ commandApprovalBusy: entry.type === "command_approval" && Boolean(approvalBusy[entry.approval.id]),
2126
+ commandApprovalError: entry.type === "command_approval" ? approvalErrors[entry.approval.id] : void 0
2127
+ }, `s-${i}`));
2128
+ flushSysUpTo(i + 1);
2129
+ }
2130
+ for (let i = 0; i < liveEntries.length; i++) {
2131
+ const entry = liveEntries[i];
2132
+ out.push(/* @__PURE__ */ jsx(Bubble, {
2133
+ entry,
2134
+ markdownReady,
2135
+ onCommandApprovalDecision: decideCommandApproval,
2136
+ commandApprovalBusy: entry.type === "command_approval" && Boolean(approvalBusy[entry.approval.id]),
2137
+ commandApprovalError: entry.type === "command_approval" ? approvalErrors[entry.approval.id] : void 0
2138
+ }, `l-${i}`));
2139
+ flushSysUpTo(baseEntries.length + i + 1);
2140
+ }
2141
+ while (sysIdx < systemBubbles.length) {
2142
+ const sb = systemBubbles[sysIdx];
2143
+ out.push(/* @__PURE__ */ jsx(Bubble, {
2144
+ entry: {
2145
+ type: "system",
2146
+ content: sb.content
2147
+ },
2148
+ markdownReady,
2149
+ onCommandApprovalDecision: decideCommandApproval
2150
+ }, `y-${sb.id}`));
2151
+ sysIdx++;
2152
+ }
2153
+ return out;
2154
+ })(),
2155
+ thinking && /* @__PURE__ */ jsxs("div", {
2156
+ className: "flex items-center gap-2 px-1 py-1 text-[0.85em] text-mocha-light",
2157
+ children: [
2158
+ /* @__PURE__ */ jsx(Dot, {}),
2159
+ /* @__PURE__ */ jsx(Dot, { delay: "0.15s" }),
2160
+ /* @__PURE__ */ jsx(Dot, { delay: "0.3s" }),
2161
+ /* @__PURE__ */ jsx("span", { children: "agent is thinking…" })
2162
+ ]
2163
+ })
2164
+ ]
2165
+ }),
2166
+ /* @__PURE__ */ jsx("p", {
2167
+ className: "sr-only",
2168
+ role: "status",
2169
+ "aria-live": "polite",
2170
+ "aria-atomic": "true",
2171
+ children: turnBusy ? `${agentName} is responding.` : "Conversation ready."
2172
+ }),
2173
+ editIdx !== null && /* @__PURE__ */ jsxs("div", {
2174
+ className: "flex items-center gap-2 border-t border-frost bg-sapphire-glow px-5 py-2 text-[0.85em] text-sapphire-deep",
2175
+ children: [/* @__PURE__ */ jsx("span", { children: "editing last message — submit to replace, or" }), /* @__PURE__ */ jsx("button", {
2176
+ type: "button",
2177
+ onClick: exitEditMode,
2178
+ className: "ml-auto rounded-sm border border-sapphire-light bg-transparent px-2 py-0.5 text-[0.92em] text-sapphire-deep hover:bg-snow",
2179
+ children: "cancel"
2180
+ })]
2181
+ }),
2182
+ attachments.length > 0 && /* @__PURE__ */ jsx("div", {
2183
+ className: "flex max-h-24 shrink-0 flex-wrap gap-2 overflow-y-auto border-t border-frost bg-ivory px-5 pt-3",
2184
+ children: attachments.map((a, i) => {
2185
+ const remove = () => setAttachments((prev) => prev.filter((_, j) => j !== i));
2186
+ return isImageMime(a.mimeType) ? /* @__PURE__ */ jsxs("div", {
2187
+ className: "relative",
2188
+ children: [/* @__PURE__ */ jsx("img", {
2189
+ src: `data:${a.mimeType};base64,${a.data}`,
2190
+ alt: "pending attachment",
2191
+ className: "h-16 w-16 rounded-md border border-frost object-cover"
2192
+ }), /* @__PURE__ */ jsx("button", {
2193
+ type: "button",
2194
+ onClick: remove,
2195
+ className: "absolute -right-1.5 -top-1.5 flex h-5 w-5 items-center justify-center rounded-full border border-frost bg-snow text-[0.7em] text-mocha hover:border-sapphire hover:text-sapphire",
2196
+ "aria-label": "remove attachment",
2197
+ children: "✕"
2198
+ })]
2199
+ }, `${a.mimeType}-${i}`) : /* @__PURE__ */ jsxs("div", {
2200
+ className: "flex items-center gap-1.5 rounded-md border border-frost bg-snow px-2 py-1 text-[0.82em] text-mocha",
2201
+ children: [
2202
+ /* @__PURE__ */ jsx("span", { children: "📄" }),
2203
+ /* @__PURE__ */ jsx("span", {
2204
+ className: "max-w-[160px] truncate",
2205
+ children: a.name ?? "file"
2206
+ }),
2207
+ /* @__PURE__ */ jsx("button", {
2208
+ type: "button",
2209
+ onClick: remove,
2210
+ className: "text-[0.85em] text-mocha hover:text-sapphire",
2211
+ "aria-label": "remove attachment",
2212
+ children: "✕"
2213
+ })
2214
+ ]
2215
+ }, `${a.name ?? "file"}-${i}`);
2216
+ })
2217
+ }),
2218
+ /* @__PURE__ */ jsx(UserQueuePanel, {
2219
+ onViewConversation: (id) => {
2220
+ setQueueConversation(id);
2221
+ setLibraryOpen(true);
2222
+ },
2223
+ ref: queueRef,
2224
+ agentId,
2225
+ selection: () => knownHeadRef.current.selection,
2226
+ onQueueMode: (mode) => {
2227
+ setQueueMode(mode);
2228
+ setQueueReady(true);
2229
+ }
2230
+ }, agentId),
2231
+ /* @__PURE__ */ jsx(AgentQuestions, { agentId }, `questions-${agentId}`),
2232
+ /* @__PURE__ */ jsxs("form", {
2233
+ className: "grid shrink-0 grid-cols-[auto_minmax(0,1fr)] items-end gap-2 border-t border-frost bg-ivory px-3 py-3 sm:grid-cols-[auto_minmax(0,1fr)_auto] sm:px-5",
2234
+ onSubmit: (e) => {
2235
+ e.preventDefault();
2236
+ send(input);
2237
+ },
2238
+ children: [
2239
+ /* @__PURE__ */ jsx("input", {
2240
+ ref: fileInputRef,
2241
+ type: "file",
2242
+ multiple: true,
2243
+ className: "hidden",
2244
+ onChange: (e) => {
2245
+ addFiles(e.target.files);
2246
+ e.target.value = "";
2247
+ }
2248
+ }),
2249
+ /* @__PURE__ */ jsx("button", {
2250
+ type: "button",
2251
+ onClick: () => fileInputRef.current?.click(),
2252
+ title: "attach images or files",
2253
+ "aria-label": "attach files",
2254
+ className: "flex h-11 items-center justify-center rounded-md border-[1.5px] border-frost bg-snow px-3 py-2 text-[1em] text-mocha transition-colors hover:border-sapphire hover:text-sapphire disabled:cursor-not-allowed disabled:opacity-50",
2255
+ children: "📎"
2256
+ }),
2257
+ /* @__PURE__ */ jsx("textarea", {
2258
+ ref: inputRef,
2259
+ rows: 1,
2260
+ value: input,
2261
+ onChange: (e) => setInput(e.target.value),
2262
+ onKeyDown,
2263
+ onPaste,
2264
+ placeholder: "Write a message…",
2265
+ title: "Shift+Enter for a new line. Paste or attach files with the paperclip.",
2266
+ autoComplete: "off",
2267
+ "aria-label": `Message ${agentName}`,
2268
+ className: "max-h-[200px] min-h-11 min-w-0 flex-1 resize-none overflow-y-auto rounded-md border-[1.5px] border-frost bg-snow px-3 py-2 text-[0.93em] leading-[1.45] text-chocolate outline-none transition-colors focus:border-sapphire focus:shadow-[0_0_0_3px_var(--color-sapphire-glow)]"
2269
+ }),
2270
+ /* @__PURE__ */ jsxs("div", {
2271
+ className: "col-span-2 flex flex-wrap justify-end gap-2 sm:col-span-1",
2272
+ children: [/* @__PURE__ */ jsx(Button, {
2273
+ variant: "primary",
2274
+ type: "submit",
2275
+ className: "min-h-11 whitespace-normal text-center",
2276
+ disabled: !input.trim() && attachments.length === 0,
2277
+ children: turnBusy || queueMode ? "Queue follow-up" : "Send"
2278
+ }), turnBusy && /* @__PURE__ */ jsx(Button, {
2279
+ variant: "danger",
2280
+ className: "min-h-11",
2281
+ onClick: () => void cancel(),
2282
+ "aria-label": "Stop response and pause queue",
2283
+ children: "Stop"
2284
+ })]
2285
+ })
2286
+ ]
2287
+ })
2288
+ ]
2289
+ });
2290
+ }
2291
+ function Bubble({ entry, markdownReady, isLastUser, isWillDrop, onEdit, onCommandApprovalDecision, commandApprovalBusy, commandApprovalError }) {
2292
+ const dropCls = isWillDrop ? "opacity-40 [&_.bubble-content]:line-through" : "";
2293
+ if (entry.type === "user") {
2294
+ if (entry.content.startsWith(INBOX_WAKE_PREFIX)) {
2295
+ const body = entry.content.slice(24);
2296
+ return /* @__PURE__ */ jsxs("details", {
2297
+ className: `my-3 rounded-lg border border-fawn bg-ivory px-3 py-2 text-sm text-mocha ${dropCls}`,
2298
+ "aria-label": "Teammate update",
2299
+ children: [
2300
+ /* @__PURE__ */ jsx("summary", {
2301
+ className: "cursor-pointer font-medium",
2302
+ children: inboxUpdateLabel(body)
2303
+ }),
2304
+ /* @__PURE__ */ jsx("p", {
2305
+ className: "mt-2 text-xs",
2306
+ children: "Original message and Agent instructions"
2307
+ }),
2308
+ /* @__PURE__ */ jsx("pre", {
2309
+ className: "mt-1 max-h-64 overflow-auto whitespace-pre-wrap break-words text-xs",
2310
+ children: body
2311
+ })
2312
+ ]
2313
+ });
2314
+ }
2315
+ return /* @__PURE__ */ jsxs("div", {
2316
+ className: `team relative my-4 flex flex-col items-end ${dropCls}`,
2317
+ children: [
2318
+ /* @__PURE__ */ jsx("span", {
2319
+ className: "sr-only",
2320
+ children: "you"
2321
+ }),
2322
+ entry.images && entry.images.length > 0 && /* @__PURE__ */ jsx("div", {
2323
+ className: "mb-1 flex max-w-[85%] flex-wrap justify-end gap-1",
2324
+ children: entry.images.map((img, i) => /* @__PURE__ */ jsx("img", {
2325
+ src: `data:${img.mimeType};base64,${img.data}`,
2326
+ alt: "attachment",
2327
+ className: "max-h-48 rounded-lg border border-sapphire-light"
2328
+ }, i))
2329
+ }),
2330
+ entry.files && entry.files.length > 0 && /* @__PURE__ */ jsx("div", {
2331
+ className: "mb-1 flex max-w-[85%] flex-wrap justify-end gap-1",
2332
+ children: entry.files.map((f, i) => /* @__PURE__ */ jsxs("span", {
2333
+ className: "flex items-center gap-1 rounded-md border border-sapphire-light bg-sapphire-glow px-2 py-1 text-[0.8em] text-chocolate",
2334
+ children: ["📄 ", /* @__PURE__ */ jsx("span", {
2335
+ className: "max-w-[180px] truncate",
2336
+ children: f.name
2337
+ })]
2338
+ }, i))
2339
+ }),
2340
+ entry.content && /* @__PURE__ */ jsx("div", {
2341
+ className: "bubble-content max-w-[85%] whitespace-pre-wrap break-words rounded-[14px_14px_4px_14px] border border-sapphire-light bg-sapphire-glow px-3 py-2 text-chocolate",
2342
+ children: entry.content
2343
+ }),
2344
+ isLastUser && onEdit && /* @__PURE__ */ jsx("button", {
2345
+ type: "button",
2346
+ onClick: onEdit,
2347
+ title: "edit and resend — replaces the last turn",
2348
+ className: "absolute left-1 top-1 rounded-sm border border-fawn bg-ivory px-2 py-0.5 text-[0.78em] font-medium text-mocha opacity-65 transition hover:border-sapphire hover:bg-sapphire-glow hover:text-sapphire hover:opacity-100 team-hover:opacity-100 team-focus-within:opacity-100",
2349
+ children: "edit"
2350
+ })
2351
+ ]
2352
+ });
2353
+ }
2354
+ if (entry.type === "assistant") {
2355
+ if (entry.content.startsWith(COMPACTION_REPLAY_PREFIX)) {
2356
+ const summary = entry.content.slice(22).trim();
2357
+ return /* @__PURE__ */ jsx("div", {
2358
+ className: `my-3 border-y border-dashed border-frost py-2 text-[0.85em] italic text-mocha-light ${dropCls}`,
2359
+ children: /* @__PURE__ */ jsxs("details", { children: [/* @__PURE__ */ jsx("summary", {
2360
+ className: "cursor-pointer select-none text-center [&::-webkit-details-marker]:hidden",
2361
+ children: "— conversation summary (click to expand) —"
2362
+ }), /* @__PURE__ */ jsx("div", {
2363
+ className: "md-content mt-2 rounded-sm bg-frost/40 p-2 not-italic",
2364
+ dangerouslySetInnerHTML: { __html: renderMd(summary, markdownReady) }
2365
+ })] })
2366
+ });
2367
+ }
2368
+ if (entry.content.startsWith("[error]") || entry.content.startsWith("[fatal]") || entry.content.startsWith("[network error]")) return /* @__PURE__ */ jsxs("div", {
2369
+ className: `my-4 flex flex-col items-start ${dropCls}`,
2370
+ children: [/* @__PURE__ */ jsx("span", {
2371
+ className: "mb-1 text-[0.72em] font-semibold uppercase tracking-wider text-danger",
2372
+ children: "error"
2373
+ }), /* @__PURE__ */ jsx("div", {
2374
+ className: "bubble-content rounded-r-sm border-l-[3px] border-l-danger bg-danger/10 py-1 pl-3 pr-2 leading-[1.55]",
2375
+ dangerouslySetInnerHTML: { __html: renderMd(entry.content, markdownReady) }
2376
+ })]
2377
+ });
2378
+ return /* @__PURE__ */ jsxs("div", {
2379
+ className: `my-4 flex flex-col items-start ${dropCls}`,
2380
+ children: [/* @__PURE__ */ jsx("span", {
2381
+ className: "sr-only",
2382
+ children: "agent"
2383
+ }), /* @__PURE__ */ jsx("div", {
2384
+ className: "md-content bubble-content w-full leading-[1.55] text-chocolate",
2385
+ dangerouslySetInnerHTML: { __html: renderMd(entry.content, markdownReady) }
2386
+ })]
2387
+ });
2388
+ }
2389
+ if (entry.type === "tool") return /* @__PURE__ */ jsx(ToolGroup, {
2390
+ items: entry.items,
2391
+ dropCls
2392
+ });
2393
+ if (entry.type === "images") return /* @__PURE__ */ jsx("div", {
2394
+ className: `my-2 space-y-2 ${dropCls}`,
2395
+ children: entry.images.map((img, i) => /* @__PURE__ */ jsx("img", {
2396
+ src: `data:${img.mimeType};base64,${img.data}`,
2397
+ alt: "screenshot",
2398
+ className: "block max-w-full rounded-lg border border-fawn"
2399
+ }, i))
2400
+ });
2401
+ if (entry.type === "result") return /* @__PURE__ */ jsx(ResultCard, { resultId: entry.resultId });
2402
+ if (entry.type === "file") {
2403
+ const isImage = entry.mimeType.startsWith("image/");
2404
+ const href = `data:${entry.mimeType};base64,${entry.data}`;
2405
+ return /* @__PURE__ */ jsxs("div", {
2406
+ className: `my-2 ${dropCls}`,
2407
+ children: [isImage && /* @__PURE__ */ jsx("img", {
2408
+ src: href,
2409
+ alt: entry.name,
2410
+ className: "mb-1 block max-w-full rounded-lg border border-fawn"
2411
+ }), /* @__PURE__ */ jsxs("a", {
2412
+ href,
2413
+ download: entry.name,
2414
+ className: "inline-flex items-center gap-2 rounded-md border border-fawn bg-ivory px-3 py-2 text-[0.88em] text-mocha hover:border-sapphire hover:text-sapphire",
2415
+ children: [
2416
+ /* @__PURE__ */ jsx("span", { children: "📄" }),
2417
+ /* @__PURE__ */ jsx("span", {
2418
+ className: "max-w-[260px] truncate",
2419
+ children: entry.name
2420
+ }),
2421
+ /* @__PURE__ */ jsx("span", {
2422
+ className: "text-[0.85em] opacity-70",
2423
+ children: "download"
2424
+ })
2425
+ ]
2426
+ })]
2427
+ });
2428
+ }
2429
+ if (entry.type === "command_approval") return /* @__PURE__ */ jsx(CommandApprovalCard, {
2430
+ approval: entry.approval,
2431
+ busy: commandApprovalBusy,
2432
+ error: commandApprovalError,
2433
+ onDecision: onCommandApprovalDecision
2434
+ });
2435
+ if (entry.type === "system") return /* @__PURE__ */ jsxs("div", {
2436
+ className: `my-3 rounded-r-sm border-l-[3px] border-sapphire bg-sapphire-glow px-3 py-1 font-mono text-[0.88em] text-sapphire-deep ${dropCls}`,
2437
+ children: [/* @__PURE__ */ jsx("span", {
2438
+ className: "mr-2 font-semibold uppercase tracking-wider opacity-80",
2439
+ children: "system"
2440
+ }), /* @__PURE__ */ jsx("span", {
2441
+ className: "whitespace-pre-wrap",
2442
+ children: entry.content
2443
+ })]
2444
+ });
2445
+ if (entry.type === "error") return /* @__PURE__ */ jsx("div", {
2446
+ className: `my-3 rounded-r-sm border-l-[3px] border-danger bg-danger/10 px-3 py-1 text-[0.92em] text-danger ${dropCls}`,
2447
+ children: entry.content
2448
+ });
2449
+ return null;
2450
+ }
2451
+ function CommandApprovalCard({ approval, busy = false, error, onDecision }) {
2452
+ const pending = approval.status === "pending";
2453
+ return /* @__PURE__ */ jsxs("section", {
2454
+ className: "my-3 rounded-lg border border-danger/50 bg-danger/5 p-4 text-chocolate",
2455
+ "aria-label": "Shell command approval",
2456
+ children: [
2457
+ /* @__PURE__ */ jsxs("div", {
2458
+ className: "flex flex-wrap items-start justify-between gap-2",
2459
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
2460
+ className: "m-0 text-[0.72em] font-semibold uppercase tracking-wider text-danger",
2461
+ children: "Shell command approval"
2462
+ }), /* @__PURE__ */ jsx("h2", {
2463
+ className: "m-0 mt-1 font-body text-[0.95em] font-semibold",
2464
+ children: pending ? "A risky command is waiting for you" : "Command decision recorded"
2465
+ })] }), /* @__PURE__ */ jsx("span", {
2466
+ className: "rounded-full border border-danger/30 px-2 py-0.5 text-[0.75em] font-semibold uppercase tracking-wide text-danger",
2467
+ children: approval.status.replaceAll("_", " ")
2468
+ })]
2469
+ }),
2470
+ /* @__PURE__ */ jsx("pre", {
2471
+ className: "mt-3 max-h-48 overflow-auto whitespace-pre-wrap break-words rounded-md border border-fawn bg-snow p-3 font-mono text-[0.82em] leading-[1.45] text-charcoal",
2472
+ children: approval.command
2473
+ }),
2474
+ /* @__PURE__ */ jsx("ul", {
2475
+ className: "mt-3 space-y-1.5 pl-5 text-[0.84em] leading-[1.4] text-mocha",
2476
+ children: approval.risks.map((risk) => /* @__PURE__ */ jsxs("li", { children: [
2477
+ /* @__PURE__ */ jsx("span", {
2478
+ className: "font-medium text-chocolate",
2479
+ children: risk.message
2480
+ }),
2481
+ " ",
2482
+ /* @__PURE__ */ jsx("code", {
2483
+ className: "text-[0.9em]",
2484
+ children: risk.code
2485
+ })
2486
+ ] }, `${risk.code}:${risk.span.start}:${risk.span.end}`))
2487
+ }),
2488
+ pending ? /* @__PURE__ */ jsxs(Fragment, { children: [
2489
+ /* @__PURE__ */ jsxs("p", {
2490
+ className: "mt-3 text-[0.78em] text-mocha-light",
2491
+ children: [
2492
+ "This approval expires",
2493
+ " ",
2494
+ /* @__PURE__ */ jsx("time", {
2495
+ dateTime: new Date(approval.expiresAt).toISOString(),
2496
+ children: new Date(approval.expiresAt).toLocaleString()
2497
+ }),
2498
+ ". Deny blocks only this command; cancel stops the whole turn."
2499
+ ]
2500
+ }),
2501
+ error && /* @__PURE__ */ jsx("p", {
2502
+ role: "alert",
2503
+ className: "mt-2 rounded-md bg-danger/10 px-3 py-2 text-[0.82em] text-danger",
2504
+ children: error
2505
+ }),
2506
+ onDecision && /* @__PURE__ */ jsxs("div", {
2507
+ className: "mt-3 flex flex-wrap gap-2",
2508
+ children: [/* @__PURE__ */ jsx(Button, {
2509
+ variant: "primary",
2510
+ disabled: busy,
2511
+ onClick: () => onDecision(approval, "deny"),
2512
+ children: "Deny"
2513
+ }), /* @__PURE__ */ jsx(Button, {
2514
+ variant: "danger",
2515
+ disabled: busy,
2516
+ onClick: () => onDecision(approval, "allow"),
2517
+ children: "Allow once"
2518
+ })]
2519
+ })
2520
+ ] }) : /* @__PURE__ */ jsx("p", {
2521
+ role: "status",
2522
+ className: "mt-3 text-[0.84em] font-medium text-mocha",
2523
+ children: commandApprovalStatusCopy(approval.status)
2524
+ })
2525
+ ]
2526
+ });
2527
+ }
2528
+ function commandApprovalStatusCopy(status) {
2529
+ switch (status) {
2530
+ case "pending": return "Waiting for a decision.";
2531
+ case "allowed": return "Allowed once — the command may run for this tool call only.";
2532
+ case "denied": return "Denied — the command was not run.";
2533
+ case "auto_denied": return "Automatically denied — no interactive approval path was available.";
2534
+ case "expired": return "Expired — the command was not run.";
2535
+ case "cancelled": return "Cancelled — the command was not run.";
2536
+ }
2537
+ }
2538
+ function commandApprovalStatusFromConflict(message) {
2539
+ if (!message.startsWith("shell approval already decided: ")) return null;
2540
+ const status = message.slice(32);
2541
+ switch (status) {
2542
+ case "allowed":
2543
+ case "denied":
2544
+ case "auto_denied":
2545
+ case "expired":
2546
+ case "cancelled": return status;
2547
+ default: return null;
2548
+ }
2549
+ }
2550
+ function ToolGroup({ items, dropCls }) {
2551
+ const codingNames = [
2552
+ "repository_context",
2553
+ "coding_command",
2554
+ "coding_environment",
2555
+ "coding_receipt",
2556
+ "send_message"
2557
+ ];
2558
+ const hasCoding = items.some((item) => codingNames.includes(item.name));
2559
+ const visibleItems = items.filter((item) => !(item.kind === "call" && codingNames.includes(item.name) && items.some((other) => other.id === item.id && other.kind !== "call")));
2560
+ const [expanded, setExpanded] = useState(false);
2561
+ const contentRef = useRef(null);
2562
+ const [overflows, setOverflows] = useState(false);
2563
+ useLayoutEffect(() => {
2564
+ const el = contentRef.current;
2565
+ if (!el) return;
2566
+ setOverflows(el.scrollHeight > 324);
2567
+ }, [items.length]);
2568
+ return /* @__PURE__ */ jsxs("div", {
2569
+ className: `my-1.5 rounded-r-sm border-l-[3px] border-fawn bg-ivory px-3 py-2 font-mono text-[0.82em] leading-[1.5] text-mocha-light ${dropCls}`,
2570
+ children: [/* @__PURE__ */ jsxs("div", {
2571
+ ref: contentRef,
2572
+ className: "relative overflow-hidden",
2573
+ style: { maxHeight: expanded || hasCoding ? "none" : `${TOOL_GROUP_MAX_HEIGHT_PX}px` },
2574
+ children: [visibleItems.map((it, i) => /* @__PURE__ */ jsx(ToolLine, { item: it }, i)), overflows && !expanded && !hasCoding && /* @__PURE__ */ jsx("div", {
2575
+ className: "pointer-events-none absolute inset-x-0 bottom-0 h-10",
2576
+ style: { background: "linear-gradient(to bottom, rgba(247,240,229,0), var(--color-ivory))" }
2577
+ })]
2578
+ }), overflows && !hasCoding && /* @__PURE__ */ jsx("button", {
2579
+ type: "button",
2580
+ onClick: () => setExpanded((v) => !v),
2581
+ className: "mt-2 rounded-sm border border-fawn bg-transparent px-2 py-0.5 text-[0.9em] text-mocha hover:border-sapphire hover:text-sapphire-deep",
2582
+ children: expanded ? "show less ↑" : "show more ↓"
2583
+ })]
2584
+ });
2585
+ }
2586
+ function ToolLine({ item }) {
2587
+ if (item.name === "send_message" && item.kind !== "error") return /* @__PURE__ */ jsxs("details", {
2588
+ className: "py-1 font-sans text-sm",
2589
+ children: [/* @__PURE__ */ jsx("summary", {
2590
+ className: "cursor-pointer",
2591
+ children: item.kind === "call" ? "Sending teammate message…" : item.body.startsWith("sent message ") ? "Teammate message sent" : "Teammate message status"
2592
+ }), /* @__PURE__ */ jsx("pre", {
2593
+ className: "mt-1 max-h-64 overflow-auto whitespace-pre-wrap break-words text-xs",
2594
+ children: item.body
2595
+ })]
2596
+ });
2597
+ if (item.kind !== "error" && [
2598
+ "repository_context",
2599
+ "coding_command",
2600
+ "coding_environment",
2601
+ "coding_receipt"
2602
+ ].includes(item.name)) return /* @__PURE__ */ jsx(CodingToolResult, {
2603
+ name: item.name,
2604
+ body: item.body,
2605
+ pending: item.kind === "call"
2606
+ });
2607
+ if (item.kind === "call") {
2608
+ const args = prettyArgs(item.body);
2609
+ const multiLine = args.includes("\n");
2610
+ return /* @__PURE__ */ jsxs("div", {
2611
+ className: "whitespace-pre-wrap break-words py-0.5",
2612
+ children: [
2613
+ /* @__PURE__ */ jsx("span", {
2614
+ className: "mr-1 opacity-45",
2615
+ children: "→"
2616
+ }),
2617
+ /* @__PURE__ */ jsx("span", {
2618
+ className: "font-medium text-mocha",
2619
+ children: item.name
2620
+ }),
2621
+ multiLine ? /* @__PURE__ */ jsx("pre", {
2622
+ className: "ml-4 mt-0.5 rounded-sm bg-[rgba(42,31,22,0.04)] px-2 py-1 font-mono text-[0.95em] leading-[1.4] text-mocha",
2623
+ children: args
2624
+ }) : args ? /* @__PURE__ */ jsxs("span", { children: [
2625
+ " (",
2626
+ args,
2627
+ ")"
2628
+ ] }) : null
2629
+ ]
2630
+ });
2631
+ }
2632
+ if (item.kind === "error") return /* @__PURE__ */ jsxs("div", {
2633
+ className: "whitespace-pre-wrap break-words py-0.5 text-rose-baziu",
2634
+ children: [/* @__PURE__ */ jsx("span", {
2635
+ className: "mr-1 opacity-45",
2636
+ children: "←"
2637
+ }), item.body]
2638
+ });
2639
+ return /* @__PURE__ */ jsxs("div", {
2640
+ className: "whitespace-pre-wrap break-words py-0.5",
2641
+ children: [/* @__PURE__ */ jsx("span", {
2642
+ className: "mr-1 opacity-45",
2643
+ children: "←"
2644
+ }), item.body]
2645
+ });
2646
+ }
2647
+ function Dot({ delay = "0s" }) {
2648
+ return /* @__PURE__ */ jsx("span", {
2649
+ className: "inline-block h-1.5 w-1.5 animate-pulse rounded-full bg-mocha-light/60",
2650
+ style: { animationDelay: delay },
2651
+ "aria-hidden": "true"
2652
+ });
2653
+ }
2654
+ /** Read-only projection used when opening the exact available source of a saved result. */
2655
+ function ResultTranscript({ messages }) {
2656
+ const markdownReady = useSyncExternalStore(subscribeToHydration, browserMarkdownReady, serverMarkdownReady);
2657
+ return /* @__PURE__ */ jsx("div", {
2658
+ "aria-label": "Source conversation",
2659
+ children: projectMessages(messages).map((entry, index) => /* @__PURE__ */ jsx(Bubble, {
2660
+ entry,
2661
+ markdownReady
2662
+ }, index))
2663
+ });
2664
+ }
2665
+ //#endregion
2666
+ export { ResultTranscript as n, ChatPane as t };