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