@bud.sh/app 0.5.0 → 0.6.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 (115) hide show
  1. package/README.md +41 -31
  2. package/dist/client/assets/bud/design.js +1 -1
  3. package/dist/client/assets/canvas-Bih9EdKJ.js +1 -0
  4. package/dist/client/assets/difference-worker-Bzg-rizy.js +8 -0
  5. package/dist/client/assets/dist-CdE5wBkg.js +5 -0
  6. package/dist/client/assets/files-1dLuoM2f.js +1 -0
  7. package/dist/client/assets/i18n-CkO6VFI0.js +1 -0
  8. package/dist/client/assets/index-Cn2mxroU.css +2 -0
  9. package/dist/client/assets/{index-ChANGtl-.js → index-DtISWZp9.js} +2 -2
  10. package/dist/client/assets/pages-BJ-tNJ4J.js +1 -0
  11. package/dist/client/assets/{project-branches-DO897zM6.js → project-branches-Ce3rXrkc.js} +1 -1
  12. package/dist/client/assets/project-comments-CPUONfs8.js +1 -0
  13. package/dist/client/assets/project-thread-links-C-TGkq59.js +1 -0
  14. package/dist/client/assets/{project-threads-Lay351p4.js → project-threads-Bv7IDBgi.js} +105 -105
  15. package/dist/client/assets/project-work-DAE9-olg.js +1 -0
  16. package/dist/client/assets/{task-design-Avb2RO4w.js → task-design-CwySIAOl.js} +3 -3
  17. package/dist/client/assets/tasks-KWsCLi_h.js +3 -0
  18. package/dist/client/assets/wasm-inline-DSUyFn1L.js +1 -0
  19. package/dist/client/assets/workspace-CVdcfaaB.js +2 -0
  20. package/dist/client/index.html +3 -3
  21. package/dist/hosted/assets/{canvas-V36732ER.js → canvas-AII16n9i.js} +97 -6
  22. package/dist/hosted/assets/{comment-data-CF2ZV_3k.js → comment-data-BXfMElfx.js} +2004 -402
  23. package/dist/hosted/assets/dist-BCSTeNf4.js +4785 -0
  24. package/dist/hosted/assets/{files-DjSCjfU7.js → files-D_1yeAVO.js} +10 -5
  25. package/dist/hosted/assets/{pages-BzjzeuPu.js → pages-CN_SbKWA.js} +3 -3
  26. package/dist/hosted/assets/{project-branches-BT97J9i7.js → project-branches-Bxp0DCwg.js} +1 -1
  27. package/dist/hosted/assets/{project-thread-links-BRLgByEe.js → project-thread-links-KqQoKBLE.js} +76 -4
  28. package/dist/{node-host/assets/screen-CZXWJEds.js → hosted/assets/screen-DpAcDUKR.js} +29 -1
  29. package/dist/hosted/assets/tasks-D66LlwxM.js +1423 -0
  30. package/dist/hosted/assets/wasm-inline-Dl9tqabx.js +4 -0
  31. package/dist/hosted/hosted.js +7885 -6219
  32. package/dist/node/assets/Stream-CAm1eJ2n.js +16604 -0
  33. package/dist/{server/assets/bridge-B_Blt55E.js → node/assets/bridge-DQSBYHmw.js} +124 -290
  34. package/dist/node/assets/config-9_jWwNBh.js +1067 -0
  35. package/dist/node/assets/load-config.server-CMVtRVmO.js +261 -0
  36. package/dist/node/assets/module-3GgtT4Xs.js +308 -0
  37. package/dist/node/assets/server-DTQFuoX2.js +846 -0
  38. package/dist/node/assets/src-D-kG1GxB.js +2 -0
  39. package/dist/node/assets/{src-CerrKjID.js → src-Dwe9APUx.js} +93 -3
  40. package/dist/node/assets/src-DysjfbM8.js +8592 -0
  41. package/dist/node/bridge.js +1 -845
  42. package/dist/node/cli.js +206 -0
  43. package/dist/node/design.js +811 -13
  44. package/dist/node/index.js +2 -739
  45. package/dist/node/plugins.js +5711 -0
  46. package/dist/node/server.js +1 -6746
  47. package/dist/node/vite.js +121 -0
  48. package/dist/{server/assets/canvas-Dc1mYCwQ.js → node-host/assets/canvas-BeDoufy1.js} +97 -7
  49. package/dist/node-host/assets/{comment-data-DyIZNpfG.js → comment-data-DE2T9yTj.js} +1980 -376
  50. package/dist/node-host/assets/dist-BCSTeNf4.js +4785 -0
  51. package/dist/node-host/assets/{files-Dh2TiQZq.js → files-Dl-PTS09.js} +11 -6
  52. package/dist/node-host/assets/{pages-BxpjbYhs.js → pages-DL2sk6_U.js} +3 -3
  53. package/dist/node-host/assets/{project-branches-B7HUGiVK.js → project-branches-DGp5ELct.js} +1 -1
  54. package/dist/{server/assets/project-thread-links-CBOpf6zk.js → node-host/assets/project-thread-links-CgBV3Jcm.js} +76 -5
  55. package/dist/{hosted/assets/screen-CKp2C7kx.js → node-host/assets/screen-B55yyVfb.js} +29 -1
  56. package/dist/node-host/assets/tasks-DZRNkoxe.js +1425 -0
  57. package/dist/node-host/assets/wasm-inline-Dl9tqabx.js +4 -0
  58. package/dist/node-host/node-host.js +6127 -4438
  59. package/dist/server/assets/{branch-previews-ClkoJ0lM.js → branch-previews-DedrpnVR.js} +1555 -673
  60. package/dist/{node-host/assets/canvas-TRUfxfL5.js → server/assets/canvas-CJWQ-jjv.js} +98 -6
  61. package/dist/server/assets/{comment-data-CsjukwAW.js → comment-data-Rg42ToTW.js} +1666 -47
  62. package/dist/server/assets/dist-BCSTeNf4.js +4785 -0
  63. package/dist/server/assets/{events-iHpLGr3k.js → events-BH_rAu7O.js} +155 -20
  64. package/dist/server/assets/{files-E9mSFeoc.js → files-CPJqKeMo.js} +12 -7
  65. package/dist/server/assets/{listener-DN5TyOjJ.js → listener-D61O6PzF.js} +708 -12
  66. package/dist/server/assets/{pages-BBBXyJWV.js → pages-KJY_Ba92.js} +4 -4
  67. package/dist/server/assets/{project-branches-B0YIK6Ur.js → project-branches-qG9ACZvo.js} +2 -2
  68. package/dist/{node-host/assets/project-thread-links-CauJF0Cz.js → server/assets/project-thread-links-CqK0lqCF.js} +77 -4
  69. package/dist/{node-host/assets/tasks-request-ZWmbc8fE.js → server/assets/project-work-DoIwxWMm.js} +87 -2
  70. package/dist/server/assets/run-BJ8r5jSh.js +572 -0
  71. package/dist/server/assets/{screen-BIQA2mr4.js → screen-CBrHPFug.js} +30 -2
  72. package/dist/server/assets/{server-DxK-UB_Z.js → server-Dkbp5stG.js} +1521 -793
  73. package/dist/server/assets/{services-BRrgXYlT.js → services-BTfig8A8.js} +234 -997
  74. package/dist/server/assets/tasks-Bgu5Lg9Y.js +1427 -0
  75. package/dist/server/assets/wasm-inline-Dl9tqabx.js +4 -0
  76. package/dist/server/capture.js +1 -2
  77. package/dist/server/migration.js +569 -12
  78. package/dist/server/runtime.js +7 -5
  79. package/dist/server/server.js +3 -3
  80. package/dist/types/assets/{config-DIC2cf_S.d.ts → config-CvcYmE2s.d.ts} +188 -50
  81. package/dist/types/assets/{contract-CAT-XFNq.d.ts → contract-B7yuuqSn.d.ts} +44 -2
  82. package/dist/types/assets/{items-CaVkByHk.d.ts → items-BQdaVksf.d.ts} +12 -1
  83. package/dist/types/assets/memory-state-BBW-FAVA.d.ts +236 -0
  84. package/dist/types/assets/picker-Be5Gx4WR.d.ts +9 -0
  85. package/dist/types/assets/{task-design-xJOhRNWh.d.ts → task-design-BDHKFHpp.d.ts} +3 -1
  86. package/dist/types/bridge.d.ts +1 -1
  87. package/dist/types/companion.d.ts +3 -3
  88. package/dist/types/design.d.ts +2 -2
  89. package/dist/types/hosted.d.ts +3 -3
  90. package/dist/types/index.d.ts +2 -2
  91. package/dist/types/node-host.d.ts +2 -2
  92. package/dist/types/plugins.d.ts +445 -0
  93. package/dist/types/screen.d.ts +2 -2
  94. package/dist/types/server.d.ts +2 -228
  95. package/dist/types/vite.d.ts +16 -0
  96. package/package.json +19 -3
  97. package/dist/client/assets/canvas-Cu6R6xxT.js +0 -1
  98. package/dist/client/assets/difference-worker-jfTSn-Tw.js +0 -8
  99. package/dist/client/assets/files-js6ElfkU.js +0 -1
  100. package/dist/client/assets/i18n-DxqFr2mi.js +0 -1
  101. package/dist/client/assets/index-CFFxP0gS.css +0 -2
  102. package/dist/client/assets/pages-6_lhiQmz.js +0 -1
  103. package/dist/client/assets/project-comments-DFFKVpia.js +0 -1
  104. package/dist/client/assets/project-thread-links-NLFWQRD2.js +0 -1
  105. package/dist/client/assets/project-work-du0Y-L5Y.js +0 -1
  106. package/dist/client/assets/tasks-CA-E8bbK.js +0 -1
  107. package/dist/client/assets/tasks-request-C55IeuiP.js +0 -1
  108. package/dist/client/assets/workspace-Cw5HvX5d.js +0 -2
  109. package/dist/hosted/assets/tasks-Dj7AyBJB.js +0 -649
  110. package/dist/hosted/assets/tasks-request-DZob623r.js +0 -113
  111. package/dist/node-host/assets/tasks-Bs1IOpeU.js +0 -651
  112. package/dist/server/assets/project-work-qSKFO5yj.js +0 -88
  113. package/dist/server/assets/tasks-CiXodsLN.js +0 -652
  114. package/dist/server/assets/tasks-request-DhwCzpRi.js +0 -115
  115. package/dist/types/assets/picker-D1Bw3ndx.d.ts +0 -14
@@ -0,0 +1,1423 @@
1
+ import { $ as Dialog, B as AgentBrowser, Ct as taskHref, Ht as CommentThreadDetail, In as taskRoutes, J as CommentThreadHeading, Mn as jsx, Nn as routes, On as Fragment, Q as FieldCardHeader, Qt as IconButton, S as ThreadTaskLinks, Si as parseWorkDetail, St as taskAgentHref, Ti as parseWorkMutationResult, Tt as taskRole, Ut as CommentThreadItem, V as TaskWorkspace, Wt as CommentComposer, Y as FieldCard, Z as FieldCardFooter, _i as parseSectionData, ar as predictWorkCreate, b as writeOptimistically, bt as agentViewOpen, cn as t, fn as ref, h as storedWorkspace, hn as on, ht as conversationLabel, in as writePreference, ir as predictWorkAction, ki as parseWorkSyncResult, l as projectStore, nn as toast, or as predictWorkUpdate, qt as Button, un as Text, w as ItemDiscussion, wt as taskLabel, x as writeRows, xt as selectedTask, y as plainRow, yt as readConversationLocation, z as TaskRunPanel } from "./comment-data-BXfMElfx.js";
2
+ import { a as requestTasks, i as TaskRequestError, o as parseTaskContext, r as projectWork, s as parseRunRecord } from "../hosted.js";
3
+ import { i as loadItemComments, n as createProjectComments, t as defineScreen } from "./screen-DpAcDUKR.js";
4
+ import { n as ProjectThreadLinks, r as ProjectTaskPicker } from "./project-thread-links-KqQoKBLE.js";
5
+ //#region app/client/factory-setup.tsx
6
+ /**
7
+ * Validates the factory's readiness from the server.
8
+ * @param value - The response body.
9
+ * @returns The readiness.
10
+ */
11
+ function parseFactoryState(value) {
12
+ let input = value;
13
+ if (!input || typeof input.enabled !== "boolean" || typeof input.ready !== "boolean" || typeof input.canAdd !== "boolean" || !(input.message === null || typeof input.message === "string") || !Array.isArray(input.problems) || input.problems.length > 10 || input.problems.some((problem) => !problem || typeof problem.value !== "string" || problem.reason !== "missing" && problem.reason !== "category")) throw new TypeError("Invalid factory state.");
14
+ let statuses = input.statuses;
15
+ let roles = [
16
+ "open",
17
+ "inProgress",
18
+ "needsApproval",
19
+ "readyToMerge",
20
+ "merged"
21
+ ];
22
+ return {
23
+ enabled: input.enabled,
24
+ ready: input.ready,
25
+ message: input.message,
26
+ problems: input.problems.map((problem) => ({
27
+ value: problem.value,
28
+ reason: problem.reason
29
+ })),
30
+ canAdd: input.canAdd,
31
+ statuses: statuses && roles.every((role) => typeof statuses[role] === "string") ? Object.fromEntries(roles.map((role) => [role, statuses[role]])) : null
32
+ };
33
+ }
34
+ async function request$1(url, token, signal, body) {
35
+ let response = await fetch(url, {
36
+ method: body === void 0 ? "GET" : "POST",
37
+ headers: {
38
+ "X-Bud-Token": token,
39
+ ...body === void 0 ? {} : { "Content-Type": "application/json" }
40
+ },
41
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
42
+ credentials: "same-origin",
43
+ redirect: "error",
44
+ cache: "no-store",
45
+ signal
46
+ });
47
+ let value = await response.json().catch(() => null);
48
+ if (!response.ok) throw new Error(value && typeof value === "object" && "error" in value && typeof value.error === "string" ? value.error : t("app.client.factorySetup.failed"));
49
+ return parseFactoryState(value);
50
+ }
51
+ var statuses = /* @__PURE__ */ new Map();
52
+ var listeners = /* @__PURE__ */ new Map();
53
+ /**
54
+ * Follows the factory's readiness as this page changes it, such as when its statuses are added.
55
+ * @param token - The capability.
56
+ * @param listener - Gets the new readiness.
57
+ * @param signal - Stops following.
58
+ */
59
+ function onFactoryStatus(token, listener, signal) {
60
+ let set = listeners.get(token) ?? /* @__PURE__ */ new Set();
61
+ listeners.set(token, set);
62
+ set.add(listener);
63
+ signal.addEventListener("abort", () => set.delete(listener), { once: true });
64
+ }
65
+ /**
66
+ * The factory's readiness, read once for the screens that show it.
67
+ * @param token - The capability.
68
+ * @param fresh - Read again, such as after adding the statuses.
69
+ * @returns The readiness.
70
+ */
71
+ function factoryStatus(token, fresh = false) {
72
+ let found = statuses.get(token);
73
+ if (!found || fresh) {
74
+ found = request$1(routes.factoryApi.status.href(), token, new AbortController().signal);
75
+ found.catch(() => statuses.delete(token));
76
+ statuses.set(token, found);
77
+ }
78
+ return found;
79
+ }
80
+ /**
81
+ * Above the task board while the factory is on but the task store lacks its statuses: what's
82
+ * missing, and Add factory statuses when the store can add them.
83
+ * @param handle - The capability and what to do once the statuses exist.
84
+ * @returns The notice, or nothing while the factory is off or ready.
85
+ */
86
+ function FactorySetup(handle) {
87
+ let state = null;
88
+ let adding = false;
89
+ function update() {
90
+ if (!handle.signal.aborted) handle.update();
91
+ }
92
+ factoryStatus(handle.props.token).then((value) => {
93
+ state = value;
94
+ update();
95
+ }, () => {});
96
+ async function add() {
97
+ adding = true;
98
+ update();
99
+ try {
100
+ state = await request$1(routes.factoryApi.addStatuses.href(), handle.props.token, handle.signal, {});
101
+ statuses.set(handle.props.token, Promise.resolve(state));
102
+ for (let listener of listeners.get(handle.props.token) ?? []) listener(state);
103
+ toast.success(t("app.client.factorySetup.added"));
104
+ handle.props.onAdded();
105
+ } catch (cause) {
106
+ toast.error(t("app.client.factorySetup.failed"), { description: cause instanceof Error ? cause.message : void 0 });
107
+ } finally {
108
+ adding = false;
109
+ update();
110
+ }
111
+ }
112
+ return () => {
113
+ if (!state?.enabled || state.ready) return null;
114
+ let names = (reason) => state.problems.filter((problem) => problem.reason === reason).map((problem) => taskLabel(problem.value)).join(", ");
115
+ return /* @__PURE__ */ jsx("div", {
116
+ class: "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2 border-b border-line bg-panel px-4 py-2",
117
+ role: "status",
118
+ "data-testid": "factory-setup",
119
+ children: [/* @__PURE__ */ jsx(Text, {
120
+ variant: "ui-caption",
121
+ class: "min-w-0 flex-1",
122
+ children: state.message ?? [names("missing") && t("app.client.factorySetup.missing", { statuses: names("missing") }), names("category") && t("app.client.factorySetup.otherKind", { statuses: names("category") })].filter(Boolean).join(" ")
123
+ }), state.canAdd ? /* @__PURE__ */ jsx(Button, {
124
+ variant: "secondary",
125
+ size: "small",
126
+ disabled: adding,
127
+ "data-testid": "factory-add-statuses",
128
+ mix: on("click", () => void add()),
129
+ children: t("app.client.factorySetup.add")
130
+ }) : null]
131
+ });
132
+ };
133
+ }
134
+ //#endregion
135
+ //#region app/client/factory-run.tsx
136
+ async function request(url, token, signal, post = false, body = {}) {
137
+ let response = await fetch(url, {
138
+ method: post ? "POST" : "GET",
139
+ headers: {
140
+ "X-Bud-Token": token,
141
+ ...post ? { "Content-Type": "application/json" } : {}
142
+ },
143
+ ...post ? { body: JSON.stringify(body) } : {},
144
+ credentials: "same-origin",
145
+ redirect: "error",
146
+ cache: "no-store",
147
+ signal
148
+ });
149
+ let value = await response.json().catch(() => null);
150
+ if (!response.ok) throw new Error(value && typeof value === "object" && "error" in value && typeof value.error === "string" ? value.error : t("app.client.factoryRun.failed"));
151
+ return value;
152
+ }
153
+ function runOf(value) {
154
+ return value?.run == null ? null : parseRunRecord(value.run);
155
+ }
156
+ var TICK = 1e4;
157
+ /**
158
+ * A task's factory run above its discussion, while the factory is on: reads the run again as the
159
+ * task changes, and carries out the panel's actions through the factory API.
160
+ * @param handle - The capability, the project, the task, and its revision and run state, which
161
+ * change with each run step.
162
+ * @returns The run panel, or nothing while the factory is off.
163
+ */
164
+ function FactoryRun(handle) {
165
+ let state = null;
166
+ let run;
167
+ let seen;
168
+ let reading = 0;
169
+ let ticking;
170
+ handle.signal.addEventListener("abort", () => clearInterval(ticking));
171
+ function update() {
172
+ if (!handle.signal.aborted) handle.update();
173
+ }
174
+ onFactoryStatus(handle.props.token, (value) => {
175
+ state = value;
176
+ update();
177
+ }, handle.signal);
178
+ factoryStatus(handle.props.token).then((value) => {
179
+ state = value;
180
+ update();
181
+ }, () => {});
182
+ async function read() {
183
+ let mine = ++reading;
184
+ try {
185
+ let value = runOf(await request(routes.factoryApi.run.href({ id: handle.props.itemId }), handle.props.token, handle.signal));
186
+ if (mine === reading) run = value;
187
+ } catch {
188
+ if (mine === reading && run === void 0) run = null;
189
+ }
190
+ update();
191
+ }
192
+ async function act(route, body = {}) {
193
+ try {
194
+ let value = runOf(await request(routes.factoryApi[route].href({ id: handle.props.itemId }), handle.props.token, handle.signal, true, body));
195
+ reading++;
196
+ run = value;
197
+ update();
198
+ } catch (cause) {
199
+ toast.error(t("app.client.factoryRun.refused"), { description: cause instanceof Error ? cause.message : void 0 });
200
+ read();
201
+ throw cause;
202
+ }
203
+ }
204
+ async function preview() {
205
+ let tab = window.open("", "_blank");
206
+ try {
207
+ let url = ((await request(routes.factoryApi.preview.href({ id: handle.props.itemId }), handle.props.token, handle.signal))?.preview)?.url;
208
+ if (typeof url !== "string" || !/^https?:\/\//.test(url)) throw new Error(t("app.client.factoryRun.noPreview"));
209
+ if (tab) {
210
+ tab.opener = null;
211
+ tab.location.href = url;
212
+ } else window.open(url, "_blank", "noopener");
213
+ } catch (cause) {
214
+ tab?.close();
215
+ toast.error(t("app.client.factoryRun.previewFailed"), { description: cause instanceof Error ? cause.message : void 0 });
216
+ }
217
+ }
218
+ return () => {
219
+ let marker = `${handle.props.revision}:${handle.props.runState ?? ""}`;
220
+ if (marker !== seen) {
221
+ seen = marker;
222
+ handle.queueTask(() => void read());
223
+ }
224
+ if (!state?.enabled) return null;
225
+ let busy = run && [
226
+ "provisioning",
227
+ "working",
228
+ "changes",
229
+ "approved",
230
+ "pushing"
231
+ ].includes(run.state);
232
+ if (busy && !ticking) ticking = setInterval(update, TICK);
233
+ else if (!busy && ticking) {
234
+ clearInterval(ticking);
235
+ ticking = void 0;
236
+ }
237
+ return /* @__PURE__ */ jsx(TaskRunPanel, {
238
+ run,
239
+ blocked: state.ready ? null : state.message ?? t("app.client.factoryRun.statusesMissing"),
240
+ preview: Boolean(run?.ports.dev),
241
+ onStart: () => act("start"),
242
+ onChanges: (message) => act("changes", { message }),
243
+ onAnswer: (message) => act("answer", { message }),
244
+ onApprove: () => act("approve"),
245
+ onDiscard: () => act("discard"),
246
+ onRetry: () => act("retry"),
247
+ onPreview: preview,
248
+ onCanvas: () => {
249
+ writePreference(`bud:canvas-branch:${handle.props.projectId}`, run.branch);
250
+ window.location.assign("/canvas");
251
+ }
252
+ });
253
+ };
254
+ }
255
+ /** Run states a hand move asks about: the run would go on although the task moved away. */
256
+ var DISCARDABLE_RUN_STATES = [
257
+ "provisioning",
258
+ "working",
259
+ "changes",
260
+ "review",
261
+ "needs_context",
262
+ "failed"
263
+ ];
264
+ /** Run states a hand move waits for: the branch is being pushed. */
265
+ var PUSHING_RUN_STATES = ["approved", "pushing"];
266
+ /**
267
+ * Discards a task's run, for a person moving the task away from the run's status.
268
+ * @param token - The capability.
269
+ * @param itemId - The task.
270
+ * @param signal - Stops the request.
271
+ */
272
+ async function discardRun(token, itemId, signal) {
273
+ await request(routes.factoryApi.discard.href({ id: itemId }), token, signal, true);
274
+ }
275
+ /**
276
+ * Asks whether to discard a task's run before moving the task by hand.
277
+ * @param handle - The task, the status it moves to, and the answer.
278
+ * @returns The dialog's render function.
279
+ */
280
+ function RunMoveDialog(handle) {
281
+ let busy = false;
282
+ async function answer(discard) {
283
+ busy = true;
284
+ handle.update();
285
+ try {
286
+ await handle.props.onAnswer(discard);
287
+ } finally {
288
+ busy = false;
289
+ if (!handle.signal.aborted) handle.update();
290
+ }
291
+ }
292
+ return () => /* @__PURE__ */ jsx(Dialog, {
293
+ label: t("app.client.factoryRun.moveTitle"),
294
+ dismissible: !busy,
295
+ initialFocus: "[data-run-move-keep]",
296
+ onClose: () => void answer(false),
297
+ children: /* @__PURE__ */ jsx(FieldCard, { children: [/* @__PURE__ */ jsx(FieldCardHeader, {
298
+ title: t("app.client.factoryRun.moveTitle"),
299
+ subtitle: t("app.client.factoryRun.moveQuestion", {
300
+ id: handle.props.itemId,
301
+ status: handle.props.status
302
+ })
303
+ }), /* @__PURE__ */ jsx(FieldCardFooter, {
304
+ class: "justify-end",
305
+ children: [/* @__PURE__ */ jsx(Button, {
306
+ variant: "ghost",
307
+ size: "small",
308
+ disabled: busy,
309
+ "data-run-move-keep": true,
310
+ mix: on("click", () => void answer(false)),
311
+ children: t("app.client.factoryRun.keepRun")
312
+ }), /* @__PURE__ */ jsx(Button, {
313
+ variant: "primary",
314
+ size: "small",
315
+ disabled: busy,
316
+ "data-testid": "run-move-discard",
317
+ mix: on("click", () => void answer(true)),
318
+ children: t("app.client.factoryRun.discardAndMove")
319
+ })]
320
+ })] })
321
+ });
322
+ }
323
+ //#endregion
324
+ //#region app/client/agent-panel.tsx
325
+ var TOUCH = 12e4;
326
+ var SANDBOXED = [
327
+ "working",
328
+ "changes",
329
+ "review",
330
+ "pushing",
331
+ "approved",
332
+ "failed"
333
+ ];
334
+ async function call(url, token, signal, body) {
335
+ let response = await fetch(url, {
336
+ method: body === void 0 ? "GET" : "POST",
337
+ headers: {
338
+ "X-Bud-Token": token,
339
+ ...body === void 0 ? {} : { "Content-Type": "application/json" }
340
+ },
341
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
342
+ credentials: "same-origin",
343
+ redirect: "error",
344
+ cache: "no-store",
345
+ signal
346
+ });
347
+ let value = await response.json().catch(() => null);
348
+ if (!response.ok) throw new Error(value && typeof value === "object" && "error" in value && typeof value.error === "string" ? value.error : t("app.client.agentPanel.failed"));
349
+ return value;
350
+ }
351
+ /**
352
+ * What a comment on the preview asks of the agent: the comment, and where on the page it is.
353
+ * @param comment - The comment and its element.
354
+ * @returns The message for Request changes.
355
+ */
356
+ function previewChange(comment) {
357
+ let element = comment.element;
358
+ let where = [`On ${comment.path}`];
359
+ if (element) {
360
+ where.push(`at ${element.label || element.breadcrumb || element.selector} <${element.tagName}>`);
361
+ if (element.source?.file) where.push(`(${element.source.file}${element.source.line ? `:${element.source.line}` : ""}${element.source.component ? `, ${element.source.component}` : ""})`);
362
+ }
363
+ return `${comment.body.trim()}\n\n${where.join(" ")}.`;
364
+ }
365
+ /**
366
+ * Agent view for a task with a run: its page beside the run's live preview and the terminal,
367
+ * through the factory API. Opening it wakes a sleeping sandbox, and it keeps the sandbox up
368
+ * while open. The terminal's sessions are brokered by Bud, so nothing about the sandbox reaches
369
+ * this page.
370
+ * @param handle - The capability, the task, its run's state and its page.
371
+ * @returns The view's render function.
372
+ */
373
+ function AgentPanel(handle) {
374
+ let run;
375
+ let preview = { state: "none" };
376
+ let seen;
377
+ let reading = 0;
378
+ let update = () => {
379
+ if (!handle.signal.aborted) handle.update();
380
+ };
381
+ let touching = setInterval(() => {
382
+ if (run && SANDBOXED.includes(run.state)) call(routes.factoryApi.touch.href({ id: handle.props.itemId }), handle.props.token, handle.signal, {}).catch(() => {});
383
+ }, TOUCH);
384
+ handle.signal.addEventListener("abort", () => clearInterval(touching));
385
+ async function showPreview() {
386
+ preview = { state: "waking" };
387
+ update();
388
+ try {
389
+ let url = ((await call(routes.factoryApi.preview.href({ id: handle.props.itemId }), handle.props.token, handle.signal))?.preview)?.url;
390
+ preview = typeof url === "string" && /^https?:\/\//.test(url) ? {
391
+ state: "ready",
392
+ url
393
+ } : { state: "none" };
394
+ } catch (cause) {
395
+ if (handle.signal.aborted) return;
396
+ preview = {
397
+ state: "failed",
398
+ message: cause instanceof Error ? cause.message : t("app.client.agentPanel.failed")
399
+ };
400
+ }
401
+ update();
402
+ }
403
+ async function read() {
404
+ let mine = ++reading;
405
+ try {
406
+ let value = await call(routes.factoryApi.run.href({ id: handle.props.itemId }), handle.props.token, handle.signal);
407
+ if (mine !== reading) return;
408
+ let previous = run;
409
+ run = value?.run == null ? null : parseRunRecord(value.run);
410
+ if (!Boolean(run?.sandbox && run.ports.dev && SANDBOXED.includes(run.state))) preview = { state: "none" };
411
+ else if (preview.state === "none" || previous?.sandbox !== run?.sandbox) showPreview();
412
+ } catch {
413
+ if (mine === reading && run === void 0) run = null;
414
+ }
415
+ update();
416
+ }
417
+ let connect = (tab, size, output, signal) => {
418
+ let offset = 0;
419
+ let cols = size.cols;
420
+ let rows = size.rows;
421
+ let applied = null;
422
+ let sending = Promise.resolve();
423
+ let post = (path, body) => {
424
+ sending = sending.then(() => call(path, handle.props.token, signal, body)).then(() => {}, () => {});
425
+ };
426
+ let encoder = new TextEncoder();
427
+ let resize = () => {
428
+ applied = {
429
+ cols,
430
+ rows
431
+ };
432
+ post(routes.factoryApi.terminalResize.href({
433
+ id: handle.props.itemId,
434
+ session: tab
435
+ }), {
436
+ cols,
437
+ rows
438
+ });
439
+ };
440
+ (async () => {
441
+ while (!signal.aborted) {
442
+ output.state("connecting");
443
+ try {
444
+ let url = new URL(routes.factoryApi.terminal.href({
445
+ id: handle.props.itemId,
446
+ session: tab
447
+ }), window.location.origin);
448
+ let asked = {
449
+ cols,
450
+ rows
451
+ };
452
+ url.searchParams.set("cols", String(cols));
453
+ url.searchParams.set("rows", String(rows));
454
+ url.searchParams.set("from", String(offset));
455
+ let response = await fetch(url, {
456
+ headers: { "X-Bud-Token": handle.props.token },
457
+ credentials: "same-origin",
458
+ cache: "no-store",
459
+ signal
460
+ });
461
+ if (!response.ok || !response.body) {
462
+ let value = await response.json().catch(() => null);
463
+ let message = typeof value?.error === "string" ? value.error : t("app.client.agentPanel.failed");
464
+ output.write(encoder.encode(`\r\n\x1b[2m${message}\x1b[0m\r\n`));
465
+ output.state("closed");
466
+ if (response.status === 404) return;
467
+ } else {
468
+ output.state("open");
469
+ applied = asked;
470
+ if (cols !== asked.cols || rows !== asked.rows) resize();
471
+ let reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
472
+ let buffer = "";
473
+ let ended = false;
474
+ while (!ended) {
475
+ let { done, value } = await reader.read();
476
+ if (done) break;
477
+ buffer += value;
478
+ let blocks = buffer.split("\n\n");
479
+ buffer = blocks.pop() ?? "";
480
+ for (let block of blocks) {
481
+ let id = /^id: (\d+)$/m.exec(block)?.[1];
482
+ let data = /^data: (.*)$/m.exec(block)?.[1];
483
+ if (/^event: exit$/m.test(block)) ended = true;
484
+ else if (id && data) {
485
+ output.write(Uint8Array.from(atob(data), (character) => character.charCodeAt(0)));
486
+ offset = Number(id);
487
+ }
488
+ }
489
+ }
490
+ output.state("closed");
491
+ applied = null;
492
+ if (ended) {
493
+ output.write(encoder.encode(`\r\n\x1b[2m${t("app.client.agentPanel.ended")}\x1b[0m\r\n`));
494
+ offset = 0;
495
+ }
496
+ }
497
+ } catch {
498
+ if (signal.aborted) return;
499
+ output.state("closed");
500
+ applied = null;
501
+ }
502
+ await new Promise((resolve) => setTimeout(resolve, 2e3));
503
+ }
504
+ })();
505
+ return {
506
+ input: (data) => post(routes.factoryApi.terminalInput.href({
507
+ id: handle.props.itemId,
508
+ session: tab
509
+ }), { data }),
510
+ resize: (nextCols, nextRows) => {
511
+ cols = nextCols;
512
+ rows = nextRows;
513
+ if (applied) resize();
514
+ }
515
+ };
516
+ };
517
+ async function comment(change) {
518
+ try {
519
+ let value = await call(routes.factoryApi.changes.href({ id: handle.props.itemId }), handle.props.token, handle.signal, { message: previewChange(change) });
520
+ run = value?.run == null ? run : parseRunRecord(value.run);
521
+ toast.success(t("app.client.agentPanel.changesSent"));
522
+ update();
523
+ } catch (cause) {
524
+ toast.error(t("app.client.agentPanel.changesRefused"), { description: cause instanceof Error ? cause.message : void 0 });
525
+ throw cause;
526
+ }
527
+ }
528
+ return () => {
529
+ let marker = handle.props.runState ?? "";
530
+ if (marker !== seen) {
531
+ seen = marker;
532
+ handle.queueTask(() => void read());
533
+ }
534
+ let terminal = run?.sandbox && run.ports.terminal && SANDBOXED.includes(run.state) ? {
535
+ tabs: [
536
+ {
537
+ id: "agent",
538
+ label: t("app.client.agentPanel.agent")
539
+ },
540
+ {
541
+ id: "dev",
542
+ label: t("app.client.agentPanel.devServer")
543
+ },
544
+ {
545
+ id: "shell",
546
+ label: t("app.client.agentPanel.shell")
547
+ }
548
+ ],
549
+ connect
550
+ } : null;
551
+ return /* @__PURE__ */ jsx(AgentBrowser, {
552
+ details: handle.props.details,
553
+ preview,
554
+ terminal,
555
+ onWake: () => void showPreview(),
556
+ onComment: run?.state === "review" ? comment : void 0
557
+ });
558
+ };
559
+ }
560
+ //#endregion
561
+ //#region app/client/task-context-block.tsx
562
+ async function get(url, token, signal) {
563
+ let response = await fetch(url, {
564
+ headers: { "X-Bud-Token": token },
565
+ credentials: "same-origin",
566
+ redirect: "error",
567
+ signal
568
+ });
569
+ if (!response.ok) throw new Error(String(response.status));
570
+ return response;
571
+ }
572
+ /**
573
+ * Where a task made from a discussion came from: the screenshot around the comment, the page,
574
+ * viewport and branch, and the element with its source, or a code comment's lines.
575
+ * @param handle - The capability and the task.
576
+ * @returns The block, or nothing for a task without context.
577
+ */
578
+ function TaskContextBlock(handle) {
579
+ let context = null;
580
+ let image = null;
581
+ let update = () => {
582
+ if (!handle.signal.aborted) handle.update();
583
+ };
584
+ handle.signal.addEventListener("abort", () => {
585
+ if (image) URL.revokeObjectURL(image);
586
+ });
587
+ (async () => {
588
+ try {
589
+ let body = await (await get(routes.tasksApi.context.href({ id: handle.props.itemId }), handle.props.token, handle.signal)).json();
590
+ context = parseTaskContext(body.context);
591
+ update();
592
+ if (context?.image) {
593
+ let picture = await get(routes.tasksApi.attachment.href({ hash: context.image }), handle.props.token, handle.signal);
594
+ image = URL.createObjectURL(await picture.blob());
595
+ update();
596
+ }
597
+ } catch {}
598
+ })();
599
+ return () => {
600
+ if (!context) return null;
601
+ let { page, element, code, viewport, branch } = context;
602
+ let source = element?.source;
603
+ let rows = [];
604
+ if (page) rows.push([t("app.client.taskContextBlock.page"), page.title === page.path ? page.path : `${page.title} (${page.path})`]);
605
+ if (viewport) rows.push([t("app.client.taskContextBlock.viewport"), `${viewport} px`]);
606
+ if (branch) rows.push([t("app.client.taskContextBlock.branch"), branch]);
607
+ if (element) rows.push([t("app.client.taskContextBlock.element"), `${element.label || element.breadcrumb} <${element.tagName}>`]);
608
+ if (source?.file) rows.push([t("app.client.taskContextBlock.source"), `${source.file}${source.line ? `:${source.line}` : ""}${source.component ? ` (${source.component})` : ""}`]);
609
+ if (code) rows.push([t("app.client.taskContextBlock.code"), `${code.path}:${code.startLine}–${code.endLine}${code.commit ? ` @ ${code.commit.slice(0, 8)}` : ""}`]);
610
+ return /* @__PURE__ */ jsx("section", {
611
+ class: "flex min-w-0 flex-col gap-3",
612
+ "data-testid": "task-context",
613
+ children: [
614
+ /* @__PURE__ */ jsx(Text, {
615
+ as: "h3",
616
+ variant: "ui",
617
+ weight: "medium",
618
+ children: t("app.client.taskContextBlock.context")
619
+ }),
620
+ image ? /* @__PURE__ */ jsx("a", {
621
+ href: image,
622
+ target: "_blank",
623
+ rel: "noopener",
624
+ class: "block overflow-hidden rounded-lg ring-1 ring-[var(--bud-card-border)]",
625
+ mix: on("click", (event) => {
626
+ event.preventDefault();
627
+ window.open(image, "_blank", "noopener");
628
+ }),
629
+ children: /* @__PURE__ */ jsx("img", {
630
+ src: image,
631
+ alt: t("app.client.taskContextBlock.screenshot"),
632
+ class: "block h-auto max-h-80 w-full object-cover",
633
+ "data-testid": "task-context-image"
634
+ })
635
+ }) : null,
636
+ rows.length ? /* @__PURE__ */ jsx("dl", {
637
+ class: "grid min-w-0 grid-cols-[auto_minmax(0,1fr)] gap-x-4 gap-y-1",
638
+ children: rows.map(([label, value]) => /* @__PURE__ */ jsx(Fragment, { children: [/* @__PURE__ */ jsx("dt", { children: /* @__PURE__ */ jsx(Text, {
639
+ variant: "ui-caption",
640
+ tone: "muted",
641
+ children: label
642
+ }) }), /* @__PURE__ */ jsx("dd", {
643
+ class: "min-w-0",
644
+ children: /* @__PURE__ */ jsx(Text, {
645
+ variant: "ui-caption",
646
+ class: "[overflow-wrap:anywhere]",
647
+ children: value
648
+ })
649
+ })] }))
650
+ }) : null
651
+ ]
652
+ });
653
+ };
654
+ }
655
+ //#endregion
656
+ //#region app/client/task-discussion.tsx
657
+ /**
658
+ * Owns a task's filtered discussions and lazy detail requests within its mounted lifetime.
659
+ * @param handle - The trusted project capability and currently displayed work-item ID.
660
+ * @returns The discussion block supplied to the task design.
661
+ */
662
+ function TaskDiscussion(handle) {
663
+ let requested = handle.props.conversationId;
664
+ let selected = requested;
665
+ let explicit = Boolean(requested);
666
+ let creating = false;
667
+ let picking = false;
668
+ let draftItemIds = [handle.props.itemId];
669
+ let comments = createProjectComments({
670
+ store: projectStore(handle.props.project),
671
+ token: handle.props.project.token,
672
+ signal: handle.signal,
673
+ filter: { itemId: handle.props.itemId },
674
+ initial: handle.props.initial,
675
+ onChange() {
676
+ if (!handle.signal.aborted) handle.update();
677
+ }
678
+ });
679
+ if (comments.loaded) selected ??= comments.snapshot.threads[0]?.id;
680
+ let mount = ref(() => {
681
+ if (comments.loaded) return;
682
+ comments.load().then(() => {
683
+ selected ??= comments.snapshot.threads[0]?.id;
684
+ if (!handle.signal.aborted) handle.update();
685
+ });
686
+ });
687
+ async function write(change) {
688
+ let result = await comments.write(change);
689
+ await handle.props.onChange?.();
690
+ return result;
691
+ }
692
+ function threadLinks(thread) {
693
+ return {
694
+ project: handle.props.project,
695
+ thread,
696
+ items: handle.props.items,
697
+ canLink: comments.snapshot.capabilities.link,
698
+ busy: comments.busy,
699
+ onChange: async () => {
700
+ await comments.load();
701
+ if (comments.snapshot.threads.some((item) => item.id === thread.id)) await comments.loadThread(thread.id, true);
702
+ await handle.props.onChange?.();
703
+ }
704
+ };
705
+ }
706
+ function links(thread) {
707
+ return /* @__PURE__ */ jsx("div", {
708
+ class: "flex items-start justify-between gap-2",
709
+ children: [handle.props.renderThreadLinks ? handle.props.renderThreadLinks(thread) : /* @__PURE__ */ jsx(ProjectThreadLinks, {
710
+ part: thread.anchor ? "links" : void 0,
711
+ ...threadLinks(thread)
712
+ }), !thread.anchor && comments.snapshot.capabilities.reply && comments.snapshot.threads.length > 1 ? /* @__PURE__ */ jsx(Button, {
713
+ variant: "ghost",
714
+ size: "small",
715
+ disabled: comments.busy,
716
+ "aria-pressed": !creating && selected === thread.id,
717
+ mix: on("click", () => {
718
+ selected = thread.id;
719
+ explicit = true;
720
+ creating = false;
721
+ handle.update();
722
+ }),
723
+ children: t("app.client.taskDiscussion.reply")
724
+ }) : null]
725
+ });
726
+ }
727
+ function heading(thread) {
728
+ return /* @__PURE__ */ jsx(CommentThreadHeading, {
729
+ location: thread.anchor ? conversationLabel(thread) : t("app.client.taskDiscussion.discussion"),
730
+ actions: handle.props.renderThreadLinks ? void 0 : /* @__PURE__ */ jsx(ProjectThreadLinks, {
731
+ part: "action",
732
+ ...threadLinks(thread)
733
+ }),
734
+ status: thread.status,
735
+ busy: comments.busy,
736
+ onStatusChange: comments.snapshot.capabilities.status ? (status) => {
737
+ write({
738
+ id: thread.id,
739
+ status
740
+ }).catch(() => {});
741
+ } : void 0
742
+ });
743
+ }
744
+ return () => {
745
+ if (requested !== handle.props.conversationId) {
746
+ requested = handle.props.conversationId;
747
+ explicit = Boolean(requested);
748
+ if (requested) {
749
+ selected = requested;
750
+ creating = false;
751
+ }
752
+ }
753
+ let { threads, capabilities } = comments.snapshot;
754
+ let reply = creating ? void 0 : threads.find((thread) => thread.id === selected && !thread.anchor) ?? (explicit ? void 0 : threads.find((thread) => !thread.anchor && thread.status !== "resolved") ?? (!capabilities.create ? threads.find((thread) => !thread.anchor) : void 0));
755
+ let replyPending = !creating && (explicit && (!reply || !capabilities.reply) || Boolean(reply && !reply.messagesLoaded));
756
+ if (comments.ready && !comments.busy && !comments.loading) handle.queueTask(() => {
757
+ if (explicit && selected) comments.loadThread(selected);
758
+ for (let thread of comments.snapshot.threads) if (!thread.messagesLoaded && (!thread.anchor || thread.id === selected)) comments.loadThread(thread.id);
759
+ });
760
+ let count = threads.reduce((sum, thread) => sum + thread.messageCount, 0);
761
+ return /* @__PURE__ */ jsx("section", {
762
+ class: "flex min-w-0 flex-col gap-3",
763
+ "aria-label": t("app.client.taskDiscussion.taskDiscussion"),
764
+ mix: mount,
765
+ children: [
766
+ /* @__PURE__ */ jsx("div", {
767
+ class: "flex items-center gap-2",
768
+ children: [
769
+ /* @__PURE__ */ jsx(Text, {
770
+ as: "h3",
771
+ variant: "ui",
772
+ weight: "medium",
773
+ children: t("app.client.taskDiscussion.discussion")
774
+ }),
775
+ count ? /* @__PURE__ */ jsx(Text, {
776
+ variant: "ui-caption",
777
+ tone: "muted",
778
+ children: count
779
+ }) : null,
780
+ comments.ready && capabilities.create && threads.length ? /* @__PURE__ */ jsx(Button, {
781
+ variant: "ghost",
782
+ size: "small",
783
+ class: "ms-auto",
784
+ disabled: comments.busy || creating,
785
+ mix: on("click", () => {
786
+ creating = true;
787
+ handle.update();
788
+ }),
789
+ children: t("app.client.taskDiscussion.newDiscussion")
790
+ }) : null
791
+ ]
792
+ }),
793
+ comments.error ? /* @__PURE__ */ jsx("div", {
794
+ class: "flex flex-col items-start gap-2",
795
+ children: [/* @__PURE__ */ jsx(Text, {
796
+ variant: "ui-caption",
797
+ role: "alert",
798
+ children: comments.error
799
+ }), /* @__PURE__ */ jsx(Button, {
800
+ variant: "secondary",
801
+ size: "small",
802
+ mix: on("click", () => {
803
+ comments.load().then(() => selected ? comments.loadThread(selected, true) : void 0);
804
+ }),
805
+ children: t("app.client.taskDiscussion.reloadComments")
806
+ })]
807
+ }) : null,
808
+ !comments.loaded ? /* @__PURE__ */ jsx(Text, {
809
+ variant: "ui-caption",
810
+ tone: "muted",
811
+ role: "status",
812
+ children: t("app.client.taskDiscussion.loadingDiscussion")
813
+ }) : !comments.ready ? /* @__PURE__ */ jsx(Text, {
814
+ variant: "ui-caption",
815
+ tone: "muted",
816
+ children: t("app.client.taskDiscussion.commentsUnavailable")
817
+ }) : /* @__PURE__ */ jsx(Fragment, { children: [threads.length ? threads.map((thread) => !thread.anchor ? /* @__PURE__ */ jsx(ItemDiscussion, {
818
+ thread,
819
+ links: links(thread)
820
+ }, thread.id) : /* @__PURE__ */ jsx(CommentThreadItem, {
821
+ id: thread.id,
822
+ expanded: selected === thread.id,
823
+ heading: heading(thread),
824
+ messages: thread.messages,
825
+ messageCount: thread.messageCount,
826
+ now: comments.now,
827
+ onExpand: () => {
828
+ selected = thread.id;
829
+ explicit = false;
830
+ comments.loadThread(thread.id);
831
+ handle.update();
832
+ },
833
+ renderDetail: () => /* @__PURE__ */ jsx(CommentThreadDetail, {
834
+ heading: heading(thread),
835
+ messages: thread.messages,
836
+ messageCount: thread.messageCount,
837
+ messagesLoaded: thread.messagesLoaded,
838
+ messagesTruncated: thread.messagesTruncated,
839
+ now: comments.now,
840
+ busy: comments.busy,
841
+ links: links(thread),
842
+ onReply: capabilities.reply ? (body, parentId) => write({
843
+ id: thread.id,
844
+ body,
845
+ parentId
846
+ }) : void 0,
847
+ viewerId: comments.viewer?.id,
848
+ onDelete: capabilities.delete ? async (messageId) => {
849
+ let whole = thread.messages[0]?.id === messageId;
850
+ await comments.remove(thread.id, messageId);
851
+ if (whole && selected === thread.id) selected = void 0;
852
+ handle.update();
853
+ } : void 0
854
+ })
855
+ }, thread.id)) : /* @__PURE__ */ jsx(Text, {
856
+ variant: "ui-caption",
857
+ tone: "muted",
858
+ children: t("app.client.taskDiscussion.noDiscussionYet")
859
+ }), capabilities.create || capabilities.reply && (explicit || threads.some((thread) => !thread.anchor)) ? /* @__PURE__ */ jsx("div", {
860
+ class: "flex min-w-0 flex-col gap-2",
861
+ children: [capabilities.create && (creating || !explicit && !threads.length) ? /* @__PURE__ */ jsx("div", {
862
+ class: "flex items-start justify-between gap-2",
863
+ "aria-label": t("app.client.taskDiscussion.newDiscussionTasks"),
864
+ children: [/* @__PURE__ */ jsx(ThreadTaskLinks, {
865
+ itemIds: draftItemIds,
866
+ items: handle.props.items ?? [],
867
+ href: taskHref,
868
+ busy: comments.busy,
869
+ onEdit: capabilities.link ? () => {
870
+ picking = true;
871
+ handle.update();
872
+ } : void 0
873
+ }), creating ? /* @__PURE__ */ jsx(Button, {
874
+ variant: "ghost",
875
+ size: "small",
876
+ disabled: comments.busy,
877
+ mix: on("click", () => {
878
+ creating = false;
879
+ handle.update();
880
+ }),
881
+ children: t("app.client.taskDiscussion.cancel")
882
+ }) : null]
883
+ }) : threads.length > 1 ? /* @__PURE__ */ jsx(Text, {
884
+ variant: "ui-caption",
885
+ tone: "muted",
886
+ children: t("app.client.taskDiscussion.replyingTo", { name: reply?.messages[0]?.author.name ?? t("app.client.taskDiscussion.theDiscussion") })
887
+ }) : null, /* @__PURE__ */ jsx(CommentComposer, {
888
+ label: t("app.client.taskDiscussion.addTaskComment"),
889
+ placeholder: t("app.client.taskDiscussion.addContextPlaceholder"),
890
+ busy: comments.busy || replyPending,
891
+ class: "w-full",
892
+ onSubmit: async (body) => {
893
+ if (replyPending) throw new Error(t("app.client.taskDiscussion.loadBeforeReplying"));
894
+ if (reply && capabilities.reply) {
895
+ await write({
896
+ id: reply.id,
897
+ body
898
+ });
899
+ return;
900
+ }
901
+ selected = (await write({
902
+ anchor: null,
903
+ itemIds: draftItemIds,
904
+ body
905
+ })).thread.id;
906
+ explicit = Boolean(selected);
907
+ creating = false;
908
+ draftItemIds = [handle.props.itemId];
909
+ handle.update();
910
+ }
911
+ }, "task-composer")]
912
+ }) : null] }),
913
+ picking ? /* @__PURE__ */ jsx(ProjectTaskPicker, {
914
+ project: handle.props.project,
915
+ selected: draftItemIds,
916
+ required: [handle.props.itemId],
917
+ onSave: async (itemIds) => {
918
+ draftItemIds = itemIds;
919
+ handle.update();
920
+ },
921
+ onClose: () => {
922
+ picking = false;
923
+ handle.update();
924
+ }
925
+ }) : null
926
+ ]
927
+ });
928
+ };
929
+ }
930
+ //#endregion
931
+ //#region app/client/project-tasks.tsx
932
+ function changedSince(row, item) {
933
+ if (row.revision !== null) return row.revision !== item.revision;
934
+ return Object.entries(row.values).some(([field, value]) => JSON.stringify(value) !== JSON.stringify(item.values[field]));
935
+ }
936
+ async function loadTask(token, id, schema, signal, conversationId) {
937
+ let [value, discussion, resolved] = await Promise.all([
938
+ requestTasks(routes.tasksApi.show.href({ id }), token, signal, (value) => value),
939
+ loadItemComments(token, id, signal, conversationId),
940
+ schema
941
+ ]);
942
+ let detail = parseWorkDetail(value, resolved);
943
+ if (detail.item.id !== id) throw new Error(t("app.client.projectTasks.differentTaskRefresh"));
944
+ return {
945
+ detail,
946
+ discussion
947
+ };
948
+ }
949
+ /**
950
+ * Opens the project's stored board, reading it from the companion only when this browser has
951
+ * none yet, and reads the task the URL opens with its discussions in parallel. An adapter
952
+ * failure is kept as data for the board.
953
+ * @param project - The project whose store and companion to read.
954
+ * @param signal - The screen's lifetime.
955
+ * @param href - The screen's location; a task page also reads that task.
956
+ * @param conversationId - A discussion the URL selects on that task page.
957
+ * @returns The store, or the error the board shows until a refresh succeeds.
958
+ */
959
+ async function loadTasks(project, signal, href, conversationId) {
960
+ let store = await projectStore(project);
961
+ let stored = storedWorkspace(store);
962
+ let workspace = stored ? Promise.resolve(stored) : projectWork(project).refresh(signal);
963
+ let id = href ? selectedTask(href) : null;
964
+ let open = (schema) => loadTask(project.token, id, schema, signal, conversationId);
965
+ let taskError = "";
966
+ let task = id ? open(workspace.then((value) => value.schema)).catch((cause) => {
967
+ if (!stored || cause instanceof TaskRequestError) throw cause;
968
+ return open(projectWork(project).refresh(signal).then((value) => value.schema));
969
+ }).catch((cause) => {
970
+ taskError = cause instanceof Error ? cause.message : t("app.client.projectTasks.couldNotLoadTask");
971
+ return null;
972
+ }) : null;
973
+ try {
974
+ await workspace;
975
+ let opened = await task;
976
+ return {
977
+ store,
978
+ stored: Boolean(stored),
979
+ error: "",
980
+ task: opened,
981
+ taskError
982
+ };
983
+ } catch (cause) {
984
+ signal.throwIfAborted();
985
+ return {
986
+ store,
987
+ stored: false,
988
+ error: cause instanceof Error ? cause.message : t("app.client.projectTasks.couldNotLoadTasks")
989
+ };
990
+ }
991
+ }
992
+ /**
993
+ * The project's work board, its task details and task writes.
994
+ * @param handle - The project and its first board.
995
+ * @returns The board render function.
996
+ */
997
+ function ProjectTasks(handle) {
998
+ let project = handle.props.project;
999
+ let store = handle.props.initial.store;
1000
+ let work = projectWork(project);
1001
+ let workspace = storedWorkspace(store);
1002
+ let stored = () => {
1003
+ let version = workspace?.snapshot.schemaVersion;
1004
+ workspace = storedWorkspace(store);
1005
+ let row = detail ? store.work.get(detail.item.id) : void 0;
1006
+ if (row && !row.$hasPendingWrites && changedSince(plainRow(row), detail.item)) loadDetail();
1007
+ else if (!detail && detailError && workspace?.snapshot.schemaVersion !== version) loadDetail();
1008
+ changed();
1009
+ };
1010
+ let subscriptions = [store.boards.subscribeChanges(stored), store.work.subscribeChanges(stored)];
1011
+ let selectedId = selectedTask(handle.props.href);
1012
+ let agentOpen = agentViewOpen(handle.props.href);
1013
+ let opened = handle.props.initial.task;
1014
+ let detail = opened?.detail.item.id === selectedId ? opened.detail : null;
1015
+ let discussion = detail ? opened.discussion : void 0;
1016
+ let loading = false;
1017
+ let detailError = handle.props.initial.task ? "" : handle.props.initial.taskError ?? "";
1018
+ let detailLoading = selectedId !== null && !detail && !detailError;
1019
+ let error = handle.props.initial.error;
1020
+ let listRequest = null;
1021
+ let detailRequest = null;
1022
+ let writeRequests = /* @__PURE__ */ new Set();
1023
+ let renderedHref = handle.props.href;
1024
+ let pendingHref;
1025
+ let deferredLocation = false;
1026
+ let mounted = false;
1027
+ function pending(open) {
1028
+ let row = open ? store.work.get(open.item.id) : void 0;
1029
+ if (!open || !row?.$hasPendingWrites) return open;
1030
+ let values = {
1031
+ ...open.item.values,
1032
+ ...plainRow(row).values
1033
+ };
1034
+ return {
1035
+ ...open,
1036
+ item: {
1037
+ ...open.item,
1038
+ values
1039
+ }
1040
+ };
1041
+ }
1042
+ function currentSchema() {
1043
+ if (!workspace) throw new Error(t("app.client.projectTasks.refreshBeforeChanges"));
1044
+ return workspace.schema;
1045
+ }
1046
+ async function write(url, body, parse) {
1047
+ let controller = new AbortController();
1048
+ writeRequests.add(controller);
1049
+ try {
1050
+ return await requestTasks(url, project.token, AbortSignal.any([handle.signal, controller.signal]), parse, fetch, { body });
1051
+ } finally {
1052
+ writeRequests.delete(controller);
1053
+ }
1054
+ }
1055
+ async function accepted(issue) {
1056
+ await writeRows(store.work, { upsert: [issue] });
1057
+ if (detail?.item.id === issue.id) detail = {
1058
+ ...detail,
1059
+ item: issue
1060
+ };
1061
+ changed();
1062
+ }
1063
+ async function refreshAfterWrite() {
1064
+ await load();
1065
+ if (selectedId) await loadDetail();
1066
+ }
1067
+ let queues = /* @__PURE__ */ new Map();
1068
+ async function save(url, body, predict, id) {
1069
+ let previous = id ? queues.get(id) : void 0;
1070
+ let settled = Promise.withResolvers();
1071
+ if (id) queues.set(id, settled.promise);
1072
+ try {
1073
+ let item = await writeOptimistically(predict, async () => {
1074
+ let prior = await previous;
1075
+ let from = "expectedRevision" in body ? body.expectedRevision : void 0;
1076
+ let result = await write(url, prior && from === prior.from && prior.to ? {
1077
+ ...body,
1078
+ expectedRevision: prior.to
1079
+ } : body, (value) => parseWorkMutationResult(value, currentSchema()));
1080
+ if (id && result.item.id !== id) throw new Error(t("app.client.projectTasks.differentTaskRefreshRetry"));
1081
+ await accepted(result.item);
1082
+ settled.resolve(from ? {
1083
+ from,
1084
+ to: result.item.revision
1085
+ } : null);
1086
+ return result.item;
1087
+ });
1088
+ if (selectedId) await loadDetail();
1089
+ return item;
1090
+ } catch (cause) {
1091
+ settled.resolve(null);
1092
+ if (!handle.signal.aborted) toast.error(t("app.client.projectTasks.undidTaskChange"), { description: cause instanceof Error ? cause.message : t("app.client.projectTasks.writeNotConfirmed") });
1093
+ refreshAfterWrite();
1094
+ throw cause;
1095
+ } finally {
1096
+ if (id && queues.get(id) === settled.promise) queues.delete(id);
1097
+ }
1098
+ }
1099
+ function predicted(id, next) {
1100
+ return () => {
1101
+ let row = store.work.get(id);
1102
+ if (!row) return;
1103
+ let { values } = next(plainRow(row));
1104
+ store.work.update(id, (draft) => {
1105
+ draft.values = values;
1106
+ });
1107
+ };
1108
+ }
1109
+ let moving = null;
1110
+ async function leaveRun(id, body, next) {
1111
+ let row = store.work.get(id);
1112
+ let run = row?.run;
1113
+ if (!row || !run) return body;
1114
+ let schema = currentSchema();
1115
+ let item = plainRow(row);
1116
+ let to = taskRole(next(item), schema, "status");
1117
+ if (taskRole(item, schema, "status") === to) return body;
1118
+ if (PUSHING_RUN_STATES.includes(run.state)) throw new Error(t("app.client.projectTasks.runPushing"));
1119
+ if (!DISCARDABLE_RUN_STATES.includes(run.state)) return body;
1120
+ let flow = Promise.withResolvers();
1121
+ let discard = await new Promise((resolve) => {
1122
+ moving = {
1123
+ itemId: id,
1124
+ status: schema.statuses.find((status) => status.value === to)?.label ?? to,
1125
+ answer: (value) => {
1126
+ resolve(value);
1127
+ return flow.promise;
1128
+ }
1129
+ };
1130
+ changed();
1131
+ });
1132
+ try {
1133
+ if (!discard) throw new Error(t("app.client.projectTasks.runKept"));
1134
+ await discardRun(project.token, id, handle.signal);
1135
+ let fresh = await operations.get(id);
1136
+ return {
1137
+ ...body,
1138
+ expectedRevision: fresh.revision ?? body.expectedRevision
1139
+ };
1140
+ } finally {
1141
+ flow.resolve();
1142
+ moving = null;
1143
+ changed();
1144
+ }
1145
+ }
1146
+ let factory = null;
1147
+ let follow = (value) => {
1148
+ factory = value;
1149
+ changed();
1150
+ };
1151
+ onFactoryStatus(project.token, follow, handle.signal);
1152
+ factoryStatus(project.token).then(follow, () => {});
1153
+ function boardColumns(schema) {
1154
+ let steps = factory?.enabled ? factory.statuses : null;
1155
+ if (!steps || !schema) return void 0;
1156
+ let shown = [
1157
+ steps.open,
1158
+ steps.inProgress,
1159
+ steps.needsApproval,
1160
+ steps.readyToMerge,
1161
+ steps.merged
1162
+ ];
1163
+ return {
1164
+ shown,
1165
+ collapsed: schema.statuses.filter((status) => !shown.includes(status.value) && (status.category === "done" || status.category === "cancelled")).map((status) => status.value)
1166
+ };
1167
+ }
1168
+ let operations = {
1169
+ create: (draft) => save(routes.tasksApi.create.href(), draft, () => {
1170
+ store.work.insert(predictWorkCreate(currentSchema(), `pending-${crypto.randomUUID()}`, draft));
1171
+ }),
1172
+ update: async (id, action) => {
1173
+ let body = await leaveRun(id, action, (item) => predictWorkUpdate(item, action));
1174
+ return save(routes.tasksApi.update.href({ id }), body, predicted(id, (item) => predictWorkUpdate(item, body)), id);
1175
+ },
1176
+ act: async (id, action) => {
1177
+ let body = await leaveRun(id, action, (item) => predictWorkAction(currentSchema(), item, action));
1178
+ return save(routes.tasksApi.mutate.href({ id }), body, predicted(id, (item) => predictWorkAction(currentSchema(), item, body)), id);
1179
+ },
1180
+ get: async (id) => {
1181
+ let next = await requestTasks(routes.tasksApi.show.href({ id }), project.token, handle.signal, (value) => parseWorkDetail(value, currentSchema()));
1182
+ if (next.item.id !== id) throw new Error(t("app.client.projectTasks.differentTaskRefresh"));
1183
+ return next.item;
1184
+ },
1185
+ section: (id, section, signal = handle.signal) => requestTasks(routes.tasksApi.section.href({
1186
+ id,
1187
+ section
1188
+ }), project.token, AbortSignal.any([handle.signal, signal]), (value) => parseSectionData(value, currentSchema())),
1189
+ sync: async (action) => {
1190
+ try {
1191
+ let result = await write(routes.tasksApi.sync.href(), action, parseWorkSyncResult);
1192
+ await refreshAfterWrite();
1193
+ return result;
1194
+ } catch (cause) {
1195
+ refreshAfterWrite();
1196
+ throw cause;
1197
+ }
1198
+ }
1199
+ };
1200
+ function changed() {
1201
+ if (!handle.signal.aborted) handle.update();
1202
+ }
1203
+ async function load() {
1204
+ listRequest?.abort();
1205
+ let controller = new AbortController();
1206
+ listRequest = controller;
1207
+ loading = true;
1208
+ error = "";
1209
+ changed();
1210
+ try {
1211
+ await work.refresh(AbortSignal.any([handle.signal, controller.signal]));
1212
+ } catch (cause) {
1213
+ if (!controller.signal.aborted && !handle.signal.aborted) error = cause instanceof Error ? cause.message : t("app.client.projectTasks.couldNotLoadTasks");
1214
+ } finally {
1215
+ if (listRequest === controller) {
1216
+ loading = false;
1217
+ changed();
1218
+ }
1219
+ }
1220
+ }
1221
+ async function loadDetail() {
1222
+ detailRequest?.abort();
1223
+ if (!selectedId) {
1224
+ detail = null;
1225
+ detailLoading = false;
1226
+ detailError = "";
1227
+ changed();
1228
+ return;
1229
+ }
1230
+ let id = selectedId;
1231
+ let controller = new AbortController();
1232
+ detailRequest = controller;
1233
+ let reopening = detail?.item.id !== id;
1234
+ if (reopening) detail = null;
1235
+ detailLoading = true;
1236
+ detailError = "";
1237
+ changed();
1238
+ try {
1239
+ let signal = AbortSignal.any([handle.signal, controller.signal]);
1240
+ if (reopening) {
1241
+ let next = await loadTask(project.token, id, currentSchema(), signal, handle.props.conversationId);
1242
+ if (controller.signal.aborted || id !== selectedId) return;
1243
+ detail = next.detail;
1244
+ discussion = next.discussion;
1245
+ return;
1246
+ }
1247
+ let next = await requestTasks(routes.tasksApi.show.href({ id }), project.token, signal, (value) => parseWorkDetail(value, currentSchema()));
1248
+ if (controller.signal.aborted || id !== selectedId) return;
1249
+ if (next.item.id !== id) throw new Error(t("app.client.projectTasks.differentTaskRefresh"));
1250
+ detail = next;
1251
+ } catch (cause) {
1252
+ if (!controller.signal.aborted && !handle.signal.aborted && id === selectedId) detailError = cause instanceof Error ? cause.message : t("app.client.projectTasks.couldNotLoadTask");
1253
+ } finally {
1254
+ if (detailRequest === controller) {
1255
+ detailLoading = false;
1256
+ changed();
1257
+ }
1258
+ }
1259
+ }
1260
+ function select(id, history = true) {
1261
+ if (id === selectedId) return;
1262
+ selectedId = id;
1263
+ agentOpen = false;
1264
+ if (history) location(false);
1265
+ loadDetail();
1266
+ changed();
1267
+ }
1268
+ function location(replace) {
1269
+ let root = taskHref(null);
1270
+ if (!handle.props.active || !readConversationLocation(window.location.href) && window.location.pathname !== root && !window.location.pathname.startsWith(`${root}/`)) {
1271
+ deferredLocation = true;
1272
+ return;
1273
+ }
1274
+ deferredLocation = false;
1275
+ let href = agentOpen && selectedId ? taskAgentHref(selectedId) : taskHref(selectedId);
1276
+ if (window.location.pathname + window.location.search + window.location.hash === href) return;
1277
+ pendingHref = new URL(href, window.location.origin).href;
1278
+ window.history[replace ? "replaceState" : "pushState"]({
1279
+ ...window.history.state,
1280
+ budTask: selectedId
1281
+ }, "", href);
1282
+ }
1283
+ let mount = ref((_node, signal) => {
1284
+ mounted = true;
1285
+ let loaded = selectedId;
1286
+ selectedId = selectedTask(handle.props.href);
1287
+ if (detail?.item.id !== selectedId && !(detailError && loaded === selectedId)) loadDetail();
1288
+ if (handle.props.initial.stored) load();
1289
+ signal.addEventListener("abort", () => {
1290
+ for (let subscription of subscriptions) subscription.unsubscribe();
1291
+ listRequest?.abort();
1292
+ detailRequest?.abort();
1293
+ for (let controller of writeRequests) controller.abort();
1294
+ }, { once: true });
1295
+ });
1296
+ return () => {
1297
+ if (mounted && renderedHref !== handle.props.href) {
1298
+ renderedHref = handle.props.href;
1299
+ if (pendingHref !== renderedHref) {
1300
+ deferredLocation = false;
1301
+ let id = selectedTask(renderedHref);
1302
+ let agent = agentViewOpen(renderedHref);
1303
+ if (id !== selectedId) handle.queueTask(() => {
1304
+ select(id, false);
1305
+ agentOpen = agent;
1306
+ changed();
1307
+ });
1308
+ else if (agent !== agentOpen) agentOpen = agent;
1309
+ }
1310
+ pendingHref = void 0;
1311
+ }
1312
+ if (deferredLocation && handle.props.active) handle.queueTask(() => location(true));
1313
+ let current = workspace;
1314
+ let runState = detail ? store.work.get(detail.item.id)?.run?.state ?? detail.item.run?.state ?? null : null;
1315
+ let agentFor = detail && runState ? detail.item.id : null;
1316
+ let linked = current?.snapshot.items.map((item) => ({
1317
+ id: item.id,
1318
+ title: taskRole(item, current.schema, "title") || void 0
1319
+ }));
1320
+ return /* @__PURE__ */ jsx("div", {
1321
+ class: "flex h-full min-h-0 flex-col",
1322
+ mix: mount,
1323
+ children: [
1324
+ /* @__PURE__ */ jsx(FactorySetup, {
1325
+ token: project.token,
1326
+ onAdded: () => void load()
1327
+ }),
1328
+ moving ? /* @__PURE__ */ jsx(RunMoveDialog, {
1329
+ itemId: moving.itemId,
1330
+ status: moving.status,
1331
+ onAnswer: moving.answer
1332
+ }, moving.itemId) : null,
1333
+ /* @__PURE__ */ jsx("div", {
1334
+ class: "min-h-0 flex-1",
1335
+ children: /* @__PURE__ */ jsx(TaskWorkspace, {
1336
+ projectName: project.name,
1337
+ project: {
1338
+ id: project.id,
1339
+ name: project.name
1340
+ },
1341
+ design: handle.props.design,
1342
+ storageKey: `bud:tasks:${project.id}:v1`,
1343
+ snapshot: current?.snapshot ?? null,
1344
+ schema: current?.schema ?? null,
1345
+ loading,
1346
+ error,
1347
+ selectedId,
1348
+ detail: pending(detail),
1349
+ discussion: detail ? /* @__PURE__ */ jsx("div", {
1350
+ class: "flex min-w-0 flex-col gap-8",
1351
+ children: [
1352
+ /* @__PURE__ */ jsx(TaskContextBlock, {
1353
+ token: project.token,
1354
+ itemId: detail.item.id
1355
+ }, `context-${detail.item.id}`),
1356
+ /* @__PURE__ */ jsx(FactoryRun, {
1357
+ token: project.token,
1358
+ projectId: project.id,
1359
+ itemId: detail.item.id,
1360
+ revision: detail.item.revision,
1361
+ runState: store.work.get(detail.item.id)?.run?.state ?? detail.item.run?.state
1362
+ }, `run-${detail.item.id}`),
1363
+ /* @__PURE__ */ jsx(TaskDiscussion, {
1364
+ project,
1365
+ itemId: detail.item.id,
1366
+ initial: discussion,
1367
+ items: linked,
1368
+ conversationId: handle.props.conversationId,
1369
+ onChange: refreshAfterWrite
1370
+ }, detail.item.id)
1371
+ ]
1372
+ }) : void 0,
1373
+ detailLoading,
1374
+ detailError,
1375
+ operations,
1376
+ columns: boardColumns(current?.schema ?? null),
1377
+ pageActions: agentFor ? /* @__PURE__ */ jsx(IconButton, {
1378
+ tooltip: true,
1379
+ icon: "terminal",
1380
+ active: agentOpen,
1381
+ label: agentOpen ? t("app.client.projectTasks.closeAgentView") : t("app.client.projectTasks.openAgentView"),
1382
+ "data-testid": "agent-view-toggle",
1383
+ mix: on("click", () => {
1384
+ agentOpen = !agentOpen;
1385
+ location(false);
1386
+ changed();
1387
+ })
1388
+ }) : void 0,
1389
+ pageFrame: agentOpen && agentFor ? (page) => /* @__PURE__ */ jsx(AgentPanel, {
1390
+ token: project.token,
1391
+ itemId: agentFor,
1392
+ runState,
1393
+ details: page
1394
+ }, `agent-${agentFor}`) : void 0,
1395
+ onSelect: select,
1396
+ onReload: () => {
1397
+ load();
1398
+ },
1399
+ onReloadDetail: () => {
1400
+ loadDetail();
1401
+ }
1402
+ })
1403
+ })
1404
+ ]
1405
+ });
1406
+ };
1407
+ }
1408
+ //#endregion
1409
+ //#region app/screens/tasks.tsx
1410
+ var tasks_default = defineScreen({
1411
+ routes: taskRoutes,
1412
+ load: ({ project, signal, url, conversation }) => loadTasks(project, signal, url.href, conversation?.id),
1413
+ render: (tasks, { project, url, active, taskDesign, conversation }) => /* @__PURE__ */ jsx(ProjectTasks, {
1414
+ project,
1415
+ initial: tasks,
1416
+ design: taskDesign,
1417
+ href: url.href,
1418
+ active,
1419
+ conversationId: conversation?.id
1420
+ })
1421
+ });
1422
+ //#endregion
1423
+ export { tasks_default as default };