@agimon-ai/po-git 0.0.0-alpha.4 → 0.0.0-alpha.41

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.
@@ -1,7 +1,7 @@
1
1
  import { createApiClient, defineActivityGroup, defineChannelFile, defineFill, defineGlobalStore, defineLeaderBinding, defineRepositorySettingsPanel, defineSessionStore, defineToolRenderer, defineWebLifecycle, defineWebPlugin, sessionApiAddress } from "@agimon-ai/po-core/web";
2
- import { AlertIcon, ArrowRightIcon, BranchIcon, Button, CheckIcon, ChevronDownIcon, ChevronRightIcon, CloseIcon, Dialog, DialogBody, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DiffView, Dot, DropdownMenuItem, EmptyState, Input, MessageIcon, MessageItem, MessageItemBody, MessageItemHeader, OptionRow, RadioGroup, RadioGroupCard, ReviewCommentDraft, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SettingsActions, SettingsCard, SettingsField, SettingsNote, SettingsRow, SettingsRowDescription, SettingsRowText, SettingsRowTitle, SettingsSection, SettingsSectionDescription, SettingsSectionHeader, SettingsSectionTitle, ShieldIcon, Spinner, StatusBadge, buildReviewPrompt, reviewCommentAnchor, toolTone } from "@agimon-ai/po-web-components";
3
- import { createElement, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
4
- import { sealedTransport } from "@agimon-ai/po-web-security/browser";
2
+ import { ArrowRightIcon, BranchIcon, Button, CheckIcon, ChevronDownIcon, ChevronUpIcon, Dialog, DialogContent, DialogTitle, DiffView, Dot, DropdownMenuItem, EmptyState, Input, MessageIcon, MessageItem, MessageItemBody, ReviewCommentDraft, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SettingsActions, SettingsCard, SettingsNote, SettingsRow, SettingsRowDescription, SettingsRowText, SettingsRowTitle, SettingsSection, SettingsSectionDescription, SettingsSectionHeader, SettingsSectionTitle, ShieldIcon, Spinner, StatusBadge, Tabs, TabsContent, TabsList, TabsTrigger, buildReviewPrompt } from "@agimon-ai/po-web-components";
3
+ import { createElement, useCallback, useEffect, useId, useRef, useState } from "react";
4
+ import { browserTransport } from "@agimon-ai/po-web-security/browser";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { useStore } from "@tanstack/react-store";
7
7
  const api = createApiClient({
@@ -51,7 +51,7 @@ const api = createApiClient({
51
51
  "session"
52
52
  ],
53
53
  basePath: "git",
54
- transport: (input, init) => sealedTransport.fetch(input, init),
54
+ transport: (input, init) => browserTransport.fetch(input, init),
55
55
  sessionAddress: sessionApiAddress
56
56
  });
57
57
  //#endregion
@@ -113,20 +113,241 @@ function createRequest(state) {
113
113
  }
114
114
  //#endregion
115
115
  //#region src/extensions/workspaces/(frontend)/fill/_components/GitNewSessionDialog.tsx
116
- const MODES = [
117
- {
116
+ function Field$1({ label, children }) {
117
+ return /* @__PURE__ */ jsxs("label", {
118
+ className: "flex flex-col gap-2 text-base text-po-text",
119
+ children: [/* @__PURE__ */ jsx("span", { children: label }), children]
120
+ });
121
+ }
122
+ /** Inline presentation also lets story capture inspect content, rather than a portal-only root. */
123
+ function GitNewSessionDialogView({ workspaceRoot, branches, loadError = "", initialForm, busy = false, error = "", onClose, onCreate }) {
124
+ const [form, setForm] = useState({
118
125
  mode: "existing",
119
- label: "existing branch"
120
- },
121
- {
122
- mode: "new",
123
- label: "new branch"
124
- },
125
- {
126
- mode: "plain",
127
- label: "no branch"
128
- }
129
- ];
126
+ newBranch: "",
127
+ baseRef: branches?.defaultBase ?? "",
128
+ name: "",
129
+ ...initialForm
130
+ });
131
+ const [query, setQuery] = useState("");
132
+ const ready = branches?.repository === true && loadError === "";
133
+ const loading = branches === null && loadError === "";
134
+ const mode = ready || loading ? form.mode : "plain";
135
+ const state = {
136
+ ...form,
137
+ mode,
138
+ baseRef: form.baseRef || branches?.defaultBase || ""
139
+ };
140
+ const problem = formProblem$1(state);
141
+ const update = (patch) => setForm((current) => ({
142
+ ...current,
143
+ ...patch
144
+ }));
145
+ const options = branches === null ? [] : branchOptions(branches, query);
146
+ const bases = branches === null ? [] : baseOptions(branches);
147
+ const submit = () => {
148
+ if (!busy && !loading && problem === void 0) onCreate(state);
149
+ };
150
+ return /* @__PURE__ */ jsxs("section", {
151
+ "data-testid": "new-session-dialog",
152
+ "aria-label": "New session",
153
+ className: "shrink-0 overflow-hidden rounded-lg border border-po-border bg-po-panel shadow-sm",
154
+ children: [
155
+ /* @__PURE__ */ jsxs("header", {
156
+ className: "border-b border-po-border-soft px-6 py-4",
157
+ children: [/* @__PURE__ */ jsx("h3", {
158
+ className: "text-xl font-semibold text-po-hi",
159
+ children: "New session"
160
+ }), /* @__PURE__ */ jsx("p", {
161
+ className: "mt-1 font-mono text-sm text-po-dim",
162
+ children: workspaceRoot
163
+ })]
164
+ }),
165
+ /* @__PURE__ */ jsxs("div", {
166
+ className: "flex flex-col gap-5 p-6",
167
+ children: [
168
+ !ready ? /* @__PURE__ */ jsx("p", {
169
+ "data-testid": "git-new-session-plain-note",
170
+ className: "text-base text-po-dim",
171
+ children: loading ? "Reading branches..." : loadError || "This workspace is not a git checkout. Start a plain session in this folder."
172
+ }) : /* @__PURE__ */ jsxs(Tabs, {
173
+ value: mode,
174
+ onValueChange: (value) => update({ mode: value }),
175
+ className: "flex flex-col gap-5",
176
+ children: [
177
+ /* @__PURE__ */ jsxs(TabsList, {
178
+ "aria-label": "Session kind",
179
+ className: "rounded-md border border-po-border-soft bg-po-bg p-1",
180
+ children: [
181
+ /* @__PURE__ */ jsx(TabsTrigger, {
182
+ "data-testid": "git-new-session-mode-existing",
183
+ value: "existing",
184
+ disabled: busy,
185
+ className: "flex-1 justify-center",
186
+ children: "Existing branch"
187
+ }),
188
+ /* @__PURE__ */ jsx(TabsTrigger, {
189
+ "data-testid": "git-new-session-mode-new",
190
+ value: "new",
191
+ disabled: busy,
192
+ className: "flex-1 justify-center",
193
+ children: "New branch"
194
+ }),
195
+ /* @__PURE__ */ jsx(TabsTrigger, {
196
+ "data-testid": "git-new-session-mode-plain",
197
+ value: "plain",
198
+ disabled: busy,
199
+ className: "flex-1 justify-center",
200
+ children: "No branch"
201
+ })
202
+ ]
203
+ }),
204
+ /* @__PURE__ */ jsxs(TabsContent, {
205
+ value: "existing",
206
+ className: "flex flex-col gap-2",
207
+ children: [
208
+ /* @__PURE__ */ jsx("p", {
209
+ className: "text-base font-medium",
210
+ children: "Branch"
211
+ }),
212
+ /* @__PURE__ */ jsx(Input, {
213
+ "data-testid": "git-new-session-branch-search",
214
+ "aria-label": "Find a branch",
215
+ placeholder: "Find a branch",
216
+ value: query,
217
+ onChange: (event) => setQuery(event.target.value),
218
+ disabled: busy
219
+ }),
220
+ /* @__PURE__ */ jsxs("fieldset", {
221
+ "aria-label": "Branches",
222
+ className: "max-h-64 min-w-0 overflow-y-auto rounded-md border border-po-border-soft bg-po-bg p-1",
223
+ children: [options.length === 0 ? /* @__PURE__ */ jsx("p", {
224
+ className: "px-3 py-2 text-sm text-po-dim",
225
+ children: "No matching branches"
226
+ }) : null, options.map((branch, index) => /* @__PURE__ */ jsxs("div", { children: [branch.remote !== void 0 && (index === 0 || options[index - 1]?.remote === void 0) ? /* @__PURE__ */ jsx("p", {
227
+ className: "mt-1 border-t border-po-border-soft px-3 pt-2 text-sm text-po-dim",
228
+ children: "Remote branches"
229
+ }) : null, /* @__PURE__ */ jsxs("button", {
230
+ "data-testid": "git-new-session-branch-option",
231
+ "data-branch": branch.name,
232
+ "data-remote": branch.remote,
233
+ "aria-pressed": form.selected?.key === branch.key,
234
+ disabled: branch.busyAt !== void 0 || busy,
235
+ onClick: () => update({ selected: branch }),
236
+ className: `flex w-full flex-col gap-1 rounded px-3 py-2 text-left text-base ${form.selected?.key === branch.key ? "bg-po-tint-green text-po-green" : "text-po-text"}`,
237
+ children: [/* @__PURE__ */ jsxs("span", {
238
+ className: "flex items-center justify-between gap-2",
239
+ children: [/* @__PURE__ */ jsx("span", {
240
+ className: "font-mono",
241
+ children: branch.remote === void 0 ? branch.name : `${branch.remote}/${branch.name}`
242
+ }), form.selected?.key === branch.key ? /* @__PURE__ */ jsx(CheckIcon, { className: "size-4" }) : null]
243
+ }), branch.busyAt !== void 0 ? /* @__PURE__ */ jsxs("span", {
244
+ className: "text-sm text-po-dim",
245
+ children: ["Checked out in ", /* @__PURE__ */ jsx("span", {
246
+ className: "font-mono break-all",
247
+ children: branch.busyAt
248
+ })]
249
+ }) : branch.current ? /* @__PURE__ */ jsx("span", {
250
+ className: "text-sm text-po-dim",
251
+ children: "Current"
252
+ }) : null]
253
+ })] }, branch.key))]
254
+ })
255
+ ]
256
+ }),
257
+ /* @__PURE__ */ jsxs(TabsContent, {
258
+ value: "new",
259
+ className: "flex flex-col gap-4",
260
+ children: [/* @__PURE__ */ jsx(Field$1, {
261
+ label: "Branch name",
262
+ children: /* @__PURE__ */ jsx(Input, {
263
+ "data-testid": "git-new-session-new-branch",
264
+ "aria-label": "Branch name",
265
+ placeholder: "feature/my-change",
266
+ className: "font-mono",
267
+ value: form.newBranch,
268
+ onChange: (event) => update({ newBranch: event.target.value }),
269
+ disabled: busy
270
+ })
271
+ }), /* @__PURE__ */ jsx(Field$1, {
272
+ label: "From",
273
+ children: /* @__PURE__ */ jsxs(Select, {
274
+ value: form.baseRef || branches?.defaultBase || "",
275
+ onValueChange: (baseRef) => update({ baseRef }),
276
+ disabled: busy,
277
+ children: [/* @__PURE__ */ jsx(SelectTrigger, {
278
+ "data-testid": "git-new-session-base",
279
+ "aria-label": "From",
280
+ className: "font-mono",
281
+ children: /* @__PURE__ */ jsx(SelectValue, {})
282
+ }), /* @__PURE__ */ jsx(SelectContent, { children: bases.map((base) => /* @__PURE__ */ jsx(SelectItem, {
283
+ value: base,
284
+ children: base
285
+ }, base)) })]
286
+ })
287
+ })]
288
+ }),
289
+ /* @__PURE__ */ jsx(TabsContent, {
290
+ value: "plain",
291
+ children: /* @__PURE__ */ jsx("p", {
292
+ className: "text-base text-po-dim",
293
+ children: "Start in this workspace folder, without creating a worktree."
294
+ })
295
+ })
296
+ ]
297
+ }),
298
+ /* @__PURE__ */ jsx(Field$1, {
299
+ label: /* @__PURE__ */ jsxs(Fragment, { children: [
300
+ "Name",
301
+ " ",
302
+ /* @__PURE__ */ jsx("span", {
303
+ className: "text-sm text-po-dim",
304
+ children: mode === "plain" ? "(optional)" : "(optional, defaults to the branch)"
305
+ })
306
+ ] }),
307
+ children: /* @__PURE__ */ jsx(Input, {
308
+ "data-testid": "new-session-name",
309
+ "aria-label": "Session name",
310
+ placeholder: "Session name",
311
+ value: form.name,
312
+ onChange: (event) => update({ name: event.target.value }),
313
+ onKeyDown: (event) => {
314
+ if (event.key === "Enter") submit();
315
+ },
316
+ disabled: busy
317
+ })
318
+ }),
319
+ mode !== "plain" ? /* @__PURE__ */ jsx("p", {
320
+ className: "text-sm leading-relaxed text-po-dim",
321
+ children: "Runs in its own worktree, so this checkout stays as it is."
322
+ }) : null,
323
+ error ? /* @__PURE__ */ jsx("p", {
324
+ "data-testid": "new-session-error",
325
+ role: "alert",
326
+ className: "rounded-md border border-po-edge-red bg-po-tint-red p-3 text-sm text-po-red",
327
+ children: error
328
+ }) : null
329
+ ]
330
+ }),
331
+ /* @__PURE__ */ jsxs("footer", {
332
+ className: "flex items-center justify-between border-t border-po-border-soft px-6 py-4",
333
+ children: [/* @__PURE__ */ jsx(Button, {
334
+ "data-testid": "new-session-cancel",
335
+ variant: "ghost",
336
+ disabled: busy,
337
+ onClick: onClose,
338
+ children: "Cancel"
339
+ }), /* @__PURE__ */ jsx(Button, {
340
+ "data-testid": "new-session-create",
341
+ variant: "primary",
342
+ title: problem,
343
+ disabled: busy || loading || problem !== void 0,
344
+ onClick: submit,
345
+ children: busy ? mode === "plain" ? "Creating session..." : "Creating worktree..." : "Create session"
346
+ })]
347
+ })
348
+ ]
349
+ });
350
+ }
130
351
  function errorOf$1(body, status) {
131
352
  return typeof body === "object" && body !== null && typeof body.error === "string" ? body.error : `The cockpit answered ${String(status)}.`;
132
353
  }
@@ -138,13 +359,6 @@ function errorOf$1(body, status) {
138
359
  function GitNewSessionDialog({ newSession }) {
139
360
  const [branches, setBranches] = useState(null);
140
361
  const [loadError, setLoadError] = useState("");
141
- const [form, setForm] = useState({
142
- mode: "existing",
143
- newBranch: "",
144
- baseRef: "",
145
- name: ""
146
- });
147
- const [query, setQuery] = useState("");
148
362
  const [error, setError] = useState("");
149
363
  const [busy, setBusy] = useState(false);
150
364
  const workspaceId = newSession?.workspaceId;
@@ -160,12 +374,7 @@ function GitNewSessionDialog({ newSession }) {
160
374
  setLoadError(errorOf$1(body, response.status));
161
375
  return;
162
376
  }
163
- const loaded = body;
164
- setBranches(loaded);
165
- setForm((current) => ({
166
- ...current,
167
- baseRef: loaded.defaultBase ?? ""
168
- }));
377
+ setBranches(body);
169
378
  }).catch(() => {
170
379
  if (!stale) setLoadError("The branches could not be loaded.");
171
380
  });
@@ -174,34 +383,23 @@ function GitNewSessionDialog({ newSession }) {
174
383
  };
175
384
  }, [workspaceId, request]);
176
385
  if (newSession === void 0) return null;
177
- const gitReady = branches?.repository === true && loadError === "";
178
- const mode = gitReady || branches === null ? form.mode : "plain";
179
- const state = {
180
- ...form,
181
- mode
182
- };
183
- const problem = formProblem$1(state);
184
- const options = branches === null ? [] : branchOptions(branches, query);
185
- const bases = branches === null ? [] : baseOptions(branches);
186
- const update = (patch) => setForm((current) => ({
187
- ...current,
188
- ...patch
189
- }));
190
- const submit = async () => {
191
- if (busy || problem !== void 0) return;
386
+ const submit = async (state) => {
387
+ if (busy || formProblem$1(state) !== void 0) return;
192
388
  setBusy(true);
193
389
  setError("");
194
390
  const body = createRequest(state);
195
391
  let failure;
196
- if (body === void 0) failure = await newSession.createPlain(state.name);
197
- else try {
198
- const response = await newSession.requestWithStepUp(api.workspace(newSession.workspaceId).createSession.url(), {
199
- method: "POST",
200
- headers: { "Content-Type": "application/json" },
201
- body: JSON.stringify(body)
202
- });
203
- const answer = await response.json().catch(() => void 0);
204
- failure = response.ok ? await newSession.openCreated(answer.sessionId) : errorOf$1(answer, response.status);
392
+ try {
393
+ if (body === void 0) failure = await newSession.createPlain(state.name);
394
+ else {
395
+ const response = await newSession.requestWithStepUp(api.workspace(newSession.workspaceId).createSession.url(), {
396
+ method: "POST",
397
+ headers: { "Content-Type": "application/json" },
398
+ body: JSON.stringify(body)
399
+ });
400
+ const answer = await response.json().catch(() => void 0);
401
+ failure = response.ok ? await newSession.openCreated(answer.sessionId) : errorOf$1(answer, response.status);
402
+ }
205
403
  } catch {
206
404
  failure = "The cockpit is unreachable.";
207
405
  }
@@ -211,159 +409,25 @@ function GitNewSessionDialog({ newSession }) {
211
409
  };
212
410
  return /* @__PURE__ */ jsx(Dialog, {
213
411
  open: true,
214
- onOpenChange: (next) => !next && !busy && newSession.close(),
412
+ onOpenChange: (next) => {
413
+ if (!next && !busy) newSession.close();
414
+ },
215
415
  children: /* @__PURE__ */ jsxs(DialogContent, {
216
416
  width: "md",
217
- "data-testid": "new-session-dialog",
417
+ className: "overflow-y-auto p-0",
218
418
  "aria-describedby": void 0,
219
- children: [/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: "new session" }) }), /* @__PURE__ */ jsxs(DialogBody, { children: [
220
- /* @__PURE__ */ jsx("p", {
221
- className: "truncate text-sm text-po-dim",
222
- title: newSession.workspaceRoot,
223
- children: newSession.workspaceRoot
224
- }),
225
- gitReady ? /* @__PURE__ */ jsx("div", {
226
- role: "radiogroup",
227
- "aria-label": "session kind",
228
- className: "flex gap-1",
229
- children: MODES.map((entry) => /* @__PURE__ */ jsx(Button, {
230
- role: "radio",
231
- "aria-checked": mode === entry.mode,
232
- variant: mode === entry.mode ? "outline" : "ghost",
233
- size: "sm",
234
- "data-testid": `git-new-session-mode-${entry.mode}`,
235
- disabled: busy,
236
- className: mode === entry.mode ? "border-po-blue/60 text-po-hi" : void 0,
237
- onClick: () => update({ mode: entry.mode }),
238
- children: entry.label
239
- }, entry.mode))
240
- }) : branches === null && loadError === "" ? /* @__PURE__ */ jsx("p", {
241
- className: "text-sm text-po-dim",
242
- children: "reading branches…"
243
- }) : /* @__PURE__ */ jsx("p", {
244
- "data-testid": "git-new-session-plain-note",
245
- className: "text-sm text-po-dim",
246
- children: loadError || "This workspace is not a git checkout, so the session starts in its folder."
247
- }),
248
- mode === "existing" && gitReady ? /* @__PURE__ */ jsxs("div", {
249
- className: "flex flex-col gap-1",
250
- children: [/* @__PURE__ */ jsx(Input, {
251
- "data-testid": "git-new-session-branch-search",
252
- "aria-label": "find a branch",
253
- value: query,
254
- autoFocus: true,
255
- disabled: busy,
256
- onChange: (event) => setQuery(event.target.value),
257
- placeholder: "find a branch"
258
- }), /* @__PURE__ */ jsxs("div", {
259
- role: "listbox",
260
- "aria-label": "branches",
261
- className: "max-h-48 overflow-y-auto rounded border border-po-border bg-po-deep p-1",
262
- children: [options.length === 0 ? /* @__PURE__ */ jsx("p", {
263
- className: "px-2 py-1 text-sm text-po-dim",
264
- children: "no matching branches"
265
- }) : null, options.map((option) => /* @__PURE__ */ jsxs(OptionRow, {
266
- density: "compact",
267
- active: form.selected?.key === option.key,
268
- disabled: option.busyAt !== void 0 || busy,
269
- "data-testid": "git-new-session-branch-option",
270
- "data-branch": option.name,
271
- "data-remote": option.remote,
272
- onClick: () => update({ selected: option }),
273
- className: `w-full px-2.5 py-1 text-base ${option.busyAt !== void 0 ? "opacity-50" : ""}`,
274
- children: [/* @__PURE__ */ jsx("span", {
275
- className: "min-w-0 flex-1 truncate",
276
- children: option.remote === void 0 ? option.name : `${option.remote}/${option.name}`
277
- }), /* @__PURE__ */ jsx("span", {
278
- className: "shrink-0 text-sm text-po-dim",
279
- children: option.current && option.busyAt !== void 0 ? "current · checked out" : option.current ? "current" : option.busyAt !== void 0 ? "checked out" : option.remote !== void 0 ? "remote" : ""
280
- })]
281
- }, option.key))]
282
- })]
283
- }) : null,
284
- mode === "new" && gitReady ? /* @__PURE__ */ jsxs("div", {
285
- className: "flex flex-col gap-2",
286
- children: [/* @__PURE__ */ jsxs("label", {
287
- htmlFor: "git-new-session-new-branch",
288
- className: "flex flex-col gap-1",
289
- children: [/* @__PURE__ */ jsx("span", {
290
- className: "text-sm text-po-dim",
291
- children: "new branch"
292
- }), /* @__PURE__ */ jsx(Input, {
293
- id: "git-new-session-new-branch",
294
- "data-testid": "git-new-session-new-branch",
295
- value: form.newBranch,
296
- autoFocus: true,
297
- disabled: busy,
298
- onChange: (event) => update({ newBranch: event.target.value }),
299
- placeholder: "feature/my-change"
300
- })]
301
- }), /* @__PURE__ */ jsxs("span", {
302
- className: "flex flex-col gap-1",
303
- children: [/* @__PURE__ */ jsx("span", {
304
- className: "text-sm text-po-dim",
305
- children: "from"
306
- }), /* @__PURE__ */ jsxs(Select, {
307
- value: form.baseRef,
308
- onValueChange: (baseRef) => update({ baseRef }),
309
- disabled: busy,
310
- children: [/* @__PURE__ */ jsx(SelectTrigger, {
311
- "data-testid": "git-new-session-base",
312
- "aria-label": "base branch",
313
- children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "base branch" })
314
- }), /* @__PURE__ */ jsx(SelectContent, { children: bases.map((base) => /* @__PURE__ */ jsx(SelectItem, {
315
- value: base,
316
- children: base === branches?.current ? `${base} (current)` : base
317
- }, base)) })]
318
- })]
319
- })]
320
- }) : null,
321
- /* @__PURE__ */ jsxs("label", {
322
- htmlFor: "new-session-name",
323
- className: "flex flex-col gap-1",
324
- children: [/* @__PURE__ */ jsxs("span", {
325
- className: "text-sm text-po-dim",
326
- children: ["name ", /* @__PURE__ */ jsxs("span", {
327
- className: "text-po-dim",
328
- children: [
329
- "(optional",
330
- mode === "plain" ? "" : "; defaults to the branch",
331
- ")"
332
- ]
333
- })]
334
- }), /* @__PURE__ */ jsx(Input, {
335
- id: "new-session-name",
336
- "data-testid": "new-session-name",
337
- value: form.name,
338
- autoFocus: mode === "plain",
339
- disabled: busy,
340
- onChange: (event) => update({ name: event.target.value }),
341
- onKeyDown: (event) => {
342
- if (event.key === "Enter") submit();
343
- },
344
- placeholder: "session name"
345
- })]
346
- }),
347
- error ? /* @__PURE__ */ jsx("pre", {
348
- "data-testid": "new-session-error",
349
- className: "max-h-28 overflow-y-auto rounded border border-po-edge-red bg-po-tint-red/40 px-2.5 py-2 text-sm whitespace-pre-wrap text-po-red",
350
- children: error
351
- }) : null,
352
- /* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button, {
353
- variant: "outline",
354
- "data-testid": "new-session-cancel",
355
- disabled: busy,
356
- onClick: () => newSession.close(),
357
- children: "cancel"
358
- }), /* @__PURE__ */ jsx(Button, {
359
- variant: "primary",
360
- "data-testid": "new-session-create",
361
- title: problem,
362
- disabled: busy || problem !== void 0,
363
- onClick: () => void submit(),
364
- children: busy ? mode === "plain" ? "creating…" : "creating worktree…" : "create"
365
- })] })
366
- ] })]
419
+ children: [/* @__PURE__ */ jsx(DialogTitle, {
420
+ className: "sr-only",
421
+ children: "New session"
422
+ }), /* @__PURE__ */ jsx(GitNewSessionDialogView, {
423
+ workspaceRoot: newSession.workspaceRoot,
424
+ branches,
425
+ loadError,
426
+ busy,
427
+ error,
428
+ onClose: newSession.close,
429
+ onCreate: (state) => void submit(state)
430
+ })]
367
431
  })
368
432
  });
369
433
  }
@@ -446,11 +510,17 @@ function saveRequestOf(form) {
446
510
  * AVOID:
447
511
  * - Echoing a token back. The view the server sends has none to echo.
448
512
  */
513
+ function Field({ label, children }) {
514
+ return /* @__PURE__ */ jsxs("label", {
515
+ className: "flex flex-col gap-2 text-base text-po-text",
516
+ children: [/* @__PURE__ */ jsx("span", { children: label }), children]
517
+ });
518
+ }
449
519
  const METHODS = [
450
520
  {
451
521
  method: "none",
452
522
  title: "This machine's git",
453
- detail: "Whatever git here already does: your credential helper, SSH config and agent."
523
+ detail: "Your credential helper, SSH config and agent, as they are."
454
524
  },
455
525
  {
456
526
  method: "ssh",
@@ -465,10 +535,14 @@ const METHODS = [
465
535
  ];
466
536
  function GitAuthForm({ saved, saving, saveError, savedNotice = false, onSave }) {
467
537
  const [form, setForm] = useState(() => formFromView(saved));
468
- const update = (patch) => setForm((current) => ({
469
- ...current,
470
- ...patch
471
- }));
538
+ const [edited, setEdited] = useState(false);
539
+ const update = (patch) => {
540
+ setEdited(true);
541
+ setForm((current) => ({
542
+ ...current,
543
+ ...patch
544
+ }));
545
+ };
472
546
  const problem = formProblem(form, saved);
473
547
  const tokenSaved = saved.https?.hasToken === true && saved.https.host === form.host.trim().toLowerCase();
474
548
  const host = form.host.trim() === "" ? "this host" : form.host.trim();
@@ -476,133 +550,156 @@ function GitAuthForm({ saved, saving, saveError, savedNotice = false, onSave })
476
550
  "data-testid": "git-auth-form",
477
551
  children: [
478
552
  /* @__PURE__ */ jsxs(SettingsSectionHeader, { children: [/* @__PURE__ */ jsx(SettingsSectionTitle, { children: "Connection" }), /* @__PURE__ */ jsx(SettingsSectionDescription, { children: "Worktree sessions use the setup of the workspace they belong to." })] }),
479
- /* @__PURE__ */ jsx(RadioGroup, {
553
+ /* @__PURE__ */ jsx("div", {
480
554
  "aria-label": "How git reaches your remote",
481
- value: form.method,
482
- onValueChange: (value) => update({ method: value }),
483
- className: "grid grid-cols-1 gap-3",
484
- children: METHODS.map((choice) => /* @__PURE__ */ jsxs(RadioGroupCard, {
485
- value: choice.method,
486
- "data-testid": `git-auth-method-${choice.method}`,
487
- className: "flex items-start gap-2 rounded-lg p-4",
488
- children: [/* @__PURE__ */ jsxs("span", {
489
- className: "flex min-w-0 flex-1 flex-col gap-1",
490
- children: [/* @__PURE__ */ jsx("span", {
491
- className: "text-base font-medium text-po-hi",
492
- children: choice.title
493
- }), /* @__PURE__ */ jsx("span", {
494
- className: "text-sm leading-relaxed text-po-dim",
495
- children: choice.detail
496
- })]
497
- }), form.method === choice.method ? /* @__PURE__ */ jsx(CheckIcon, { className: "size-4 shrink-0 text-po-green" }) : null]
555
+ className: "flex flex-col gap-3",
556
+ children: METHODS.map((choice) => /* @__PURE__ */ jsxs("div", {
557
+ className: `overflow-hidden rounded-lg border ${form.method === choice.method ? "border-po-green" : "border-po-border-soft"}`,
558
+ children: [
559
+ /* @__PURE__ */ jsxs("button", {
560
+ type: "button",
561
+ "aria-pressed": form.method === choice.method,
562
+ disabled: saving,
563
+ "data-testid": `git-auth-method-${choice.method}`,
564
+ onClick: () => update({ method: choice.method }),
565
+ className: `flex w-full items-start gap-3 p-4 text-left ${form.method === choice.method ? "bg-po-tint-green" : "bg-po-bg"}`,
566
+ children: [/* @__PURE__ */ jsxs("span", {
567
+ className: "flex flex-1 flex-col gap-1",
568
+ children: [/* @__PURE__ */ jsx("span", {
569
+ className: "text-base font-semibold text-po-hi",
570
+ children: choice.title
571
+ }), /* @__PURE__ */ jsx("span", {
572
+ className: "text-sm leading-relaxed text-po-dim",
573
+ children: choice.detail
574
+ })]
575
+ }), form.method === choice.method ? /* @__PURE__ */ jsx(CheckIcon, { className: "mt-0.5 size-4 shrink-0 text-po-green" }) : null]
576
+ }),
577
+ form.method === choice.method && form.method === "ssh" ? /* @__PURE__ */ jsxs("div", {
578
+ className: "flex flex-col gap-3 border-t border-po-border-soft p-4",
579
+ children: [/* @__PURE__ */ jsx(Field, {
580
+ label: "Key path",
581
+ children: /* @__PURE__ */ jsx(Input, {
582
+ "data-testid": "git-auth-key-path",
583
+ value: form.keyPath,
584
+ placeholder: "~/.ssh/id_ed25519",
585
+ spellCheck: false,
586
+ disabled: saving,
587
+ onChange: (event) => update({ keyPath: event.target.value }),
588
+ className: "font-mono"
589
+ })
590
+ }), /* @__PURE__ */ jsx("p", {
591
+ className: "text-sm leading-relaxed text-po-dim",
592
+ children: "Leave blank to use SSH config and ssh-agent. Passphrases need ssh-agent. Trust an unknown host in a terminal first."
593
+ })]
594
+ }) : null,
595
+ form.method === choice.method && form.method === "https" ? /* @__PURE__ */ jsxs("div", {
596
+ className: "flex flex-col gap-4 border-t border-po-border-soft p-4",
597
+ children: [
598
+ /* @__PURE__ */ jsxs("div", {
599
+ className: "grid grid-cols-2 gap-3",
600
+ children: [/* @__PURE__ */ jsx(Field, {
601
+ label: "Host",
602
+ children: /* @__PURE__ */ jsx(Input, {
603
+ "data-testid": "git-auth-host",
604
+ value: form.host,
605
+ spellCheck: false,
606
+ disabled: saving,
607
+ onChange: (event) => update({ host: event.target.value }),
608
+ className: "font-mono"
609
+ })
610
+ }), /* @__PURE__ */ jsx(Field, {
611
+ label: "Username",
612
+ children: /* @__PURE__ */ jsx(Input, {
613
+ "data-testid": "git-auth-username",
614
+ value: form.username,
615
+ autoComplete: "username",
616
+ spellCheck: false,
617
+ disabled: saving,
618
+ onChange: (event) => update({ username: event.target.value })
619
+ })
620
+ })]
621
+ }),
622
+ /* @__PURE__ */ jsx(Field, {
623
+ label: /* @__PURE__ */ jsxs(Fragment, { children: [
624
+ "Token",
625
+ " ",
626
+ tokenSaved ? /* @__PURE__ */ jsx("span", {
627
+ "data-testid": "git-auth-token-saved",
628
+ className: "ml-2 text-sm text-po-green",
629
+ children: "A token is saved"
630
+ }) : null
631
+ ] }),
632
+ children: /* @__PURE__ */ jsx(Input, {
633
+ "data-testid": "git-auth-token",
634
+ type: "password",
635
+ value: form.token,
636
+ autoComplete: "new-password",
637
+ placeholder: tokenSaved ? "Leave blank to keep it" : "Personal access token",
638
+ disabled: saving,
639
+ onChange: (event) => update({ token: event.target.value })
640
+ })
641
+ }),
642
+ /* @__PURE__ */ jsxs("p", {
643
+ className: "text-sm text-po-dim",
644
+ children: [
645
+ "Sent only to ",
646
+ /* @__PURE__ */ jsx("span", {
647
+ className: "font-mono",
648
+ children: host
649
+ }),
650
+ ". Other hosts use this machine's git."
651
+ ]
652
+ })
653
+ ]
654
+ }) : null
655
+ ]
498
656
  }, choice.method))
499
657
  }),
500
- form.method === "ssh" ? /* @__PURE__ */ jsx(SettingsCard, { children: /* @__PURE__ */ jsx(SettingsField, {
501
- id: "git-auth-key-path",
502
- label: "Key path",
503
- description: /* @__PURE__ */ jsxs(Fragment, { children: [
504
- "Leave it blank to use your SSH config and ssh-agent. A key with a passphrase works only through ssh-agent. An unknown host is refused: run ",
505
- /* @__PURE__ */ jsx("code", {
658
+ /* @__PURE__ */ jsxs(SettingsNote, {
659
+ className: "flex items-start gap-2 leading-relaxed",
660
+ children: [/* @__PURE__ */ jsx(ShieldIcon, {
661
+ "aria-hidden": true,
662
+ className: "mt-0.5 size-4 shrink-0"
663
+ }), /* @__PURE__ */ jsxs("span", { children: [
664
+ "Only cockpit pull, push and rebase use these credentials, not the agent.",
665
+ /* @__PURE__ */ jsx("br", {}),
666
+ "Stored in ",
667
+ /* @__PURE__ */ jsx("span", {
506
668
  className: "font-mono",
507
- children: "ssh -T git@github.com"
669
+ children: "~/.po/git/credentials.json"
508
670
  }),
509
- " once in a terminal to trust it."
510
- ] }),
511
- children: /* @__PURE__ */ jsx(Input, {
512
- id: "git-auth-key-path",
513
- "data-testid": "git-auth-key-path",
514
- value: form.keyPath,
515
- placeholder: "~/.ssh/id_ed25519",
516
- spellCheck: false,
517
- onChange: (event) => update({ keyPath: event.target.value }),
518
- className: "font-mono text-sm"
519
- })
520
- }) }) : null,
521
- form.method === "https" ? /* @__PURE__ */ jsxs(SettingsCard, { children: [
522
- /* @__PURE__ */ jsx(SettingsField, {
523
- id: "git-auth-host",
524
- label: "Host",
525
- description: "The one host this token is sent to.",
526
- children: /* @__PURE__ */ jsx(Input, {
527
- id: "git-auth-host",
528
- "data-testid": "git-auth-host",
529
- value: form.host,
530
- spellCheck: false,
531
- onChange: (event) => update({ host: event.target.value }),
532
- className: "font-mono text-sm"
533
- })
534
- }),
535
- /* @__PURE__ */ jsx(SettingsField, {
536
- id: "git-auth-username",
537
- label: "Username",
538
- children: /* @__PURE__ */ jsx(Input, {
539
- id: "git-auth-username",
540
- "data-testid": "git-auth-username",
541
- value: form.username,
542
- autoComplete: "username",
543
- spellCheck: false,
544
- onChange: (event) => update({ username: event.target.value }),
545
- className: "text-base"
546
- })
547
- }),
548
- /* @__PURE__ */ jsx(SettingsField, {
549
- id: "git-auth-token",
550
- label: "Token",
551
- description: /* @__PURE__ */ jsxs(Fragment, { children: [
552
- tokenSaved ? /* @__PURE__ */ jsxs("span", {
553
- "data-testid": "git-auth-token-saved",
554
- className: "text-po-green",
555
- children: ["A token is saved.", " "]
556
- }) : null,
557
- "Sent only to ",
558
- host,
559
- ". A remote on any other host keeps using this machine's own git credentials."
560
- ] }),
561
- children: /* @__PURE__ */ jsx(Input, {
562
- id: "git-auth-token",
563
- "data-testid": "git-auth-token",
564
- type: "password",
565
- value: form.token,
566
- autoComplete: "new-password",
567
- placeholder: tokenSaved ? "Leave blank to keep it" : "Personal access token",
568
- onChange: (event) => update({ token: event.target.value }),
569
- className: "text-base"
570
- })
571
- })
572
- ] }) : null,
671
+ ", readable only by you."
672
+ ] })]
673
+ }),
573
674
  /* @__PURE__ */ jsxs(SettingsActions, {
574
675
  className: "gap-4",
575
676
  children: [/* @__PURE__ */ jsx(Button, {
576
677
  variant: "primary",
577
- size: "sm",
578
678
  "data-testid": "git-auth-save",
579
679
  disabled: problem !== void 0 || saving,
580
680
  loading: saving,
581
- loadingLabel: "Saving",
582
- onClick: () => onSave(saveRequestOf(form)),
681
+ loadingLabel: "Saving...",
682
+ onClick: () => {
683
+ if (!saving && problem === void 0) onSave(saveRequestOf(form));
684
+ },
583
685
  children: "Save connection"
584
- }), problem !== void 0 ? /* @__PURE__ */ jsx(SettingsNote, {
585
- "data-testid": "git-auth-problem",
586
- className: "text-po-dim",
587
- children: problem
588
- }) : saveError !== void 0 ? /* @__PURE__ */ jsx(SettingsNote, {
589
- role: "alert",
590
- "data-testid": "git-auth-error",
591
- className: "text-po-red",
592
- children: saveError
593
- }) : savedNotice ? /* @__PURE__ */ jsx(SettingsNote, {
686
+ }), savedNotice && !edited && problem === void 0 && saveError === void 0 ? /* @__PURE__ */ jsx("span", {
594
687
  "data-testid": "git-auth-saved",
595
- className: "text-po-green",
688
+ className: "text-sm text-po-green",
596
689
  children: "Saved."
597
690
  }) : null]
598
691
  }),
599
- /* @__PURE__ */ jsxs(SettingsNote, {
600
- className: "flex items-start gap-2",
601
- children: [/* @__PURE__ */ jsx(ShieldIcon, {
602
- "aria-hidden": true,
603
- className: "mt-0.5 size-3.5 shrink-0"
604
- }), /* @__PURE__ */ jsx("span", { children: "Used only when you pull, push or rebase from this workspace's sessions in the cockpit, worktrees included. The agent's own git commands never see these credentials. Stored outside the repository in ~/.po/git/credentials.json, readable only by you, and never in the system keychain." })]
605
- })
692
+ problem !== void 0 ? /* @__PURE__ */ jsx("p", {
693
+ "data-testid": "git-auth-problem",
694
+ className: "text-sm text-po-red",
695
+ children: problem
696
+ }) : null,
697
+ saveError !== void 0 ? /* @__PURE__ */ jsx("p", {
698
+ role: "alert",
699
+ "data-testid": "git-auth-error",
700
+ className: "text-sm text-po-red",
701
+ children: saveError
702
+ }) : null
606
703
  ]
607
704
  });
608
705
  }
@@ -893,200 +990,394 @@ function requestWorktreeClose(sessionId, id) {
893
990
  });
894
991
  }
895
992
  //#endregion
993
+ //#region src/extensions/workspaces/sessions/(frontend)/fill/_components/WorktreeCreateDialog.tsx
994
+ function WorktreeSessionMenu({ sessionId, sessionReservationId }) {
995
+ return /* @__PURE__ */ jsx(DropdownMenuItem, {
996
+ disabled: sessionId === null,
997
+ "data-testid": `session-worktree-${sessionReservationId ?? sessionId}`,
998
+ onSelect: () => {
999
+ if (sessionId !== null) worktreeCreateDialog.update(() => ({
1000
+ sessionId,
1001
+ ...sessionReservationId === void 0 ? {} : { reservationId: sessionReservationId }
1002
+ }));
1003
+ },
1004
+ children: "Create worktree session..."
1005
+ });
1006
+ }
1007
+ function WorktreeCreateOverlay(props) {
1008
+ const target = useStore(worktreeCreateDialog.store);
1009
+ const { holdSessionChannels } = props;
1010
+ useEffect(() => {
1011
+ if (target === null) return;
1012
+ return holdSessionChannels?.(target.sessionId, (frame) => {
1013
+ if (frame.type !== worktreesChannel.channel) return;
1014
+ const payload = worktreesChannel.parse(frame.payload);
1015
+ if (payload !== null) worktreesChannel.apply(target.sessionId, payload);
1016
+ });
1017
+ }, [target, holdSessionChannels]);
1018
+ if (target === null) return null;
1019
+ return /* @__PURE__ */ jsx(Dialog, {
1020
+ open: true,
1021
+ onOpenChange: (open) => {
1022
+ if (!open) worktreeCreateDialog.update(() => null);
1023
+ },
1024
+ children: /* @__PURE__ */ jsxs(DialogContent, {
1025
+ width: "md",
1026
+ className: "w-96 overflow-y-auto border-0",
1027
+ "data-testid": "session-worktree-dialog",
1028
+ "aria-describedby": void 0,
1029
+ children: [/* @__PURE__ */ jsx(DialogTitle, {
1030
+ className: "sr-only",
1031
+ children: "Create a worktree session"
1032
+ }), /* @__PURE__ */ createElement(WorktreeCreateForm, {
1033
+ ...props,
1034
+ key: `${target.sessionId}:${target.reservationId ?? ""}`,
1035
+ sessionId: target.sessionId,
1036
+ sessionReservationId: target.reservationId,
1037
+ onCancel: () => worktreeCreateDialog.update(() => null),
1038
+ onCreated: () => worktreeCreateDialog.update(() => null)
1039
+ })]
1040
+ })
1041
+ });
1042
+ }
1043
+ function WorktreeCreateForm({ sessionId, sessionReservationId, onCancel, onRequested, onCreated }) {
1044
+ const session = useStore(worktreeActivity.store, (state) => worktreeActivity.select(state, sessionId));
1045
+ const [branch, setBranch] = useState("");
1046
+ const [baseRef, setBaseRef] = useState("");
1047
+ const sent = useRef(void 0);
1048
+ useEffect(() => {
1049
+ const request = sent.current;
1050
+ if (request === void 0) return;
1051
+ if (session.pending !== void 0) {
1052
+ request.sawPending = true;
1053
+ return;
1054
+ }
1055
+ if (!session.worktrees.some((worktree) => worktree.branch === request.branch) && !request.sawPending) return;
1056
+ sent.current = void 0;
1057
+ if (session.errorTarget?.action !== "create") onCreated?.();
1058
+ }, [
1059
+ session.pending,
1060
+ session.worktrees,
1061
+ session.errorTarget,
1062
+ onCreated
1063
+ ]);
1064
+ return /* @__PURE__ */ jsx(WorktreeCreateView, {
1065
+ branch,
1066
+ baseRef,
1067
+ onBranchChange: setBranch,
1068
+ onBaseChange: setBaseRef,
1069
+ busy: session.pending !== void 0,
1070
+ error: session.errorTarget?.action === "create" ? session.error : void 0,
1071
+ onCancel,
1072
+ onCreate: () => {
1073
+ if (sessionId !== null) {
1074
+ sent.current = {
1075
+ branch: branch.trim(),
1076
+ sawPending: false
1077
+ };
1078
+ onRequested?.(branch.trim());
1079
+ requestWorktreeCreate(sessionId, branch, baseRef, sessionReservationId);
1080
+ }
1081
+ },
1082
+ disabled: sessionId === null
1083
+ });
1084
+ }
1085
+ function WorktreeCreateView({ branch, baseRef, onBranchChange, onBaseChange, busy = false, error, onCancel, onCreate, disabled = false }) {
1086
+ const id = useId();
1087
+ return /* @__PURE__ */ jsxs("div", {
1088
+ className: "rounded-lg border border-po-border-soft bg-po-panel p-5 text-po-text shadow-sm",
1089
+ children: [
1090
+ /* @__PURE__ */ jsx("h2", {
1091
+ id: `${id}-title`,
1092
+ className: "text-xl font-semibold text-po-hi",
1093
+ children: "Create a worktree session"
1094
+ }),
1095
+ /* @__PURE__ */ jsx("p", {
1096
+ className: "mt-2 text-sm leading-relaxed text-po-dim",
1097
+ children: "Create a separate checkout and session without moving your existing sessions."
1098
+ }),
1099
+ /* @__PURE__ */ jsxs("form", {
1100
+ onSubmit: (event) => {
1101
+ event.preventDefault();
1102
+ if (!busy && !disabled && branch.trim()) onCreate?.();
1103
+ },
1104
+ children: [/* @__PURE__ */ jsxs("div", {
1105
+ className: "mt-5 flex flex-col gap-4",
1106
+ children: [/* @__PURE__ */ jsxs("div", {
1107
+ className: "flex flex-col gap-2",
1108
+ children: [
1109
+ /* @__PURE__ */ jsx("label", {
1110
+ htmlFor: `${id}-branch`,
1111
+ className: "text-base font-medium",
1112
+ children: "Branch"
1113
+ }),
1114
+ /* @__PURE__ */ jsx(Input, {
1115
+ id: `${id}-branch`,
1116
+ "data-testid": "git-worktree-branch",
1117
+ className: "font-mono",
1118
+ placeholder: "wt/fix-auth",
1119
+ value: branch,
1120
+ onChange: (e) => onBranchChange?.(e.target.value),
1121
+ disabled: busy,
1122
+ "aria-invalid": !!error,
1123
+ "aria-describedby": error ? `${id}-error` : void 0
1124
+ }),
1125
+ error && /* @__PURE__ */ jsx("p", {
1126
+ id: `${id}-error`,
1127
+ role: "alert",
1128
+ "data-testid": "git-worktree-error",
1129
+ className: "text-sm text-po-red",
1130
+ children: error
1131
+ })
1132
+ ]
1133
+ }), /* @__PURE__ */ jsxs("div", {
1134
+ className: "flex flex-col gap-2",
1135
+ children: [/* @__PURE__ */ jsx("label", {
1136
+ htmlFor: `${id}-base`,
1137
+ className: "text-base font-medium",
1138
+ children: "Start from"
1139
+ }), /* @__PURE__ */ jsx(Input, {
1140
+ size: "sm",
1141
+ id: `${id}-base`,
1142
+ "data-testid": "git-worktree-base",
1143
+ className: "font-mono",
1144
+ placeholder: "Current branch",
1145
+ value: baseRef,
1146
+ onChange: (e) => onBaseChange?.(e.target.value),
1147
+ disabled: busy
1148
+ })]
1149
+ })]
1150
+ }), /* @__PURE__ */ jsxs("footer", {
1151
+ className: "mt-5 flex items-center justify-end gap-2 border-t border-po-border-soft pt-4",
1152
+ children: [/* @__PURE__ */ jsx(Button, {
1153
+ type: "button",
1154
+ variant: "ghost",
1155
+ disabled: busy,
1156
+ onClick: onCancel,
1157
+ children: "Cancel"
1158
+ }), /* @__PURE__ */ jsxs(Button, {
1159
+ type: "submit",
1160
+ variant: "primary",
1161
+ "data-testid": "git-worktree-create",
1162
+ disabled: busy || disabled || !branch.trim(),
1163
+ children: [busy && /* @__PURE__ */ jsx(Spinner, {
1164
+ className: "size-3",
1165
+ label: "Creating worktree"
1166
+ }), busy ? "Creating worktree..." : "Create worktree"]
1167
+ })]
1168
+ })]
1169
+ })
1170
+ ]
1171
+ });
1172
+ }
1173
+ //#endregion
896
1174
  //#region src/extensions/workspaces/sessions/(frontend)/_components/WorktreesPanel.tsx
897
- /**
898
- * The '# git' group's tab: this session's worktrees, and the form that makes one.
899
- *
900
- * DESIGN PATTERNS:
901
- * - Lives in the activity dock, opened by the group's name. A worktree is work
902
- * in progress belonging to the session that started it, so it sits beside
903
- * agents, runners and workflows rather than in a settings screen.
904
- * - Header, form, then cards, the same rhythm the runners and workflows panels
905
- * use. A worktree row is a card because it carries three facts and two
906
- * actions, which a single line cannot hold without becoming a puzzle.
907
- * - Local form state is never reset by a data refresh. Direct lifecycle events
908
- * update the list without discarding a half-typed branch name.
909
- * - Acts through the plugin's own hub channel, so a worktree made here and one
910
- * made by the tool take exactly the same path.
911
- * - Creating waits on the cockpit starting a session, so the hub reports each
912
- * phase on that channel and the header shows it turning. A button that looks
913
- * idle for two minutes reads as broken.
914
- *
915
- * AVOID:
916
- * - Deriving form state from refreshed store data instead of the user's input.
917
- * - Colour classes outside the token set. `po-error`, `po-line` and
918
- * `po-bright` are not tokens: they compile to nothing and the row silently
919
- * loses its border.
920
- */
921
- const WORKTREES_TAB_ID = "git-worktrees";
922
1175
  function worktreesTab() {
923
1176
  return {
924
- id: WORKTREES_TAB_ID,
1177
+ id: "git-worktrees",
925
1178
  label: "worktrees",
926
1179
  panel: WorktreesPanel
927
1180
  };
928
1181
  }
929
- function WorktreesPanel({ sessionId, sessionReservationId }) {
930
- const session = useStore(worktreeActivity.store, (state) => worktreeActivity.select(state, sessionId));
931
- const [branch, setBranch] = useState("");
932
- const [baseRef, setBaseRef] = useState("");
933
- const busy = session.pending !== void 0;
934
- const canCreate = sessionId !== null && branch.trim() !== "" && !busy;
935
- const createError = session.errorTarget?.action === "create" ? session.error : void 0;
936
- const closeErrorId = session.errorTarget?.action === "close" ? session.errorTarget.id : void 0;
937
- const panelError = session.error !== void 0 && (session.errorTarget === void 0 || closeErrorId !== void 0 && !session.worktrees.some((worktree) => worktree.id === closeErrorId));
938
- const create = () => {
939
- if (sessionId === null || !canCreate) return;
940
- requestWorktreeCreate(sessionId, branch, baseRef, sessionReservationId);
941
- if (sessionReservationId === void 0) {
942
- setBranch("");
943
- setBaseRef("");
944
- }
945
- };
1182
+ function WorktreesPanel(props) {
1183
+ const session = useStore(worktreeActivity.store, (state) => worktreeActivity.select(state, props.sessionId));
1184
+ const [creating, setCreating] = useState(session.errorTarget?.action === "create");
1185
+ const [requestedBranch, setRequestedBranch] = useState();
1186
+ const sawPending = useRef(false);
1187
+ useEffect(() => {
1188
+ if (!requestedBranch) return;
1189
+ if (session.worktrees.some((w) => w.branch === requestedBranch) || sawPending.current && session.pending === void 0) setRequestedBranch(void 0);
1190
+ else if (session.pending !== void 0) sawPending.current = true;
1191
+ }, [
1192
+ requestedBranch,
1193
+ session.pending,
1194
+ session.worktrees
1195
+ ]);
1196
+ return /* @__PURE__ */ jsx("div", {
1197
+ className: "min-h-0 flex-1 overflow-auto",
1198
+ children: /* @__PURE__ */ jsx(WorktreesPanelView, {
1199
+ worktrees: session.worktrees,
1200
+ pending: session.pending,
1201
+ startingBranch: session.pending === void 0 || session.worktrees.some((w) => w.branch === requestedBranch) ? void 0 : requestedBranch,
1202
+ error: session.errorTarget?.action === "create" && creating ? void 0 : session.error,
1203
+ closeErrorId: session.errorTarget?.action === "close" ? session.errorTarget.id : void 0,
1204
+ onNew: () => setCreating(true),
1205
+ onClose: props.sessionId === null ? void 0 : (id) => requestWorktreeClose(props.sessionId, id),
1206
+ form: creating ? /* @__PURE__ */ jsx(WorktreeCreateForm, {
1207
+ ...props,
1208
+ onCancel: () => setCreating(false),
1209
+ onCreated: () => setCreating(false),
1210
+ onRequested: (branch) => {
1211
+ sawPending.current = false;
1212
+ setRequestedBranch(branch);
1213
+ }
1214
+ }) : void 0
1215
+ })
1216
+ });
1217
+ }
1218
+ function WorktreesPanelView({ worktrees, pending, error, closeErrorId, onNew, onClose, form, startingIds = [], startingBranch }) {
1219
+ const groups = [worktrees.filter((w) => !w.orphaned && !w.unowned), worktrees.filter((w) => w.orphaned || w.unowned)];
946
1220
  return /* @__PURE__ */ jsxs("div", {
947
1221
  "data-testid": "git-worktrees-panel",
948
- className: "flex min-h-0 flex-1 flex-col overflow-auto",
1222
+ className: "rounded-lg border border-po-border-soft bg-po-panel p-5",
949
1223
  children: [
950
- /* @__PURE__ */ jsxs("div", {
951
- className: "flex min-h-11 shrink-0 items-center gap-2.5 border-b border-po-border-soft px-3 sm:px-[26px]",
1224
+ /* @__PURE__ */ jsxs("header", {
1225
+ className: "flex items-center justify-between gap-4 border-b border-po-border-soft pb-4",
952
1226
  children: [
953
- /* @__PURE__ */ jsx("span", {
1227
+ /* @__PURE__ */ jsx("h2", {
954
1228
  className: "text-lg font-semibold text-po-hi",
955
- children: "worktrees"
1229
+ children: "Worktrees"
956
1230
  }),
957
1231
  /* @__PURE__ */ jsx("span", {
958
1232
  "data-testid": "git-worktrees-count",
959
- className: "text-sm text-po-dim",
960
- children: session.worktrees.length === 0 ? "none yet" : `${String(session.worktrees.length)} open`
1233
+ className: "sr-only",
1234
+ children: worktrees.length === 0 ? "none yet" : `${worktrees.length} open`
961
1235
  }),
962
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
963
- session.pending === void 0 ? null : /* @__PURE__ */ jsxs("span", {
964
- "data-testid": "git-worktree-pending",
965
- className: "flex min-w-0 items-center gap-1.5",
966
- children: [/* @__PURE__ */ jsx(Spinner, {
967
- className: "h-3 w-3 shrink-0 text-po-faint",
968
- label: session.pending
969
- }), /* @__PURE__ */ jsx("span", {
970
- className: "min-w-0 truncate text-sm text-po-dim",
971
- children: session.pending
972
- })]
1236
+ /* @__PURE__ */ jsx(Button, {
1237
+ variant: "outline",
1238
+ size: "sm",
1239
+ onClick: onNew,
1240
+ children: "New worktree"
973
1241
  })
974
1242
  ]
975
1243
  }),
976
- /* @__PURE__ */ jsxs("div", {
977
- className: "flex shrink-0 flex-col gap-1.5 border-b border-po-border-soft px-3 py-2.5 sm:px-[26px]",
978
- children: [/* @__PURE__ */ jsxs("div", {
979
- className: "flex items-center gap-2",
980
- children: [
981
- /* @__PURE__ */ jsx(Input, {
982
- "data-testid": "git-worktree-branch",
983
- "aria-label": "Branch",
984
- size: "sm",
985
- className: "min-w-0 flex-1",
986
- placeholder: "wt/fix-auth",
987
- value: branch,
988
- onChange: (event) => setBranch(event.target.value),
989
- onKeyDown: (event) => {
990
- if (event.key === "Enter") create();
991
- }
992
- }),
993
- /* @__PURE__ */ jsx(Input, {
994
- "data-testid": "git-worktree-base",
995
- "aria-label": "Base ref",
996
- size: "sm",
997
- placeholder: "base (current)",
998
- value: baseRef,
999
- onChange: (event) => setBaseRef(event.target.value),
1000
- className: "w-32 shrink-0"
1001
- }),
1002
- /* @__PURE__ */ jsx(Button, {
1003
- "data-testid": "git-worktree-create",
1004
- size: "sm",
1005
- disabled: !canCreate,
1006
- onClick: create,
1007
- children: "Create"
1008
- })
1009
- ]
1010
- }), createError === void 0 ? /* @__PURE__ */ jsx("p", {
1011
- className: "text-sm text-po-dim",
1012
- children: "Creates the worktree outside the repository and starts a session nested under this one."
1013
- }) : /* @__PURE__ */ jsx("p", {
1014
- role: "alert",
1015
- "data-testid": "git-worktree-error",
1016
- className: "text-sm text-po-red",
1017
- children: createError
1244
+ form,
1245
+ pending && !startingBranch && /* @__PURE__ */ jsxs("p", {
1246
+ "data-testid": "git-worktree-pending",
1247
+ className: "flex items-center gap-2 pt-3 text-sm text-po-dim",
1248
+ children: [/* @__PURE__ */ jsx(Spinner, {
1249
+ className: "size-3",
1250
+ label: pending
1251
+ }), /* @__PURE__ */ jsx("span", {
1252
+ className: "inline-block first-letter:uppercase",
1253
+ children: pending
1018
1254
  })]
1019
1255
  }),
1020
- panelError ? /* @__PURE__ */ jsx("p", {
1256
+ error && (!closeErrorId || !worktrees.some((w) => w.id === closeErrorId)) && /* @__PURE__ */ jsx("p", {
1021
1257
  role: "alert",
1022
1258
  "data-testid": "git-worktree-error",
1023
- className: "px-3 pt-2 text-sm text-po-red sm:px-[26px]",
1024
- children: session.error
1025
- }) : null,
1026
- session.worktrees.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
1259
+ className: "text-sm text-po-red",
1260
+ children: error
1261
+ }),
1262
+ worktrees.length === 0 && !startingBranch ? /* @__PURE__ */ jsx(EmptyState, {
1027
1263
  "data-testid": "git-worktrees-empty",
1028
- className: "px-4 py-8",
1029
- title: "no worktrees yet",
1030
- description: "a worktree is its own checkout on its own branch, with its own session nested under this one."
1031
- }) : /* @__PURE__ */ jsx("ul", {
1264
+ className: "py-10",
1265
+ title: "No worktrees yet",
1266
+ description: "A worktree gives a branch its own checkout and a session nested under this one."
1267
+ }) : /* @__PURE__ */ jsx("div", {
1032
1268
  "data-testid": "git-worktrees-list",
1033
- className: "flex flex-col gap-2.5 px-3 py-3 sm:px-[26px]",
1034
- children: session.worktrees.map((worktree) => /* @__PURE__ */ jsx(WorktreeCard, {
1035
- worktree,
1036
- sessionId,
1037
- busy,
1038
- error: closeErrorId === worktree.id ? session.error : void 0
1039
- }, worktree.id))
1269
+ children: groups.map((group, i) => (group.length > 0 || i === 0 && startingBranch) && /* @__PURE__ */ jsxs("section", { children: [
1270
+ /* @__PURE__ */ jsxs("h3", {
1271
+ className: "flex items-center gap-2 pt-5 text-lg font-semibold text-po-hi",
1272
+ children: [
1273
+ i === 0 ? "Working" : "Needs a look",
1274
+ " ",
1275
+ /* @__PURE__ */ jsxs("span", {
1276
+ className: "text-sm font-normal text-po-dim",
1277
+ children: [group.length + (i === 0 && startingBranch ? 1 : 0), " open"]
1278
+ })
1279
+ ]
1280
+ }),
1281
+ i === 1 && /* @__PURE__ */ jsxs("p", {
1282
+ className: "pt-2 text-sm leading-relaxed text-po-dim",
1283
+ children: [
1284
+ group.length === 2 ? "Two worktrees" : group.length === 1 ? "One worktree" : `${group.length} worktrees`,
1285
+ " ",
1286
+ "would like your eye, whenever you are ready."
1287
+ ]
1288
+ }),
1289
+ i === 0 && startingBranch && /* @__PURE__ */ jsxs("div", {
1290
+ "data-testid": "git-worktree-starting",
1291
+ className: "flex min-w-0 flex-col gap-2 border-b border-po-border-soft py-4",
1292
+ children: [/* @__PURE__ */ jsxs("div", {
1293
+ className: "flex min-w-0 items-center gap-2",
1294
+ children: [/* @__PURE__ */ jsx(Spinner, {
1295
+ className: "size-3 shrink-0 text-po-dim",
1296
+ label: "Starting session"
1297
+ }), /* @__PURE__ */ jsx("span", {
1298
+ className: "min-w-0 flex-1 truncate font-mono text-base font-medium text-po-hi",
1299
+ children: startingBranch
1300
+ })]
1301
+ }), /* @__PURE__ */ jsx("p", {
1302
+ className: "pl-4 text-sm text-po-dim",
1303
+ children: "Starting session..."
1304
+ })]
1305
+ }),
1306
+ group.map((w) => /* @__PURE__ */ jsx(WorktreeRow, {
1307
+ worktree: w,
1308
+ starting: startingIds.includes(w.id) || !!pending?.includes("starting") && w.sessionId === null && !w.orphaned && !w.unowned,
1309
+ disabled: !!pending || !onClose,
1310
+ onClose: () => onClose?.(w.id),
1311
+ error: closeErrorId === w.id ? error : void 0
1312
+ }, w.id))
1313
+ ] }, i))
1040
1314
  })
1041
1315
  ]
1042
1316
  });
1043
1317
  }
1044
- /** One worktree: what it is, where it is, and the one thing you can do to it. */
1045
- function WorktreeCard({ worktree, sessionId, busy, error }) {
1046
- return /* @__PURE__ */ jsxs("li", {
1318
+ function WorktreeBadge({ worktree }) {
1319
+ if (worktree.orphaned) return /* @__PURE__ */ jsx(StatusBadge, {
1320
+ tone: "neutral",
1321
+ size: "xs",
1322
+ className: "bg-po-tint-yellow text-po-yellow",
1323
+ children: "Orphaned"
1324
+ });
1325
+ if (worktree.unowned) return /* @__PURE__ */ jsx(StatusBadge, {
1326
+ tone: "info",
1327
+ size: "xs",
1328
+ children: "Unowned"
1329
+ });
1330
+ return null;
1331
+ }
1332
+ function WorktreeRow({ worktree, compact = false, starting = false, error, disabled = false, onClose }) {
1333
+ return /* @__PURE__ */ jsxs("div", {
1047
1334
  "data-testid": `git-worktree-${worktree.id}`,
1048
- className: "flex min-w-0 flex-col gap-1.5 rounded-md border border-po-border-soft bg-po-panel p-2.5",
1335
+ className: compact ? "flex min-w-0 flex-col gap-1 py-2" : "flex min-w-0 flex-col gap-2 border-b border-po-border-soft py-4",
1049
1336
  children: [
1050
1337
  /* @__PURE__ */ jsxs("div", {
1051
- className: "flex min-w-0 items-center gap-1.5",
1338
+ className: "flex min-w-0 items-center gap-2",
1052
1339
  children: [
1053
- /* @__PURE__ */ jsx(Dot, { tone: worktree.orphaned ? "red" : worktree.sessionId === null ? "muted" : "blue" }),
1340
+ starting ? /* @__PURE__ */ jsx(Spinner, {
1341
+ className: "size-3 shrink-0 text-po-dim",
1342
+ label: "Starting session"
1343
+ }) : /* @__PURE__ */ jsx(Dot, {
1344
+ size: "md",
1345
+ tone: worktree.orphaned ? "yellow" : worktree.unowned ? "blue" : "green"
1346
+ }),
1054
1347
  /* @__PURE__ */ jsx("span", {
1055
- className: "min-w-0 flex-1 truncate text-base font-semibold text-po-hi",
1348
+ title: worktree.branch,
1349
+ className: "min-w-0 flex-1 truncate font-mono text-base font-medium text-po-hi",
1056
1350
  children: worktree.branch
1057
1351
  }),
1058
- worktree.orphaned ? /* @__PURE__ */ jsx(StatusBadge, {
1059
- tone: "error",
1060
- size: "xs",
1061
- children: "orphaned"
1062
- }) : null,
1063
- worktree.unowned ? /* @__PURE__ */ jsx(StatusBadge, {
1064
- tone: "info",
1065
- size: "xs",
1066
- children: "unowned"
1067
- }) : null
1352
+ /* @__PURE__ */ jsx(WorktreeBadge, { worktree }),
1353
+ !compact && /* @__PURE__ */ jsx(Button, {
1354
+ variant: "ghost",
1355
+ size: "sm",
1356
+ onClick: onClose,
1357
+ disabled: starting || disabled,
1358
+ "data-testid": `git-worktree-close-${worktree.id}`,
1359
+ "aria-label": `Close ${worktree.branch}`,
1360
+ children: "Close"
1361
+ })
1068
1362
  ]
1069
1363
  }),
1070
- /* @__PURE__ */ jsx("span", {
1071
- className: "min-w-0 truncate text-sm text-po-dim",
1364
+ !compact && /* @__PURE__ */ jsx("p", {
1365
+ className: "truncate pl-4 font-mono text-sm text-po-dim",
1366
+ title: worktree.path,
1072
1367
  children: worktree.path
1073
1368
  }),
1074
- /* @__PURE__ */ jsxs("div", {
1075
- className: "flex items-center gap-1.5 pt-0.5",
1076
- children: [/* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }), sessionId === null ? null : /* @__PURE__ */ jsx(Button, {
1077
- variant: "danger-outline",
1078
- size: "xs",
1079
- disabled: busy,
1080
- "data-testid": `git-worktree-close-${worktree.id}`,
1081
- className: "px-2 ",
1082
- onClick: () => requestWorktreeClose(sessionId, worktree.id),
1083
- children: "close"
1084
- })]
1369
+ starting && /* @__PURE__ */ jsx("p", {
1370
+ className: "pl-4 text-sm text-po-dim",
1371
+ children: "Starting session..."
1372
+ }),
1373
+ !compact && (worktree.orphaned || worktree.unowned) && /* @__PURE__ */ jsx("p", {
1374
+ className: "pl-4 text-sm text-po-dim first-letter:uppercase",
1375
+ children: worktree.orphaned ? "its folder is missing" : "its session has ended"
1085
1376
  }),
1086
- error === void 0 ? null : /* @__PURE__ */ jsx("p", {
1377
+ error && /* @__PURE__ */ jsx("p", {
1087
1378
  role: "alert",
1088
1379
  "data-testid": "git-worktree-error",
1089
- className: "text-sm text-po-red",
1380
+ className: "pl-4 text-sm text-po-red",
1090
1381
  children: error
1091
1382
  })
1092
1383
  ]
@@ -1264,50 +1555,6 @@ var gitChanges_web_default = defineChannelFile(gitChangesChannel);
1264
1555
  //#region src/extensions/workspaces/sessions/(frontend)/channel/gitWorktrees.web.ts
1265
1556
  var gitWorktrees_web_default = defineChannelFile(worktreesChannel);
1266
1557
  //#endregion
1267
- //#region src/extensions/workspaces/sessions/(frontend)/fill/_components/WorktreeCreateDialog.tsx
1268
- function WorktreeSessionMenu({ sessionId, sessionReservationId }) {
1269
- return /* @__PURE__ */ jsx(DropdownMenuItem, {
1270
- disabled: sessionId === null,
1271
- "data-testid": `session-worktree-${sessionReservationId ?? sessionId}`,
1272
- onSelect: () => {
1273
- if (sessionId !== null) worktreeCreateDialog.update(() => ({
1274
- sessionId,
1275
- ...sessionReservationId === void 0 ? {} : { reservationId: sessionReservationId }
1276
- }));
1277
- },
1278
- children: "create worktree session…"
1279
- });
1280
- }
1281
- function WorktreeCreateOverlay(props) {
1282
- const target = useStore(worktreeCreateDialog.store);
1283
- const { holdSessionChannels } = props;
1284
- useEffect(() => {
1285
- if (target === null) return;
1286
- return holdSessionChannels?.(target.sessionId, (frame) => {
1287
- if (frame.type !== worktreesChannel.channel) return;
1288
- const payload = worktreesChannel.parse(frame.payload);
1289
- if (payload !== null) worktreesChannel.apply(target.sessionId, payload);
1290
- });
1291
- }, [target, holdSessionChannels]);
1292
- if (target === null) return null;
1293
- return /* @__PURE__ */ jsx(Dialog, {
1294
- open: true,
1295
- onOpenChange: (open) => {
1296
- if (!open) worktreeCreateDialog.update(() => null);
1297
- },
1298
- children: /* @__PURE__ */ jsxs(DialogContent, {
1299
- width: "lg",
1300
- "data-testid": "session-worktree-dialog",
1301
- children: [/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: "create worktree session" }) }), /* @__PURE__ */ jsxs(DialogBody, { children: [/* @__PURE__ */ jsx(DialogDescription, { children: "Creates a separate checkout and a new session. Existing sessions stay in their current directories." }), /* @__PURE__ */ createElement(WorktreesPanel, {
1302
- ...props,
1303
- key: `${target.sessionId}:${target.reservationId ?? ""}`,
1304
- sessionId: target.sessionId,
1305
- sessionReservationId: target.reservationId
1306
- })] })]
1307
- })
1308
- });
1309
- }
1310
- //#endregion
1311
1558
  //#region src/extensions/workspaces/sessions/(frontend)/fill/Worktree.session-menu.web.tsx
1312
1559
  var Worktree_session_menu_web_default = defineFill({ component: WorktreeSessionMenu });
1313
1560
  //#endregion
@@ -1411,6 +1658,23 @@ function conflictPrompt(branch, conflicts) {
1411
1658
  ].join("\n");
1412
1659
  }
1413
1660
  /**
1661
+ * The one sync the bar fills in: pull while the branch is behind (a push would
1662
+ * be rejected), push while it is only ahead or has no upstream yet. Nothing
1663
+ * while a rebase is paused, detached, or already level.
1664
+ */
1665
+ function suggestedSyncAction(changes) {
1666
+ if (changes?.branch === void 0 || changes.rebase !== void 0) return void 0;
1667
+ if (changes.upstream === void 0) return "push";
1668
+ if (changes.upstream.behind > 0) return "pull";
1669
+ return changes.upstream.ahead > 0 ? "push" : void 0;
1670
+ }
1671
+ /** "2 to push, 1 new on origin/feat/git", or "Up to date with origin/feat/git". */
1672
+ function upstreamSummary(upstream) {
1673
+ if (upstream === void 0) return "No upstream yet";
1674
+ const parts = [...upstream.ahead > 0 ? [`${String(upstream.ahead)} to push`] : [], ...upstream.behind > 0 ? [`${String(upstream.behind)} new on ${upstream.ref}`] : []];
1675
+ return parts.length === 0 ? `Up to date with ${upstream.ref}` : parts.join(", ");
1676
+ }
1677
+ /**
1414
1678
  * Hands one deferred follow-up to the session. Returns the reason it failed,
1415
1679
  * or undefined once sent. The sender throws when the session is disconnected.
1416
1680
  */
@@ -1425,486 +1689,303 @@ function deliverToAgent(send, sessionId, message) {
1425
1689
  return error instanceof Error ? error.message : "The session did not take the message.";
1426
1690
  }
1427
1691
  }
1428
- //#endregion
1429
- //#region src/extensions/workspaces/sessions/(frontend)/_lib/gitReviewTree.ts
1430
- function buildTree(files) {
1431
- const root = {
1432
- path: "",
1433
- dirs: /* @__PURE__ */ new Map(),
1434
- files: []
1435
- };
1436
- for (const file of files) {
1437
- const parts = file.path.split("/");
1438
- parts.pop();
1439
- let dir = root;
1440
- for (const part of parts) {
1441
- let next = dir.dirs.get(part);
1442
- if (next === void 0) {
1443
- next = {
1444
- path: dir.path === "" ? part : `${dir.path}/${part}`,
1445
- dirs: /* @__PURE__ */ new Map(),
1446
- files: []
1447
- };
1448
- dir.dirs.set(part, next);
1449
- }
1450
- dir = next;
1451
- }
1452
- dir.files.push(file);
1453
- }
1454
- return root;
1455
- }
1456
- /** Folders first, then files, each alphabetical; single-child folder chains render as one row. */
1457
- function flattenTree(files, isOpen) {
1458
- const rows = [];
1459
- const walk = (dir, depth) => {
1460
- for (const [segment, child] of [...dir.dirs].sort(([left], [right]) => left.localeCompare(right))) {
1461
- let name = segment;
1462
- let node = child;
1463
- while (node.files.length === 0 && node.dirs.size === 1) {
1464
- const [[nextName, next]] = [...node.dirs];
1465
- name = `${name}/${nextName}`;
1466
- node = next;
1467
- }
1468
- const open = isOpen(node.path);
1469
- rows.push({
1470
- kind: "dir",
1471
- path: node.path,
1472
- name,
1473
- depth,
1474
- open
1475
- });
1476
- if (open) walk(node, depth + 1);
1477
- }
1478
- for (const file of [...dir.files].sort((left, right) => left.path.localeCompare(right.path))) rows.push({
1479
- kind: "file",
1480
- file,
1481
- depth
1482
- });
1483
- };
1484
- walk(buildTree(files), 0);
1485
- return rows;
1486
- }
1487
- //#endregion
1488
- //#region src/extensions/workspaces/sessions/(frontend)/_components/GitReviewBrowser.tsx
1489
- /**
1490
- * The review tab's file list: every changed file as a folder tree (the VS Code
1491
- * explorer shape), with its status, its line counts and its comments.
1492
- *
1493
- * DESIGN PATTERNS:
1494
- * - Picking a row scrolls the diff pane to that file; the list never opens a
1495
- * tab of its own. One tab holds the whole review.
1496
- * - Rows show only the file name, so a long path never truncates away the part
1497
- * that tells files apart. Folders say the rest; the full path is the tooltip.
1498
- * - Folder chains with a single child collapse into one row (`src/web/lib`),
1499
- * like VS Code's compact folders, so deep trees stay shallow.
1500
- * - Folders fold on click; a search shows every match with its folders open.
1501
- * - Each row is a button, reachable with Tab; arrow keys move and pick in one
1502
- * step across the visible files, so the diff follows the cursor.
1503
- *
1504
- * AVOID:
1505
- * - Colour classes outside the token set.
1506
- */
1507
- const STATUS_MARK = {
1508
- added: {
1509
- letter: "A",
1510
- tone: "text-po-green",
1511
- label: "added"
1512
- },
1513
- modified: {
1514
- letter: "M",
1515
- tone: "text-po-yellow",
1516
- label: "modified"
1517
- },
1518
- deleted: {
1519
- letter: "D",
1520
- tone: "text-po-red",
1521
- label: "deleted"
1522
- },
1523
- untracked: {
1524
- letter: "U",
1525
- tone: "text-po-blue",
1526
- label: "untracked"
1527
- },
1528
- conflicted: {
1529
- letter: "C",
1530
- tone: "text-po-red",
1531
- label: "conflicted"
1532
- }
1692
+ //#endregion
1693
+ //#region src/extensions/workspaces/sessions/(frontend)/_components/GitReviewBrowser.tsx
1694
+ const status = {
1695
+ added: [
1696
+ "A",
1697
+ "text-po-green",
1698
+ "Added"
1699
+ ],
1700
+ modified: [
1701
+ "M",
1702
+ "text-po-yellow",
1703
+ "Modified"
1704
+ ],
1705
+ deleted: [
1706
+ "D",
1707
+ "text-po-red",
1708
+ "Deleted"
1709
+ ],
1710
+ untracked: [
1711
+ "U",
1712
+ "text-po-blue",
1713
+ "Untracked"
1714
+ ],
1715
+ conflicted: [
1716
+ "C",
1717
+ "text-po-red",
1718
+ "Conflicted"
1719
+ ]
1533
1720
  };
1534
- const baseName = (path) => path.slice(path.lastIndexOf("/") + 1);
1535
- /** Left padding per depth level; tight so deep files keep room for their names. */
1536
- const indent = (depth) => ({ paddingLeft: `${String(.75 + depth * .5)}rem` });
1537
1721
  function GitReviewBrowser({ files, activePath, commentCounts, onPick }) {
1538
1722
  const [query, setQuery] = useState("");
1539
- const [collapsed, setCollapsed] = useState(/* @__PURE__ */ new Set());
1540
- const needle = query.trim().toLocaleLowerCase();
1541
- const shown = needle === "" ? files : files.filter((file) => file.path.toLocaleLowerCase().includes(needle));
1542
- const rows = flattenTree(shown, (dirPath) => needle !== "" || !collapsed.has(dirPath));
1543
- const ordered = rows.flatMap((row) => row.kind === "file" ? [row.file] : []);
1723
+ const shown = files.filter((f) => f.path.toLowerCase().includes(query.trim().toLowerCase()));
1544
1724
  const listRef = useRef(null);
1545
1725
  useEffect(() => {
1546
1726
  if (activePath === void 0) return;
1547
- (listRef.current?.querySelector(`[data-path="${CSS.escape(activePath)}"]`))?.scrollIntoView({ block: "nearest" });
1727
+ listRef.current?.querySelector(`[data-path="${CSS.escape(activePath)}"]`)?.scrollIntoView({ block: "nearest" });
1548
1728
  }, [activePath]);
1549
- const toggle = (dirPath) => {
1550
- setCollapsed((prev) => {
1551
- const next = new Set(prev);
1552
- if (!next.delete(dirPath)) next.add(dirPath);
1553
- return next;
1554
- });
1555
- };
1556
- const allDirs = () => new Set(flattenTree(files, () => true).flatMap((row) => row.kind === "dir" ? [row.path] : []));
1557
1729
  /** Arrow keys pick the neighbouring row and move focus with it, so the diff follows the cursor. */
1558
1730
  const onKeyDown = (event) => {
1559
1731
  if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
1560
- if (ordered.length === 0) return;
1732
+ if (shown.length === 0) return;
1561
1733
  event.preventDefault();
1562
- const current = ordered.findIndex((file) => file.path === activePath);
1734
+ const current = shown.findIndex((file) => file.path === activePath);
1563
1735
  const step = event.key === "ArrowDown" ? 1 : -1;
1564
- const next = current === -1 ? 0 : (current + step + ordered.length) % ordered.length;
1565
- const file = ordered[next];
1736
+ const next = current === -1 ? 0 : (current + step + shown.length) % shown.length;
1737
+ const file = shown[next];
1566
1738
  if (file === void 0) return;
1567
1739
  onPick(file.path);
1568
1740
  event.currentTarget.closest("[data-git-review-list]")?.querySelectorAll("[data-git-review-row]")[next]?.focus();
1569
1741
  };
1570
1742
  return /* @__PURE__ */ jsxs("nav", {
1743
+ "aria-label": "Changed files",
1571
1744
  "data-testid": "git-review-browser",
1572
- "aria-label": "changed files",
1573
- className: "flex min-h-0 w-64 shrink-0 flex-col border-r border-po-border-soft",
1745
+ className: "flex min-h-0 w-80 shrink-0 flex-col border-r border-po-border-soft",
1574
1746
  children: [/* @__PURE__ */ jsxs("div", {
1575
- className: "flex shrink-0 flex-col gap-1 px-3 pt-2.5 pb-1.5",
1747
+ className: "flex flex-col gap-3 border-b border-po-border-soft p-3",
1576
1748
  children: [/* @__PURE__ */ jsx(Input, {
1577
1749
  "data-testid": "git-review-search",
1578
- "aria-label": "search changed file paths",
1579
1750
  size: "sm",
1751
+ "aria-label": "Search paths",
1752
+ placeholder: "Search paths",
1580
1753
  value: query,
1581
- placeholder: "search paths…",
1582
- onChange: (event) => setQuery(event.target.value)
1583
- }), /* @__PURE__ */ jsxs("div", {
1584
- className: "flex items-center justify-between gap-2",
1585
- children: [/* @__PURE__ */ jsx("span", {
1586
- className: "text-sm text-po-dim",
1587
- children: needle === "" ? `${String(files.length)} changed` : `${String(shown.length)} of ${String(files.length)} match`
1588
- }), needle === "" ? /* @__PURE__ */ jsx(Button, {
1589
- variant: "link",
1590
- size: "xs",
1591
- "data-testid": "git-review-fold-all",
1592
- className: "h-auto px-0",
1593
- onClick: () => setCollapsed(collapsed.size === 0 ? allDirs() : /* @__PURE__ */ new Set()),
1594
- children: collapsed.size === 0 ? "collapse all" : "expand all"
1595
- }) : null]
1754
+ onChange: (e) => setQuery(e.target.value)
1755
+ }), /* @__PURE__ */ jsxs("span", {
1756
+ className: "text-sm text-po-dim",
1757
+ children: [shown.length, " changed"]
1596
1758
  })]
1597
1759
  }), /* @__PURE__ */ jsxs("div", {
1598
1760
  ref: listRef,
1599
1761
  "data-git-review-list": true,
1600
- className: "min-h-0 flex-1 overflow-y-auto pb-2",
1601
- children: [rows.map((row) => {
1602
- if (row.kind === "dir") {
1603
- const Chevron = row.open ? ChevronDownIcon : ChevronRightIcon;
1604
- return /* @__PURE__ */ jsxs(Button, {
1605
- variant: "ghost",
1606
- size: "card",
1607
- "aria-expanded": row.open,
1608
- "data-testid": `git-review-browser-dir-${row.path}`,
1609
- title: row.path,
1610
- onClick: () => toggle(row.path),
1611
- style: indent(row.depth),
1612
- className: "min-w-0 flex-row items-center gap-1 rounded-none border-l-2 border-transparent py-1 pr-3 hover:bg-po-panel/60",
1613
- children: [/* @__PURE__ */ jsx(Chevron, {
1614
- "aria-hidden": true,
1615
- className: "h-3 w-3 shrink-0 text-po-faint"
1616
- }), /* @__PURE__ */ jsx("span", {
1617
- className: "min-w-0 flex-1 truncate text-base text-po-dim",
1618
- children: row.name
1619
- })]
1620
- }, `dir:${row.path}`);
1621
- }
1622
- const { file } = row;
1623
- const mark = STATUS_MARK[file.status];
1624
- const comments = commentCounts[file.path] ?? 0;
1625
- const active = file.path === activePath;
1626
- return /* @__PURE__ */ jsxs(Button, {
1627
- variant: "ghost",
1628
- size: "card",
1762
+ className: "min-h-0 flex-1 overflow-y-auto",
1763
+ children: [shown.map((f) => {
1764
+ const slash = f.path.lastIndexOf("/");
1765
+ const count = commentCounts[f.path] ?? 0;
1766
+ return /* @__PURE__ */ jsxs("button", {
1629
1767
  "data-git-review-row": true,
1630
- "data-path": file.path,
1631
- "aria-current": active ? "true" : void 0,
1632
- "data-testid": `git-review-browser-file-${file.path}`,
1633
- title: `${file.path} (${mark.label})`,
1634
- onClick: () => onPick(file.path),
1768
+ "data-path": f.path,
1769
+ "data-testid": `git-review-browser-file-${f.path}`,
1635
1770
  onKeyDown,
1636
- style: indent(row.depth),
1637
- className: `min-w-0 flex-row items-center gap-1.5 rounded-none border-l-2 py-1 pr-3 ${active ? "border-po-blue bg-po-panel" : "border-transparent hover:bg-po-panel/60"}`,
1771
+ title: f.path,
1772
+ "aria-current": activePath === f.path ? "true" : void 0,
1773
+ onClick: () => onPick(f.path),
1774
+ className: `flex w-full min-w-0 items-start gap-2 border-l-2 px-3 py-3 text-left ${activePath === f.path ? "border-po-green bg-po-tint-green" : "border-transparent hover:bg-po-panel"}`,
1638
1775
  children: [
1639
1776
  /* @__PURE__ */ jsx("span", {
1640
- "aria-label": mark.label,
1641
- className: `w-3 shrink-0 text-center text-sm font-semibold ${mark.tone}`,
1642
- children: mark.letter
1643
- }),
1644
- /* @__PURE__ */ jsx("span", {
1645
- className: `min-w-0 flex-1 truncate text-base text-po-hi ${active ? "font-semibold" : ""} ${file.status === "deleted" ? "line-through opacity-70" : ""}`,
1646
- children: baseName(file.path)
1777
+ className: `pt-0.5 text-sm font-semibold ${status[f.status][1]}`,
1778
+ children: status[f.status][0]
1647
1779
  }),
1648
- comments === 0 ? null : /* @__PURE__ */ jsxs("span", {
1649
- className: "flex shrink-0 items-center gap-0.5 text-sm text-po-blue",
1650
- title: `${String(comments)} comments`,
1651
- children: [/* @__PURE__ */ jsx(MessageIcon, {
1652
- "aria-hidden": true,
1653
- className: "h-2.5 w-2.5"
1654
- }), comments]
1780
+ /* @__PURE__ */ jsxs("span", {
1781
+ className: "min-w-0 flex-1",
1782
+ children: [/* @__PURE__ */ jsx("span", {
1783
+ className: "block truncate text-base text-po-text",
1784
+ children: f.path.slice(slash + 1)
1785
+ }), slash < 0 ? null : /* @__PURE__ */ jsx("span", {
1786
+ className: "block truncate font-mono text-sm text-po-dim",
1787
+ children: f.path.slice(0, slash)
1788
+ })]
1655
1789
  }),
1656
- file.binary ? /* @__PURE__ */ jsx("span", {
1657
- className: "shrink-0 text-sm text-po-dim",
1658
- children: "binary"
1659
- }) : /* @__PURE__ */ jsxs("span", {
1660
- className: "shrink-0 text-2xs",
1661
- children: [
1790
+ /* @__PURE__ */ jsxs("span", {
1791
+ className: "flex shrink-0 flex-col items-end gap-1 text-sm",
1792
+ children: [/* @__PURE__ */ jsxs("span", { children: [
1662
1793
  /* @__PURE__ */ jsxs("span", {
1663
1794
  className: "text-po-green",
1664
- children: ["+", file.added]
1795
+ children: ["+", f.added]
1665
1796
  }),
1666
1797
  " ",
1667
1798
  /* @__PURE__ */ jsxs("span", {
1668
1799
  className: "text-po-red",
1669
- children: ["-", file.removed]
1800
+ children: ["-", f.removed]
1670
1801
  })
1671
- ]
1802
+ ] }), count > 0 && /* @__PURE__ */ jsxs("span", {
1803
+ className: "flex items-center gap-1 text-po-blue",
1804
+ children: [/* @__PURE__ */ jsx(MessageIcon, { className: "h-3 w-3" }), count]
1805
+ })]
1672
1806
  })
1673
1807
  ]
1674
- }, file.path);
1675
- }), ordered.length === 0 ? /* @__PURE__ */ jsx("p", {
1676
- className: "px-3 py-2 text-sm text-po-dim",
1677
- children: "no path matches"
1678
- }) : null]
1808
+ }, f.path);
1809
+ }), shown.length === 0 && /* @__PURE__ */ jsx("p", {
1810
+ className: "p-3 text-sm text-po-dim",
1811
+ children: "No paths match."
1812
+ })]
1679
1813
  })]
1680
1814
  });
1681
1815
  }
1682
1816
  //#endregion
1683
1817
  //#region src/extensions/workspaces/sessions/(frontend)/_components/GitSyncBar.tsx
1684
- /**
1685
- * The review tab's header: what is being reviewed, and the manual sync that
1686
- * goes with it.
1687
- *
1688
- * DESIGN PATTERNS:
1689
- * - Everything here is a report or a request. The hub runs the git, one
1690
- * command per session, and answers on the channel: pending while it runs,
1691
- * an error target when it fails, a paused rebase when it stops on conflicts.
1692
- * - A push the remote rejects is not an error to shrug at: the strip asks
1693
- * before forcing, and the force it sends is a lease, which git refuses if
1694
- * the remote holds commits this branch never fetched and integrated.
1695
- * - A paused rebase blocks the sync buttons. The two ways out are named on the
1696
- * banner, and abort says what it throws away.
1697
- *
1698
- * AVOID:
1699
- * - Colour classes outside the token set.
1700
- */
1701
- function GitSyncBar({ changes, mergeBase, pending, error, errorTarget, onSync, onForcePush, onAskAgent, onDismissError }) {
1702
- const busy = pending !== void 0;
1703
- const paused = changes?.rebase !== void 0;
1704
- const forceRequired = errorTarget?.action === "push" && "forceRequired" in errorTarget && errorTarget.forceRequired === true;
1705
- const syncError = error !== void 0 && !forceRequired ? error : void 0;
1706
- const upstream = changes?.upstream;
1818
+ function GitSyncBar({ secondary = false, ...props }) {
1819
+ const c = props.changes;
1820
+ const paused = c?.rebase !== void 0;
1821
+ const busy = props.pending !== void 0;
1822
+ const rejected = props.errorTarget?.action === "push" && props.errorTarget.forceRequired;
1823
+ const suggested = suggestedSyncAction(c);
1824
+ const upstream = upstreamSummary(c?.upstream);
1707
1825
  return /* @__PURE__ */ jsxs("div", {
1708
1826
  "data-testid": "git-sync-bar",
1709
- className: "flex shrink-0 flex-col gap-2 border-b border-po-border-soft px-3 py-2.5 sm:px-[26px]",
1827
+ className: "flex flex-col gap-3 border-b border-po-border-soft p-4",
1710
1828
  children: [
1711
1829
  /* @__PURE__ */ jsxs("div", {
1712
- className: "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1.5",
1830
+ className: "flex flex-wrap items-center gap-3 text-sm",
1713
1831
  children: [
1714
- /* @__PURE__ */ jsxs("span", {
1715
- className: "flex min-w-0 items-center gap-1.5",
1716
- children: [
1717
- /* @__PURE__ */ jsx(BranchIcon, {
1718
- "aria-hidden": true,
1719
- className: "h-3.5 w-3.5 shrink-0 text-po-faint"
1720
- }),
1721
- /* @__PURE__ */ jsx("span", {
1722
- "data-testid": "git-review-branch",
1723
- className: "truncate text-base font-semibold text-po-hi",
1724
- children: changes?.branch ?? "detached HEAD"
1725
- }),
1726
- /* @__PURE__ */ jsx("span", {
1727
- className: "shrink-0 text-sm text-po-dim",
1728
- children: changes?.base === void 0 ? "uncommitted changes" : `vs ${changes.base}`
1729
- }),
1730
- mergeBase === void 0 ? null : /* @__PURE__ */ jsxs("span", {
1731
- className: "shrink-0 text-sm text-po-dim",
1732
- children: ["from ", mergeBase]
1733
- })
1734
- ]
1832
+ /* @__PURE__ */ jsx(BranchIcon, { className: "h-4 w-4 text-po-dim" }),
1833
+ /* @__PURE__ */ jsx("span", {
1834
+ "data-testid": "git-review-branch",
1835
+ className: "font-mono text-base font-semibold text-po-hi",
1836
+ children: c?.branch ?? "Detached HEAD"
1837
+ }),
1838
+ /* @__PURE__ */ jsx("span", {
1839
+ className: "text-po-dim",
1840
+ children: c?.base === void 0 ? "Uncommitted changes" : /* @__PURE__ */ jsxs(Fragment, { children: ["vs ", /* @__PURE__ */ jsx("span", {
1841
+ className: "font-mono",
1842
+ children: c.base
1843
+ })] })
1735
1844
  }),
1736
- changes === void 0 ? null : /* @__PURE__ */ jsxs("span", {
1845
+ props.mergeBase && /* @__PURE__ */ jsxs("span", {
1846
+ className: "text-po-dim",
1847
+ children: ["from ", /* @__PURE__ */ jsx("span", {
1848
+ className: "font-mono",
1849
+ children: props.mergeBase
1850
+ })]
1851
+ }),
1852
+ c === void 0 ? null : /* @__PURE__ */ jsxs("span", {
1737
1853
  "data-testid": "git-review-totals",
1738
- className: "flex shrink-0 items-center gap-1.5 text-sm",
1854
+ className: "contents",
1739
1855
  children: [
1740
1856
  /* @__PURE__ */ jsxs("span", {
1741
- className: "font-semibold text-po-green",
1742
- children: ["+", changes.added]
1857
+ className: "text-po-green",
1858
+ children: ["+", c?.added]
1743
1859
  }),
1744
1860
  /* @__PURE__ */ jsxs("span", {
1745
- className: "font-semibold text-po-red",
1746
- children: ["-", changes.removed]
1861
+ className: "text-po-red",
1862
+ children: ["-", c?.removed]
1747
1863
  }),
1748
1864
  /* @__PURE__ */ jsxs("span", {
1749
1865
  className: "text-po-dim",
1750
1866
  children: [
1751
- changes.files,
1752
- changes.truncated ? "+" : "",
1867
+ c?.files,
1868
+ c?.truncated ? "+" : "",
1753
1869
  " ",
1754
- changes.files === 1 ? "file" : "files"
1870
+ c?.files === 1 ? "file" : "files"
1755
1871
  ]
1756
1872
  })
1757
1873
  ]
1758
1874
  }),
1759
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
1760
- upstream === void 0 ? /* @__PURE__ */ jsx("span", {
1761
- className: "shrink-0 text-sm text-po-dim",
1762
- children: "no upstream yet"
1763
- }) : /* @__PURE__ */ jsxs("span", {
1764
- className: "shrink-0 text-sm text-po-dim",
1765
- title: upstream.ref,
1766
- children: [
1767
- "↑",
1768
- upstream.ahead,
1769
- " ↓",
1770
- upstream.behind,
1771
- " ",
1772
- upstream.ref
1773
- ]
1875
+ /* @__PURE__ */ jsx("span", { className: "flex-1" }),
1876
+ /* @__PURE__ */ jsx("span", {
1877
+ className: "text-po-dim",
1878
+ children: c?.branch === void 0 ? "No branch checked out" : c.upstream === void 0 || !upstream.endsWith(c.upstream.ref) ? upstream : /* @__PURE__ */ jsxs(Fragment, { children: [upstream.slice(0, -c.upstream.ref.length), /* @__PURE__ */ jsx("span", {
1879
+ className: "font-mono",
1880
+ children: c.upstream.ref
1881
+ })] })
1774
1882
  }),
1775
- /* @__PURE__ */ jsxs("span", {
1776
- className: "flex shrink-0 items-center gap-1.5",
1883
+ /* @__PURE__ */ jsx("div", {
1884
+ className: "flex gap-1.5",
1777
1885
  children: [
1778
- /* @__PURE__ */ jsx(Button, {
1779
- size: "xs",
1780
- "data-testid": "git-sync-pull",
1781
- disabled: busy || paused || upstream === void 0,
1782
- title: upstream === void 0 ? "push once to set an upstream" : `fetch, then rebase onto ${upstream.ref}`,
1783
- onClick: () => onSync("pull"),
1784
- children: "pull"
1785
- }),
1786
- /* @__PURE__ */ jsx(Button, {
1787
- size: "xs",
1788
- "data-testid": "git-sync-push",
1789
- disabled: busy || paused || changes?.branch === void 0,
1790
- title: upstream === void 0 ? "push and set the upstream" : `push to ${upstream.ref}`,
1791
- onClick: () => onSync("push"),
1792
- children: "push"
1793
- }),
1794
- /* @__PURE__ */ jsx(Button, {
1795
- size: "xs",
1796
- "data-testid": "git-sync-rebase",
1797
- disabled: busy || paused || changes?.base === void 0,
1798
- title: changes?.base === void 0 ? "no base branch to rebase onto" : `fetch, then rebase onto ${changes.base}`,
1799
- onClick: () => onSync("rebase"),
1800
- children: "rebase"
1801
- })
1802
- ]
1886
+ "pull",
1887
+ "push",
1888
+ "rebase"
1889
+ ].map((action) => /* @__PURE__ */ jsx(Button, {
1890
+ "data-testid": `git-sync-${action}`,
1891
+ size: "sm",
1892
+ variant: !busy && !paused && !rejected && !props.error && c?.branch !== void 0 && suggested === action ? secondary ? "outline" : "primary" : "ghost",
1893
+ disabled: busy || paused || action !== "rebase" && c?.branch === void 0 || action === "pull" && c?.upstream === void 0 || action === "rebase" && c?.base === void 0,
1894
+ onClick: () => props.onSync(action),
1895
+ children: action === "pull" ? "Pull" : action === "push" ? "Push" : "Rebase"
1896
+ }, action))
1803
1897
  })
1804
1898
  ]
1805
1899
  }),
1806
- pending === void 0 ? null : /* @__PURE__ */ jsxs("span", {
1900
+ busy && /* @__PURE__ */ jsxs("div", {
1807
1901
  "data-testid": "git-sync-pending",
1808
- className: "flex min-w-0 items-center gap-1.5",
1902
+ className: "flex items-center gap-2 text-sm text-po-dim",
1809
1903
  children: [/* @__PURE__ */ jsx(Spinner, {
1810
- className: "h-3 w-3 shrink-0 text-po-faint",
1811
- label: pending
1812
- }), /* @__PURE__ */ jsx("span", {
1813
- className: "min-w-0 truncate text-sm text-po-dim",
1814
- children: pending
1815
- })]
1816
- }),
1817
- syncError === void 0 ? null : /* @__PURE__ */ jsxs("div", {
1818
- role: "alert",
1819
- "data-testid": "git-sync-error",
1820
- className: "flex min-w-0 items-start gap-1.5 text-sm text-po-red",
1821
- children: [
1822
- /* @__PURE__ */ jsx(AlertIcon, {
1823
- "aria-hidden": true,
1824
- className: "mt-px h-3 w-3 shrink-0"
1825
- }),
1826
- /* @__PURE__ */ jsx("span", {
1827
- className: "min-w-0 flex-1",
1828
- children: syncError
1829
- }),
1830
- /* @__PURE__ */ jsx(Button, {
1831
- variant: "ghost",
1832
- size: "xs",
1833
- className: "h-auto min-h-0 px-1 py-0 ",
1834
- onClick: onDismissError,
1835
- children: "dismiss"
1836
- })
1837
- ]
1904
+ className: "h-4 w-4 text-po-dim",
1905
+ label: props.pending
1906
+ }), props.pending]
1838
1907
  }),
1839
- forceRequired ? /* @__PURE__ */ jsxs("div", {
1908
+ rejected ? /* @__PURE__ */ jsxs("div", {
1840
1909
  "data-testid": "git-push-force-confirm",
1841
- className: "flex min-w-0 flex-wrap items-center gap-2 rounded-md border border-po-edge-yellow bg-po-tint-yellow px-2.5 py-2",
1910
+ className: "flex flex-wrap items-center gap-3 rounded-md border border-po-edge-yellow bg-po-tint-yellow p-3",
1842
1911
  children: [
1843
1912
  /* @__PURE__ */ jsx("span", {
1844
- className: "min-w-0 flex-1 text-base text-po-hi",
1845
- children: "The remote branch has commits yours replaced, usually after a rebase. Force push with lease? It is refused if the remote holds commits you never fetched and integrated."
1913
+ className: "flex-1 text-base text-po-text",
1914
+ children: "origin has commits you have not pulled yet."
1846
1915
  }),
1847
1916
  /* @__PURE__ */ jsx(Button, {
1917
+ size: "sm",
1848
1918
  variant: "danger-outline",
1849
- size: "xs",
1850
1919
  "data-testid": "git-push-force",
1851
1920
  disabled: busy,
1852
- onClick: onForcePush,
1853
- children: "force push"
1921
+ onClick: props.onForcePush,
1922
+ children: "Force push with lease"
1854
1923
  }),
1855
1924
  /* @__PURE__ */ jsx(Button, {
1925
+ size: "sm",
1856
1926
  variant: "ghost",
1857
- size: "xs",
1858
1927
  "data-testid": "git-push-force-cancel",
1859
- onClick: onDismissError,
1860
- children: "cancel"
1928
+ onClick: props.onDismissError,
1929
+ children: "Not now"
1861
1930
  })
1862
1931
  ]
1863
- }) : null,
1864
- changes?.rebase === void 0 ? null : /* @__PURE__ */ jsxs("div", {
1932
+ }) : props.error && /* @__PURE__ */ jsxs("div", {
1933
+ role: "alert",
1934
+ "data-testid": "git-sync-error",
1935
+ className: "flex items-center gap-3 rounded-md border border-po-edge-red bg-po-tint-red p-3 text-base text-po-red",
1936
+ children: [/* @__PURE__ */ jsx("span", {
1937
+ className: "flex-1",
1938
+ children: props.error
1939
+ }), /* @__PURE__ */ jsx(Button, {
1940
+ size: "sm",
1941
+ variant: "ghost",
1942
+ onClick: props.onDismissError,
1943
+ children: "Dismiss"
1944
+ })]
1945
+ }),
1946
+ paused && /* @__PURE__ */ jsxs("div", {
1865
1947
  "data-testid": "git-rebase-banner",
1866
- className: "flex min-w-0 flex-col gap-1.5 rounded-md border border-po-edge-red bg-po-tint-red px-2.5 py-2",
1948
+ className: "flex flex-col gap-2 rounded-md border border-po-edge-yellow bg-po-tint-yellow p-3",
1867
1949
  children: [
1868
- /* @__PURE__ */ jsxs("span", {
1869
- className: "flex min-w-0 items-center gap-1.5",
1870
- children: [/* @__PURE__ */ jsx(StatusBadge, {
1871
- tone: "error",
1872
- size: "xs",
1873
- children: "rebase paused"
1874
- }), /* @__PURE__ */ jsx("span", {
1875
- className: "text-base text-po-hi",
1876
- children: changes.rebase.conflicts.length === 0 ? "git stopped the rebase" : `conflicts in ${String(changes.rebase.conflicts.length)} ${changes.rebase.conflicts.length === 1 ? "file" : "files"}`
1877
- })]
1878
- }),
1879
- changes.rebase.conflicts.length === 0 ? null : /* @__PURE__ */ jsx("ul", {
1880
- className: "flex flex-col gap-0.5 pl-1",
1881
- children: changes.rebase.conflicts.map((file) => /* @__PURE__ */ jsx("li", {
1882
- className: "truncate font-mono text-sm text-po-dim",
1883
- children: file
1884
- }, file))
1885
- }),
1886
- /* @__PURE__ */ jsxs("span", {
1887
- className: "flex flex-wrap items-center gap-1.5",
1950
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(StatusBadge, {
1951
+ tone: "running",
1952
+ size: "xs",
1953
+ children: "Rebase paused"
1954
+ }), /* @__PURE__ */ jsxs("span", {
1955
+ className: "ml-2 text-base text-po-text",
1956
+ children: [
1957
+ "Conflicts in ",
1958
+ c.rebase?.conflicts.length,
1959
+ " ",
1960
+ c.rebase?.conflicts.length === 1 ? "file" : "files"
1961
+ ]
1962
+ })] }),
1963
+ c.rebase?.conflicts.map((path) => /* @__PURE__ */ jsx("p", {
1964
+ className: "font-mono text-sm text-po-dim",
1965
+ children: path
1966
+ }, path)),
1967
+ /* @__PURE__ */ jsxs("div", {
1968
+ className: "flex items-center gap-2",
1888
1969
  children: [
1889
1970
  /* @__PURE__ */ jsx(Button, {
1890
- variant: "primary",
1891
- size: "xs",
1971
+ variant: secondary ? "outline" : "primary",
1972
+ size: "sm",
1892
1973
  "data-testid": "git-rebase-ask-agent",
1893
1974
  disabled: busy,
1894
- onClick: onAskAgent,
1895
- children: "ask agent to resolve"
1975
+ onClick: props.onAskAgent,
1976
+ children: "Ask Po to resolve"
1896
1977
  }),
1897
1978
  /* @__PURE__ */ jsx(Button, {
1898
1979
  variant: "danger-outline",
1899
- size: "xs",
1980
+ size: "sm",
1900
1981
  "data-testid": "git-rebase-abort",
1901
1982
  disabled: busy,
1902
- onClick: () => onSync("abort-rebase"),
1903
- children: "abort rebase"
1983
+ onClick: () => props.onSync("abort-rebase"),
1984
+ children: "Abort rebase"
1904
1985
  }),
1905
1986
  /* @__PURE__ */ jsx("span", {
1906
1987
  className: "text-sm text-po-dim",
1907
- children: "abort discards any conflict resolution in progress"
1988
+ children: "Abort discards conflict resolution in progress."
1908
1989
  })
1909
1990
  ]
1910
1991
  })
@@ -1915,29 +1996,7 @@ function GitSyncBar({ changes, mergeBase, pending, error, errorTarget, onSync, o
1915
1996
  }
1916
1997
  //#endregion
1917
1998
  //#region src/extensions/workspaces/sessions/(frontend)/_components/GitReviewView.tsx
1918
- /**
1919
- * The review tab, drawn from what it is handed: the sync header, the changed
1920
- * files on the left, every diff stacked on the right, and one footer that
1921
- * sends every comment as a single message.
1922
- *
1923
- * DESIGN PATTERNS:
1924
- * - Like a pull request's file view. Picking a file in the list scrolls to it;
1925
- * each file's header stays pinned while its diff scrolls past.
1926
- * - Only nearby file bodies mount and load; distant files retain their headers
1927
- * and measured-height placeholders.
1928
- * - A comment is drawn under the line it is about, on the side it is about.
1929
- * The open draft sits in the same place, so writing happens in context.
1930
- * - Only UI state lives here (the open draft, which files are collapsed, which
1931
- * is active). Data and comments come in as props; the panel owns fetching.
1932
- *
1933
- * AVOID:
1934
- * - Clearing comments here. The owner clears them once the host took the send.
1935
- */
1936
- const PICK_ALIGNMENT_MS = 300;
1937
- const ESTIMATED_ROW_HEIGHT = 21;
1938
- const MAX_DIFF_ROWS = 5e3;
1939
1999
  const NO_FILES = [];
1940
- const NO_PATHS = /* @__PURE__ */ new Set();
1941
2000
  /** Stable, URL-safe element id for one file's section. */
1942
2001
  function sectionId(index) {
1943
2002
  return `git-review-file-${String(index)}`;
@@ -1945,122 +2004,30 @@ function sectionId(index) {
1945
2004
  function sideOf(row) {
1946
2005
  return row.marker === "-" ? "old" : "new";
1947
2006
  }
1948
- function GitReviewView({ sync, summary, files, comments, onLoadFile, onAddComment, onRemoveComment, onSendReview, onDiscard, onDraftChange, sendError, initialDraft }) {
2007
+ function GitReviewView({ sync, summary, files, comments, onLoadFile, onAddComment, onRemoveComment, onSendReview, onDiscard, onDraftChange, sendError, initialDraft, initialPath }) {
1949
2008
  const entries = summary.state === "ready" ? summary.summary.files : NO_FILES;
1950
- const [openOverride, setOpenOverride] = useState({});
1951
- const [picked, setPicked] = useState();
1952
- const [navigation, setNavigation] = useState();
1953
- const [proximity, setProximity] = useState({
1954
- entries: NO_FILES,
1955
- paths: NO_PATHS
1956
- });
1957
- const near = proximity.entries === entries ? proximity.paths : NO_PATHS;
1958
- const paneRef = useRef(null);
1959
- const [heights, setHeights] = useState(/* @__PURE__ */ new Map());
1960
- const pickedAt = useRef(0);
2009
+ const [picked, setPicked] = useState(initialPath ?? initialDraft?.path);
1961
2010
  const activePath = entries.some((entry) => entry.path === picked) ? picked : entries[0]?.path;
2011
+ const index = entries.findIndex((entry) => entry.path === activePath);
2012
+ const file = entries[index];
1962
2013
  const [draft, setDraft] = useState(initialDraft);
1963
2014
  useEffect(() => {
1964
- const pane = paneRef.current;
1965
- if (pane === null || typeof IntersectionObserver === "undefined") return;
1966
- setHeights(/* @__PURE__ */ new Map());
1967
- let observer;
1968
- const observe = () => {
1969
- observer?.disconnect();
1970
- observer = new IntersectionObserver((changes) => {
1971
- for (const change of changes) {
1972
- if (change.isIntersecting) continue;
1973
- const section = change.target;
1974
- const path = section.dataset.reviewPath;
1975
- const body = section.querySelector("[data-review-body=\"measured\"]");
1976
- if (path !== void 0 && body !== null) {
1977
- const height = body.offsetHeight;
1978
- setHeights((measured) => new Map(measured).set(path, height));
1979
- }
1980
- }
1981
- setProximity((current) => {
1982
- const next = new Set(current.entries === entries ? current.paths : NO_PATHS);
1983
- for (const change of changes) {
1984
- const path = change.target.dataset.reviewPath;
1985
- if (path === void 0) continue;
1986
- if (change.isIntersecting) next.add(path);
1987
- else next.delete(path);
1988
- }
1989
- return {
1990
- entries,
1991
- paths: next
1992
- };
1993
- });
1994
- }, {
1995
- root: pane,
1996
- rootMargin: `${String(pane.clientHeight)}px 0px`
1997
- });
1998
- for (const section of pane.querySelectorAll("[data-review-path]")) observer.observe(section);
1999
- };
2000
- observe();
2001
- const resize = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(observe);
2002
- resize?.observe(pane);
2003
- let frame = 0;
2004
- const scroll = () => {
2005
- cancelAnimationFrame(frame);
2006
- frame = requestAnimationFrame(() => {
2007
- if (Date.now() - pickedAt.current < PICK_ALIGNMENT_MS) return;
2008
- const rect = pane.getBoundingClientRect();
2009
- const section = document.elementFromPoint(rect.left + 8, rect.top + 1)?.closest("[data-review-path]");
2010
- if (section !== null && section !== void 0 && pane.contains(section)) setPicked(section.dataset.reviewPath);
2011
- });
2012
- };
2013
- pane.addEventListener("scroll", scroll, { passive: true });
2014
- return () => {
2015
- observer.disconnect();
2016
- resize?.disconnect();
2017
- pane.removeEventListener("scroll", scroll);
2018
- cancelAnimationFrame(frame);
2019
- };
2020
- }, [entries]);
2021
- useLayoutEffect(() => {
2022
- if (navigation === void 0) return;
2023
- [...paneRef.current?.querySelectorAll("[data-review-path]") ?? []].find((element) => element.dataset.reviewPath === navigation)?.scrollIntoView({ block: "start" });
2024
- }, [navigation, openOverride]);
2025
- useEffect(() => {
2026
- if (navigation === void 0) return;
2027
- const timeout = setTimeout(() => setNavigation(void 0), PICK_ALIGNMENT_MS);
2028
- return () => clearTimeout(timeout);
2029
- }, [navigation]);
2030
- useEffect(() => {
2031
- for (const file of entries) if ((openOverride[file.path] ?? true) && (near.has(file.path) || navigation === file.path || draft?.path === file.path || comments.some((comment) => comment.path === file.path)) && files[file.path] === void 0) onLoadFile(file.path);
2015
+ if (activePath !== void 0 && files[activePath] === void 0) onLoadFile(activePath);
2032
2016
  }, [
2033
- entries,
2017
+ activePath,
2034
2018
  files,
2035
- near,
2036
- navigation,
2037
- draft,
2038
- comments,
2039
- openOverride,
2040
2019
  onLoadFile
2041
2020
  ]);
2042
2021
  const commentCounts = {};
2043
2022
  for (const comment of comments) commentCounts[comment.path] = (commentCounts[comment.path] ?? 0) + 1;
2044
- const isOpen = (path) => openOverride[path] ?? true;
2045
- const open = (path) => {
2046
- setOpenOverride((current) => current[path] === true ? current : {
2047
- ...current,
2048
- [path]: true
2049
- });
2050
- };
2051
2023
  const pick = (path) => {
2052
2024
  if (!entries.some((entry) => entry.path === path)) return;
2053
- pickedAt.current = Date.now();
2025
+ if (path !== activePath && draft !== void 0) {
2026
+ if (!window.confirm("Discard the unsent comment and open another file?")) return;
2027
+ setDraft(void 0);
2028
+ onDraftChange?.(false);
2029
+ }
2054
2030
  setPicked(path);
2055
- setNavigation(path);
2056
- open(path);
2057
- };
2058
- const toggle = (path) => {
2059
- if (isOpen(path)) setOpenOverride((current) => ({
2060
- ...current,
2061
- [path]: false
2062
- }));
2063
- else open(path);
2064
2031
  };
2065
2032
  const submitDraft = (body) => {
2066
2033
  if (draft === void 0) return;
@@ -2082,87 +2049,116 @@ function GitReviewView({ sync, summary, files, comments, onLoadFile, onAddCommen
2082
2049
  children: [
2083
2050
  /* @__PURE__ */ jsx(GitSyncBar, {
2084
2051
  ...sync,
2052
+ secondary: comments.length > 0,
2085
2053
  ...summary.state === "ready" && summary.summary.mergeBase !== void 0 ? { mergeBase: summary.summary.mergeBase } : {}
2086
2054
  }),
2087
2055
  summary.state === "loading" ? /* @__PURE__ */ jsxs("div", {
2088
2056
  "data-testid": "git-review-loading",
2089
- className: "flex flex-1 items-start gap-2 px-4 py-6",
2057
+ className: "flex min-h-48 flex-1 items-center justify-center gap-2 text-sm text-po-dim",
2090
2058
  children: [/* @__PURE__ */ jsx(Spinner, {
2091
- className: "h-3 w-3 text-po-faint",
2092
- label: "reading changes"
2059
+ className: "h-4 w-4 text-po-dim",
2060
+ label: "Reading changes"
2093
2061
  }), /* @__PURE__ */ jsx("span", {
2094
2062
  className: "text-sm text-po-dim",
2095
- children: "reading changes…"
2063
+ children: "Reading changes..."
2096
2064
  })]
2097
- }) : summary.state === "error" ? /* @__PURE__ */ jsx("p", {
2098
- role: "alert",
2065
+ }) : summary.state === "error" ? /* @__PURE__ */ jsx(EmptyState, {
2099
2066
  "data-testid": "git-review-error",
2100
- className: "flex-1 px-4 py-6 text-sm text-po-red",
2101
- children: summary.error
2067
+ className: "min-h-48 text-po-red",
2068
+ title: /* @__PURE__ */ jsx("span", {
2069
+ className: "text-po-red",
2070
+ children: "Changes could not be read"
2071
+ }),
2072
+ description: summary.error
2102
2073
  }) : !summary.summary.repository ? /* @__PURE__ */ jsx(EmptyState, {
2103
2074
  "data-testid": "git-review-no-repository",
2104
- className: "px-4 py-8",
2105
- title: "not a git checkout",
2106
- description: "this session's folder is not inside a git repository, so there is nothing to review."
2075
+ className: "min-h-48",
2076
+ title: "Not a Git checkout",
2077
+ description: "This session's folder is not inside a Git repository."
2107
2078
  }) : entries.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
2108
2079
  "data-testid": "git-review-empty",
2109
- className: "px-4 py-8",
2110
- title: "no changes",
2111
- description: summary.summary.base === void 0 ? "nothing uncommitted in this checkout." : `nothing differs from ${summary.summary.base}, committed or not.`
2080
+ className: "min-h-48",
2081
+ title: "No changes yet",
2082
+ description: summary.summary.base === void 0 ? "Nothing uncommitted in this checkout." : `Nothing differs from ${summary.summary.base}, committed or not.`
2112
2083
  }) : /* @__PURE__ */ jsxs("div", {
2084
+ "data-testid": "git-review-content",
2113
2085
  className: "flex min-h-0 flex-1",
2114
2086
  children: [/* @__PURE__ */ jsx(GitReviewBrowser, {
2115
2087
  files: entries,
2116
2088
  commentCounts,
2117
2089
  onPick: pick,
2118
2090
  ...activePath === void 0 ? {} : { activePath }
2119
- }), /* @__PURE__ */ jsxs("div", {
2120
- ref: paneRef,
2091
+ }), /* @__PURE__ */ jsxs("main", {
2121
2092
  "data-testid": "git-review-diffs",
2122
2093
  className: "min-h-0 min-w-0 flex-1 overflow-y-auto",
2123
- children: [summary.summary.truncated ? /* @__PURE__ */ jsx("p", {
2124
- className: "px-4 pt-2 text-sm text-po-dim",
2125
- children: "more files changed than the review lists; the rest are left out."
2126
- }) : null, entries.map((file, index) => /* @__PURE__ */ jsx(FileSection, {
2127
- id: sectionId(index),
2128
- entry: file,
2129
- state: files[file.path],
2130
- collapsed: !isOpen(file.path),
2131
- mounted: near.has(file.path) || navigation === file.path || draft?.path === file.path || comments.some((comment) => comment.path === file.path),
2132
- placeholderHeight: heights.get(file.path) ?? (file.binary ? 40 : Math.min(MAX_DIFF_ROWS, file.added + file.removed + 6) * ESTIMATED_ROW_HEIGHT),
2133
- comments: comments.filter((comment) => comment.path === file.path),
2134
- draft: draft?.path === file.path ? draft.selection : void 0,
2135
- onToggle: () => toggle(file.path),
2136
- onSelect: (selection) => {
2137
- setPicked(file.path);
2138
- setDraft({
2139
- path: file.path,
2140
- selection
2141
- });
2142
- onDraftChange?.(true);
2143
- },
2144
- onSubmitDraft: submitDraft,
2145
- onCancelDraft: () => {
2146
- setDraft(void 0);
2147
- onDraftChange?.(false);
2148
- },
2149
- onRemoveComment,
2150
- onRetry: () => onLoadFile(file.path)
2151
- }, file.path))]
2094
+ children: [
2095
+ summary.summary.truncated ? /* @__PURE__ */ jsx("p", {
2096
+ className: "px-4 pt-2 text-sm text-po-dim",
2097
+ children: "More files changed than the review lists; the rest are left out."
2098
+ }) : null,
2099
+ /* @__PURE__ */ jsxs("div", {
2100
+ className: "flex items-center justify-between border-b border-po-border-soft px-4 py-2",
2101
+ children: [
2102
+ /* @__PURE__ */ jsx(Button, {
2103
+ variant: "ghost",
2104
+ size: "sm",
2105
+ disabled: index <= 0,
2106
+ onClick: () => pick(entries[index - 1].path),
2107
+ children: "Previous file"
2108
+ }),
2109
+ /* @__PURE__ */ jsxs("span", {
2110
+ className: "text-sm text-po-dim",
2111
+ children: [
2112
+ index + 1,
2113
+ " of ",
2114
+ entries.length
2115
+ ]
2116
+ }),
2117
+ /* @__PURE__ */ jsx(Button, {
2118
+ variant: "ghost",
2119
+ size: "sm",
2120
+ disabled: index >= entries.length - 1,
2121
+ onClick: () => pick(entries[index + 1].path),
2122
+ children: "Next file"
2123
+ })
2124
+ ]
2125
+ }),
2126
+ file === void 0 ? null : /* @__PURE__ */ jsx(FileSection, {
2127
+ id: sectionId(index),
2128
+ entry: file,
2129
+ state: files[file.path],
2130
+ comments: comments.filter((comment) => comment.path === file.path),
2131
+ draft: draft?.path === file.path ? draft.selection : void 0,
2132
+ onSelect: (selection) => {
2133
+ setDraft({
2134
+ path: file.path,
2135
+ selection
2136
+ });
2137
+ onDraftChange?.(true);
2138
+ },
2139
+ onSubmitDraft: submitDraft,
2140
+ onCancelDraft: () => {
2141
+ setDraft(void 0);
2142
+ onDraftChange?.(false);
2143
+ },
2144
+ onRemoveComment,
2145
+ onRetry: () => onLoadFile(file.path)
2146
+ }, file.path)
2147
+ ]
2152
2148
  })]
2153
2149
  }),
2154
2150
  /* @__PURE__ */ jsxs("div", {
2155
2151
  "data-testid": "git-review-footer",
2156
- className: "flex shrink-0 flex-wrap items-center gap-2 border-t border-po-border-soft bg-po-panel px-3 py-2 sm:px-[26px]",
2152
+ className: "flex shrink-0 flex-wrap items-center gap-3 border-t border-po-border-soft bg-po-panel px-4 py-3",
2157
2153
  children: [
2158
2154
  /* @__PURE__ */ jsx(MessageIcon, {
2159
2155
  "aria-hidden": true,
2160
- className: "h-3 w-3 text-po-faint"
2156
+ className: "h-4 w-4 text-po-blue"
2161
2157
  }),
2162
2158
  /* @__PURE__ */ jsx("span", {
2163
2159
  "data-testid": "git-review-comment-count",
2164
2160
  className: "text-sm text-po-dim",
2165
- children: comments.length === 0 ? "no comments yet: select lines or click a line number" : `${String(comments.length)} ${comments.length === 1 ? "comment" : "comments"}`
2161
+ children: comments.length === 0 ? "No comments yet: select lines or click a line number" : `${String(comments.length)} ${comments.length === 1 ? "comment" : "comments"} queued`
2166
2162
  }),
2167
2163
  /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1" }),
2168
2164
  sendError === void 0 ? null : /* @__PURE__ */ jsx("span", {
@@ -2173,11 +2169,11 @@ function GitReviewView({ sync, summary, files, comments, onLoadFile, onAddCommen
2173
2169
  }),
2174
2170
  /* @__PURE__ */ jsx(Button, {
2175
2171
  variant: "ghost",
2176
- size: "xs",
2172
+ size: "sm",
2177
2173
  "data-testid": "git-review-discard",
2178
2174
  disabled: comments.length === 0,
2179
2175
  onClick: onDiscard,
2180
- children: "discard"
2176
+ children: "Discard"
2181
2177
  }),
2182
2178
  /* @__PURE__ */ jsx(Button, {
2183
2179
  variant: "primary",
@@ -2185,7 +2181,7 @@ function GitReviewView({ sync, summary, files, comments, onLoadFile, onAddCommen
2185
2181
  "data-testid": "git-review-send",
2186
2182
  disabled: comments.length === 0,
2187
2183
  onClick: onSendReview,
2188
- children: "send review"
2184
+ children: "Send review"
2189
2185
  })
2190
2186
  ]
2191
2187
  })
@@ -2193,34 +2189,34 @@ function GitReviewView({ sync, summary, files, comments, onLoadFile, onAddCommen
2193
2189
  });
2194
2190
  }
2195
2191
  const STATUS_WORD = {
2196
- added: "added",
2197
- modified: "modified",
2198
- deleted: "deleted",
2199
- untracked: "new, untracked",
2200
- conflicted: "conflicted"
2192
+ added: "Added",
2193
+ modified: "Modified",
2194
+ deleted: "Deleted",
2195
+ untracked: "Untracked",
2196
+ conflicted: "Conflicted"
2201
2197
  };
2202
2198
  /** One file: a pinned header, then its diff with comments drawn under the lines they are about. */
2203
- function FileSection({ id, entry, state, collapsed, mounted, placeholderHeight, comments, draft, onToggle, onSelect, onSubmitDraft, onCancelDraft, onRemoveComment, onRetry }) {
2199
+ function FileSection({ id, entry, state, comments, draft, onSelect, onSubmitDraft, onCancelDraft, onRemoveComment, onRetry }) {
2204
2200
  const renderAfterRow = (row) => {
2205
2201
  const side = sideOf(row);
2206
2202
  const here = comments.filter((comment) => (comment.side ?? "new") === side && (comment.endLine ?? comment.startLine) === row.line);
2207
2203
  const drafting = draft !== void 0 && draft.side === side && draft.endLine === row.line;
2208
2204
  if (here.length === 0 && !drafting) return null;
2209
2205
  return /* @__PURE__ */ jsxs("div", {
2210
- className: "flex flex-col gap-1.5 border-y border-po-border-soft bg-po-bg px-2 py-1.5 font-sans whitespace-normal",
2206
+ className: "font-sans whitespace-normal",
2211
2207
  children: [here.map((comment) => /* @__PURE__ */ jsxs("div", {
2212
2208
  "data-testid": `git-review-comment-${comment.id}`,
2213
- className: "flex min-w-0 items-start gap-2 rounded-md border border-po-border-soft bg-po-panel px-2 py-1.5",
2209
+ className: "m-3 flex items-start gap-3 rounded-md border border-po-border-soft bg-po-panel p-3 font-sans whitespace-normal",
2214
2210
  children: [
2215
2211
  /* @__PURE__ */ jsx(MessageIcon, {
2216
2212
  "aria-hidden": true,
2217
- className: "mt-0.5 h-3 w-3 shrink-0 text-po-blue"
2213
+ className: "mt-1 h-4 w-4 shrink-0 text-po-blue"
2218
2214
  }),
2219
2215
  /* @__PURE__ */ jsxs("span", {
2220
- className: "flex min-w-0 flex-1 flex-col gap-0.5",
2221
- children: [/* @__PURE__ */ jsx("span", {
2222
- className: "font-mono text-sm text-po-dim",
2223
- children: reviewCommentAnchor(comment)
2216
+ className: "flex min-w-0 flex-1 flex-col",
2217
+ children: [/* @__PURE__ */ jsxs("span", {
2218
+ className: "mb-1 text-sm text-po-dim",
2219
+ children: ["You · line ", comment.endLine ?? comment.startLine]
2224
2220
  }), /* @__PURE__ */ jsx("span", {
2225
2221
  className: "text-base whitespace-pre-wrap text-po-text",
2226
2222
  children: comment.body
@@ -2228,15 +2224,15 @@ function FileSection({ id, entry, state, collapsed, mounted, placeholderHeight,
2228
2224
  }),
2229
2225
  /* @__PURE__ */ jsx(Button, {
2230
2226
  variant: "ghost",
2231
- size: "icon",
2232
- "aria-label": "remove comment",
2233
- title: "remove comment",
2227
+ size: "sm",
2228
+ "aria-label": "Remove comment",
2229
+ title: "Remove comment",
2234
2230
  onClick: () => onRemoveComment(comment.id),
2235
- children: /* @__PURE__ */ jsx(CloseIcon, { className: "h-3 w-3" })
2231
+ children: "Remove"
2236
2232
  })
2237
2233
  ]
2238
2234
  }, comment.id)), drafting ? /* @__PURE__ */ jsx("div", {
2239
- className: "rounded-md border border-po-border bg-po-panel",
2235
+ className: "m-3 rounded-md border border-po-border-soft bg-po-panel font-sans whitespace-normal [&_pre]:text-sm [&_p]:first-letter:uppercase",
2240
2236
  children: /* @__PURE__ */ jsx(ReviewCommentDraft, {
2241
2237
  snippet: draft.snippet,
2242
2238
  startLine: draft.startLine,
@@ -2253,90 +2249,73 @@ function FileSection({ id, entry, state, collapsed, mounted, placeholderHeight,
2253
2249
  id,
2254
2250
  "data-review-path": entry.path,
2255
2251
  "data-testid": `git-review-file-${entry.path}`,
2256
- className: "border-b border-po-border-soft",
2257
- children: [/* @__PURE__ */ jsxs(Button, {
2258
- variant: "ghost",
2259
- size: "card",
2260
- "aria-expanded": !collapsed,
2261
- onClick: onToggle,
2262
- className: "sticky top-0 z-10 flex-row items-center gap-1.5 rounded-none border-b border-po-border-soft bg-po-rail px-3 py-1.5 hover:bg-po-panel sm:px-4",
2252
+ children: [/* @__PURE__ */ jsxs("div", {
2253
+ className: "flex flex-wrap items-center gap-3 border-b border-po-border-soft bg-po-panel px-4 py-3",
2263
2254
  children: [
2264
- collapsed ? /* @__PURE__ */ jsx(ChevronRightIcon, {
2265
- "aria-hidden": true,
2266
- className: "h-3 w-3 shrink-0 text-po-faint"
2267
- }) : /* @__PURE__ */ jsx(ChevronDownIcon, {
2268
- "aria-hidden": true,
2269
- className: "h-3 w-3 shrink-0 text-po-faint"
2270
- }),
2271
2255
  /* @__PURE__ */ jsx("span", {
2272
- className: "min-w-0 flex-1 truncate font-mono text-base font-semibold text-po-hi",
2256
+ className: "min-w-0 flex-1 truncate font-mono text-base text-po-hi",
2273
2257
  children: entry.path
2274
2258
  }),
2275
2259
  /* @__PURE__ */ jsx("span", {
2276
- className: "shrink-0 text-sm font-semibold text-po-dim",
2260
+ className: "shrink-0 text-sm text-po-dim",
2277
2261
  children: STATUS_WORD[entry.status]
2278
2262
  }),
2263
+ entry.binary ? null : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
2264
+ className: "text-sm text-po-green",
2265
+ children: ["+", entry.added]
2266
+ }), /* @__PURE__ */ jsxs("span", {
2267
+ className: "text-sm text-po-red",
2268
+ children: ["-", entry.removed]
2269
+ })] }),
2279
2270
  comments.length === 0 ? null : /* @__PURE__ */ jsxs("span", {
2280
- className: "flex shrink-0 items-center gap-0.5 text-sm text-po-blue",
2281
- children: [/* @__PURE__ */ jsx(MessageIcon, {
2282
- "aria-hidden": true,
2283
- className: "h-2.5 w-2.5"
2284
- }), comments.length]
2285
- }),
2286
- entry.binary ? null : /* @__PURE__ */ jsxs("span", {
2287
- className: "shrink-0 text-2xs",
2271
+ className: "flex shrink-0 items-center gap-1 text-sm text-po-blue",
2288
2272
  children: [
2289
- /* @__PURE__ */ jsxs("span", {
2290
- className: "text-po-green",
2291
- children: ["+", entry.added]
2273
+ /* @__PURE__ */ jsx(MessageIcon, {
2274
+ "aria-hidden": true,
2275
+ className: "h-3 w-3"
2292
2276
  }),
2277
+ comments.length,
2293
2278
  " ",
2294
- /* @__PURE__ */ jsxs("span", {
2295
- className: "text-po-red",
2296
- children: ["-", entry.removed]
2297
- })
2279
+ comments.length === 1 ? "comment" : "comments"
2298
2280
  ]
2299
2281
  })
2300
2282
  ]
2301
- }), collapsed ? null : !mounted ? /* @__PURE__ */ jsx("div", {
2302
- "aria-hidden": true,
2303
- style: { height: placeholderHeight }
2304
- }) : /* @__PURE__ */ jsx("div", {
2283
+ }), /* @__PURE__ */ jsx("div", {
2305
2284
  "data-review-body": state === void 0 || state.state === "loading" ? "loading" : "measured",
2306
- style: state === void 0 || state.state === "loading" ? { minHeight: placeholderHeight } : void 0,
2307
2285
  children: state === void 0 || state.state === "loading" ? /* @__PURE__ */ jsxs("div", {
2308
2286
  className: "flex items-center gap-2 px-4 py-3",
2309
2287
  children: [/* @__PURE__ */ jsx(Spinner, {
2310
2288
  className: "h-3 w-3 text-po-faint",
2311
- label: `loading ${entry.path}`
2289
+ label: `Loading ${entry.path}`
2312
2290
  }), /* @__PURE__ */ jsx("span", {
2313
2291
  className: "text-sm text-po-dim",
2314
- children: "loading diff…"
2292
+ children: "Loading diff..."
2315
2293
  })]
2316
2294
  }) : state.state === "error" ? /* @__PURE__ */ jsxs("div", {
2317
- className: "flex items-center gap-2 px-4 py-3",
2295
+ className: "flex min-h-48 flex-col items-center justify-center gap-3",
2318
2296
  children: [/* @__PURE__ */ jsx("span", {
2319
2297
  role: "alert",
2320
- className: "text-sm text-po-red",
2298
+ className: "text-base text-po-red",
2321
2299
  children: state.error
2322
2300
  }), /* @__PURE__ */ jsx(Button, {
2323
- variant: "ghost",
2324
- size: "xs",
2301
+ variant: "outline",
2302
+ size: "sm",
2325
2303
  onClick: onRetry,
2326
- children: "retry"
2304
+ children: "Retry"
2327
2305
  })]
2328
- }) : state.diff.binary ? /* @__PURE__ */ jsx("p", {
2329
- className: "px-4 py-3 text-sm text-po-dim",
2330
- children: "binary file, not drawn"
2331
- }) : state.diff.tooLarge ? /* @__PURE__ */ jsx("p", {
2332
- className: "px-4 py-3 text-sm text-po-dim",
2333
- children: "this diff is too large to draw here"
2306
+ }) : state.diff.binary ? /* @__PURE__ */ jsx(EmptyState, {
2307
+ className: "min-h-48",
2308
+ title: "Binary file",
2309
+ description: `${entry.path} cannot be shown as a text diff.`
2310
+ }) : state.diff.tooLarge ? /* @__PURE__ */ jsx(EmptyState, {
2311
+ className: "min-h-48",
2312
+ title: "This diff is too large to show",
2313
+ description: "Other changed files are still available to review."
2334
2314
  }) : /* @__PURE__ */ jsx(DiffView, {
2335
2315
  hunks: state.diff.hunks,
2336
2316
  testId: `git-review-diff-${id}`,
2337
2317
  onSelect,
2338
- renderAfterRow,
2339
- className: "py-1"
2318
+ renderAfterRow
2340
2319
  })
2341
2320
  })]
2342
2321
  });
@@ -2474,7 +2453,10 @@ function GitReviewPanel({ sessionId, sendSessionFrame }) {
2474
2453
  ...activity.pending === void 0 ? {} : { pending: activity.pending },
2475
2454
  ...error === void 0 ? {} : { error },
2476
2455
  ...activity.errorTarget === void 0 || error === void 0 ? {} : { errorTarget: activity.errorTarget },
2477
- onSync: (action) => requestGitSync(sessionId, action),
2456
+ onSync: (action) => {
2457
+ if (action === "abort-rebase" && !window.confirm("Abort rebase? This discards conflict resolution in progress.")) return;
2458
+ requestGitSync(sessionId, action);
2459
+ },
2478
2460
  onForcePush: () => requestGitSync(sessionId, "push", true),
2479
2461
  onAskAgent: () => send(conflictPrompt(branch, changes?.rebase?.conflicts ?? [])),
2480
2462
  onDismissError: () => setDismissedError(activity.error)
@@ -2496,8 +2478,10 @@ function GitReviewPanel({ sessionId, sendSessionFrame }) {
2496
2478
  * The '# git' group's branch row: this session's code change against its base.
2497
2479
  *
2498
2480
  * DESIGN PATTERNS:
2499
- * - One lane, branch and `+A -R`. Files, base, upstream and a paused rebase live
2500
- * in the review tab's header, which this lane opens.
2481
+ * - One lane, branch and `+A -R`, straight on the group body like every other
2482
+ * activity group: the dock already draws the card, so no inner card or heading.
2483
+ * Files, base, upstream and a paused rebase live in the review tab's header,
2484
+ * which this lane opens.
2501
2485
  * - Stays clickable at zero, because the review tab is also where pull, push
2502
2486
  * and rebase live.
2503
2487
  * - Reads the focused session's checkout, whether it is a worktree or not.
@@ -2509,128 +2493,112 @@ function DiffActivitySection({ sessionId, openTransientTab }) {
2509
2493
  const session = useStore(gitChanges.store, (state) => gitChanges.select(state, sessionId));
2510
2494
  const changes = session.changes;
2511
2495
  if (sessionId === null || changes === void 0) return null;
2512
- const zero = changes.added === 0 && changes.removed === 0;
2513
- const against = changes.base === void 0 ? "uncommitted" : `vs ${changes.base}`;
2496
+ return /* @__PURE__ */ jsx(DiffActivityView, {
2497
+ changes,
2498
+ pending: session.pending,
2499
+ onReview: () => openTransientTab(reviewTab())
2500
+ });
2501
+ }
2502
+ function DiffActivityView({ changes, pending, onReview, branch }) {
2503
+ const zero = changes?.added === 0 && changes?.removed === 0;
2504
+ const against = changes?.base === void 0 ? "uncommitted" : `vs ${changes.base}`;
2514
2505
  return /* @__PURE__ */ jsxs("div", {
2515
- className: "flex min-w-0 items-center gap-2",
2506
+ className: "flex min-w-0 flex-col gap-1",
2516
2507
  children: [/* @__PURE__ */ jsxs(Button, {
2517
2508
  variant: "ghost",
2518
- size: "xs",
2509
+ size: "card",
2519
2510
  "data-testid": "activity-diff-changes",
2520
- "aria-label": `review ${changes.branch ?? "detached HEAD"} changes: ${String(changes.added)} added, ${String(changes.removed)} removed ${against}`,
2521
- title: `${String(changes.files)} ${changes.files === 1 ? "file" : "files"} ${against}`,
2522
- className: `h-auto min-w-0 flex-1 shrink gap-2 px-0 py-0.5 hover:bg-transparent ${zero ? "text-po-faint" : ""}`,
2523
- onClick: () => openTransientTab(reviewTab()),
2524
- children: [
2525
- /* @__PURE__ */ jsx("span", {
2526
- className: "min-w-0 flex-1 truncate text-left font-mono font-semibold text-po-hi",
2527
- children: changes.branch ?? "detached HEAD"
2528
- }),
2529
- /* @__PURE__ */ jsxs("span", {
2530
- className: zero ? "" : "text-po-green",
2511
+ className: "min-w-0 flex-row items-center gap-2 rounded-md px-1 py-1 font-normal hover:bg-po-panel",
2512
+ "aria-label": changes ? `review ${changes.branch ?? "detached HEAD"} changes: ${changes.added} added, ${changes.removed} removed ${against}` : "Changes not reported yet",
2513
+ title: changes ? `${changes.files} ${changes.files === 1 ? "file" : "files"} ${against}` : void 0,
2514
+ onClick: onReview,
2515
+ children: [/* @__PURE__ */ jsx("span", {
2516
+ className: "min-w-0 flex-1 truncate text-left font-mono text-sm text-po-hi",
2517
+ children: changes?.branch ?? branch ?? "detached HEAD"
2518
+ }), !changes || zero ? /* @__PURE__ */ jsx("span", {
2519
+ className: "shrink-0 text-sm text-po-dim",
2520
+ children: zero ? "No changes yet" : "Not reported yet"
2521
+ }) : /* @__PURE__ */ jsxs("span", {
2522
+ className: "flex shrink-0 gap-2 text-sm",
2523
+ children: [/* @__PURE__ */ jsxs("span", {
2524
+ className: "text-po-green",
2531
2525
  children: ["+", changes.added]
2532
- }),
2533
- /* @__PURE__ */ jsxs("span", {
2534
- className: zero ? "" : "text-po-red",
2526
+ }), /* @__PURE__ */ jsxs("span", {
2527
+ className: "text-po-red",
2535
2528
  children: ["-", changes.removed]
2536
- })
2537
- ]
2538
- }), session.pending === void 0 ? null : /* @__PURE__ */ jsxs("span", {
2539
- className: "flex min-w-0 items-center gap-1.5",
2529
+ })]
2530
+ })]
2531
+ }), pending && /* @__PURE__ */ jsxs("p", {
2532
+ className: "flex items-center gap-2 px-1 text-sm text-po-dim",
2540
2533
  children: [/* @__PURE__ */ jsx(Spinner, {
2541
- className: "h-3 w-3 shrink-0 text-po-faint",
2542
- label: session.pending
2534
+ className: "size-3",
2535
+ label: pending
2543
2536
  }), /* @__PURE__ */ jsx("span", {
2544
- className: "min-w-0 truncate text-sm text-po-dim",
2545
- children: session.pending
2537
+ className: "inline-block first-letter:uppercase",
2538
+ children: pending
2546
2539
  })]
2547
2540
  })]
2548
2541
  });
2549
2542
  }
2550
2543
  //#endregion
2551
2544
  //#region src/extensions/workspaces/sessions/(frontend)/fill/_components/WorktreesActivitySection.tsx
2552
- /**
2553
- * The '# git' group's body in the activity dock.
2554
- *
2555
- * DESIGN PATTERNS:
2556
- * - The focused checkout's branch and diff stay visible, even with no child worktrees.
2557
- * - One line per worktree, the same rule the runners rail follows: enough to
2558
- * notice it, not enough to work with it. The group's own name opens the tab
2559
- * that has room.
2560
- * - Idle still shows the launcher. A repository with no worktrees is the state
2561
- * where someone most wants to make one.
2562
- *
2563
- * AVOID:
2564
- * - Doing work here. This section reports and launches; the panel and the tool
2565
- * own everything else.
2566
- */
2567
2545
  function WorktreesActivitySection(props) {
2546
+ const session = useStore(worktreeActivity.store, (state) => worktreeActivity.select(state, props.sessionId));
2568
2547
  return /* @__PURE__ */ jsxs("div", {
2569
- className: "flex min-w-0 flex-col gap-2",
2570
- children: [/* @__PURE__ */ jsx(DiffActivitySection, { ...props }), /* @__PURE__ */ jsx(WorktreesSummary, { ...props })]
2548
+ className: "flex min-w-0 flex-col gap-1",
2549
+ children: [/* @__PURE__ */ jsx(DiffActivitySection, { ...props }), /* @__PURE__ */ jsx(WorktreesActivityView, {
2550
+ worktrees: session.worktrees.filter((w) => w.sessionId !== props.sessionId),
2551
+ pending: session.pending,
2552
+ onOpen: () => props.openTransientTab(worktreesTab()),
2553
+ onCreate: props.sessionId === null ? void 0 : () => worktreeCreateDialog.update(() => ({
2554
+ sessionId: props.sessionId,
2555
+ ...props.sessionReservationId === void 0 ? {} : { reservationId: props.sessionReservationId }
2556
+ }))
2557
+ })]
2571
2558
  });
2572
2559
  }
2573
- function WorktreesSummary({ sessionId, openTransientTab }) {
2574
- const session = useStore(worktreeActivity.store, (state) => worktreeActivity.select(state, sessionId));
2575
- const worktrees = session.worktrees.filter((worktree) => worktree.sessionId !== sessionId);
2576
- if (worktrees.length === 0 && session.pending === void 0) return /* @__PURE__ */ jsxs("div", {
2577
- className: "flex flex-wrap items-center gap-x-4 gap-y-1",
2578
- children: [/* @__PURE__ */ jsx("p", {
2579
- "data-testid": "activity-summary-git",
2580
- className: "text-sm text-po-dim",
2581
- children: "Idle"
2582
- }), sessionId === null ? null : /* @__PURE__ */ jsxs(Button, {
2560
+ function WorktreesActivityView({ worktrees, pending, onOpen, onCreate, startingIds = [] }) {
2561
+ return /* @__PURE__ */ jsxs("div", {
2562
+ className: "flex min-w-0 flex-col gap-1",
2563
+ children: [pending && /* @__PURE__ */ jsxs("p", {
2564
+ className: "flex items-center gap-2 px-1 text-sm text-po-dim",
2565
+ children: [/* @__PURE__ */ jsx(Spinner, {
2566
+ className: "size-3",
2567
+ label: pending
2568
+ }), /* @__PURE__ */ jsx("span", {
2569
+ className: "inline-block first-letter:uppercase",
2570
+ children: pending
2571
+ })]
2572
+ }), worktrees.length === 0 ? onCreate ? /* @__PURE__ */ jsxs(Button, {
2583
2573
  variant: "link",
2584
2574
  size: "sm",
2585
2575
  "data-testid": "activity-git-create",
2586
- className: "h-auto gap-1 px-0 text-po-green",
2587
- onClick: () => openTransientTab(worktreesTab()),
2576
+ className: "h-auto self-start gap-1 px-1 text-po-green",
2577
+ onClick: onCreate,
2588
2578
  children: ["Create a worktree", /* @__PURE__ */ jsx(ArrowRightIcon, {
2589
2579
  "aria-hidden": true,
2590
2580
  className: "size-3.5"
2591
2581
  })]
2582
+ }) : /* @__PURE__ */ jsx("span", {
2583
+ "data-testid": "activity-summary-git",
2584
+ className: "px-1 text-sm text-po-dim",
2585
+ children: "Idle"
2586
+ }) : /* @__PURE__ */ jsx("div", {
2587
+ "data-testid": "activity-git-worktrees",
2588
+ children: worktrees.map((w) => /* @__PURE__ */ jsx(Button, {
2589
+ variant: "ghost",
2590
+ size: "card",
2591
+ className: "block w-full min-w-0 p-0 text-left font-normal",
2592
+ "data-testid": `activity-git-worktree-${w.id}`,
2593
+ onClick: onOpen,
2594
+ children: /* @__PURE__ */ jsx(WorktreeRow, {
2595
+ compact: true,
2596
+ worktree: w,
2597
+ starting: startingIds.includes(w.id) || !!pending?.includes("starting") && w.sessionId === null && !w.orphaned && !w.unowned
2598
+ })
2599
+ }, w.id))
2592
2600
  })]
2593
2601
  });
2594
- return /* @__PURE__ */ jsxs("div", {
2595
- "data-testid": "activity-git-worktrees",
2596
- className: "flex flex-col gap-0.5",
2597
- children: [session.pending === void 0 ? null : /* @__PURE__ */ jsxs("span", {
2598
- className: "flex min-w-0 items-center gap-1.5 px-1",
2599
- children: [/* @__PURE__ */ jsx(Spinner, {
2600
- className: "h-3 w-3 shrink-0 text-po-faint",
2601
- label: session.pending
2602
- }), /* @__PURE__ */ jsx("span", {
2603
- className: "min-w-0 truncate text-sm text-po-dim",
2604
- children: session.pending
2605
- })]
2606
- }), worktrees.map((worktree) => /* @__PURE__ */ jsx(Button, {
2607
- variant: "ghost",
2608
- size: "card",
2609
- "data-testid": `activity-git-worktree-${worktree.id}`,
2610
- className: "min-w-0 gap-1.5 px-1",
2611
- onClick: () => openTransientTab(worktreesTab()),
2612
- children: /* @__PURE__ */ jsxs("span", {
2613
- className: "flex w-full min-w-0 items-center gap-1.5",
2614
- children: [
2615
- /* @__PURE__ */ jsx(Dot, { tone: worktree.orphaned ? "red" : worktree.sessionId === null ? "muted" : "blue" }),
2616
- /* @__PURE__ */ jsx("span", {
2617
- className: "min-w-0 flex-1 truncate text-left text-base font-semibold text-po-hi",
2618
- children: worktree.branch
2619
- }),
2620
- worktree.orphaned ? /* @__PURE__ */ jsx(StatusBadge, {
2621
- tone: "error",
2622
- size: "xs",
2623
- children: "orphaned"
2624
- }) : null,
2625
- worktree.unowned ? /* @__PURE__ */ jsx(StatusBadge, {
2626
- tone: "info",
2627
- size: "xs",
2628
- children: "unowned"
2629
- }) : null
2630
- ]
2631
- })
2632
- }, worktree.id))]
2633
- });
2634
2602
  }
2635
2603
  //#endregion
2636
2604
  //#region src/extensions/workspaces/sessions/(frontend)/fill/Worktrees.activity.git.web.tsx
@@ -2664,49 +2632,162 @@ function startGitRuntime(runtime) {
2664
2632
  var start_web_default = defineWebLifecycle(startGitRuntime);
2665
2633
  //#endregion
2666
2634
  //#region src/extensions/workspaces/sessions/(frontend)/tool/_components/RunWorktreeToolMessage.tsx
2667
- const ACTION_LABEL = {
2668
- spawn_worktree: "Create worktree",
2669
- close_worktree: "Close worktree",
2670
- list: "List worktrees",
2671
- status: "Worktree status",
2672
- merge: "Merge worktree",
2673
- prune: "Prune worktrees",
2674
- send: "Message worktree",
2675
- messages: "Read worktree messages"
2635
+ const VERBS = {
2636
+ spawn_worktree: [
2637
+ "is creating",
2638
+ "created",
2639
+ "create"
2640
+ ],
2641
+ close_worktree: [
2642
+ "is closing",
2643
+ "closed",
2644
+ "close"
2645
+ ],
2646
+ list: [
2647
+ "is listing",
2648
+ "listed",
2649
+ "list"
2650
+ ],
2651
+ status: [
2652
+ "is checking",
2653
+ "checked",
2654
+ "check"
2655
+ ],
2656
+ merge: [
2657
+ "is merging",
2658
+ "merged",
2659
+ "merge"
2660
+ ],
2661
+ prune: [
2662
+ "is pruning",
2663
+ "pruned",
2664
+ "prune"
2665
+ ],
2666
+ send: [
2667
+ "is messaging",
2668
+ "messaged",
2669
+ "message"
2670
+ ],
2671
+ messages: [
2672
+ "is reading messages from",
2673
+ "read messages from",
2674
+ "read messages from"
2675
+ ]
2676
2676
  };
2677
- /**
2678
- * The timeline item for `run_worktree`.
2679
- *
2680
- * DESIGN PATTERNS:
2681
- * - Built on MessageItem, like every other tool renderer in the cockpit. That
2682
- * is what gives it the shared collapse toggle and the RUNNING/OK/ERROR badge
2683
- * rather than a hand-rolled ellipsis that never resolves.
2684
- * - The action and its subject go in the heading because that is what a reader
2685
- * scanning a transcript needs: "close_worktree a1b2c3d4" answers the question
2686
- * the raw JSON arguments make them decode.
2687
- * - A create takes as long as the cockpit needs to start a session, and the
2688
- * tool streams each phase into `output`, so the body is where the wait is
2689
- * visible.
2690
- */
2691
2677
  function RunWorktreeToolMessage({ args, output, running, isError }) {
2692
- const action = typeof args.action === "string" ? args.action : "";
2693
- const subject = typeof args.branch === "string" ? args.branch : typeof args.id === "string" ? args.id : "";
2694
- return /* @__PURE__ */ jsxs(MessageItem, {
2695
- tone: toolTone({
2696
- running,
2697
- isError
2698
- }),
2699
- expandable: output !== "",
2700
- children: [/* @__PURE__ */ jsx(MessageItemHeader, {
2701
- title: ACTION_LABEL[action] ?? "Worktree",
2702
- children: subject === "" ? null : /* @__PURE__ */ jsx("span", {
2703
- className: "min-w-0 truncate font-mono text-po-dim",
2704
- children: subject
2705
- })
2706
- }), output === "" ? null : /* @__PURE__ */ jsx(MessageItemBody, { children: /* @__PURE__ */ jsx("pre", {
2707
- className: `whitespace-pre-wrap font-mono text-sm ${isError ? "text-po-red" : "text-po-dim"}`,
2708
- children: output
2709
- }) })]
2678
+ return /* @__PURE__ */ jsx(RunWorktreeToolMessageView, {
2679
+ action: typeof args.action === "string" ? args.action : "",
2680
+ subject: typeof args.branch === "string" ? args.branch : typeof args.id === "string" ? args.id : "",
2681
+ output,
2682
+ running,
2683
+ isError
2684
+ });
2685
+ }
2686
+ function RunWorktreeToolMessageView({ action, subject = "", output = "", running = false, isError = false, defaultExpanded = false }) {
2687
+ const tone = running ? "neutral" : isError ? "error" : "ok";
2688
+ const verbs = VERBS[action] ?? [
2689
+ "is running",
2690
+ "finished",
2691
+ "run"
2692
+ ];
2693
+ const rows = action === "list" ? output.split("\n").map((line) => line.split(/\s{2,}/)).filter((parts) => parts.length >= 3) : [];
2694
+ const listingCount = output.match(/^(\d+) worktree/);
2695
+ return /* @__PURE__ */ jsx(MessageItem, {
2696
+ tone,
2697
+ expandable: true,
2698
+ defaultExpanded,
2699
+ className: "bg-po-panel",
2700
+ children: ({ expanded, toggle }) => /* @__PURE__ */ jsxs(Fragment, { children: [
2701
+ /* @__PURE__ */ jsxs("div", {
2702
+ className: "flex items-start gap-3 p-4",
2703
+ children: [/* @__PURE__ */ jsx("div", {
2704
+ className: "min-w-0 flex-1",
2705
+ children: /* @__PURE__ */ jsxs("p", {
2706
+ className: "text-base font-semibold leading-relaxed text-po-hi",
2707
+ children: [
2708
+ "Po ",
2709
+ isError && !running ? `could not ${verbs[2]}` : verbs[running ? 0 : 1],
2710
+ " ",
2711
+ action === "list" && !running && !isError ? `${listingCount?.[1] ?? rows.length} ${Number(listingCount?.[1] ?? rows.length) === 1 ? "worktree" : "worktrees"}` : subject ? /* @__PURE__ */ jsx("span", {
2712
+ className: "font-mono",
2713
+ children: subject
2714
+ }) : "worktrees",
2715
+ running ? "..." : isError && output ? `: ${output.split("\n")[0]}` : ""
2716
+ ]
2717
+ })
2718
+ }), /* @__PURE__ */ jsxs(StatusBadge, {
2719
+ tone,
2720
+ size: "xs",
2721
+ className: running ? "gap-1 bg-po-rail text-po-dim" : "",
2722
+ children: [running && /* @__PURE__ */ jsx(Spinner, {
2723
+ className: "size-3",
2724
+ label: "Running"
2725
+ }), running ? "Running" : isError ? "Failed" : "Completed"]
2726
+ })]
2727
+ }),
2728
+ /* @__PURE__ */ jsxs(Button, {
2729
+ variant: "ghost",
2730
+ size: "sm",
2731
+ onClick: toggle,
2732
+ "aria-expanded": expanded,
2733
+ className: "mb-2 ml-3 text-po-dim",
2734
+ children: ["Details ", expanded ? /* @__PURE__ */ jsx(ChevronUpIcon, { className: "size-3" }) : /* @__PURE__ */ jsx(ChevronDownIcon, { className: "size-3" })]
2735
+ }),
2736
+ expanded && /* @__PURE__ */ jsx(MessageItemBody, { children: rows.length > 0 && !isError ? /* @__PURE__ */ jsxs("table", {
2737
+ className: "w-full text-left text-sm",
2738
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", {
2739
+ className: "border-b border-po-border-soft",
2740
+ children: [
2741
+ /* @__PURE__ */ jsx("th", {
2742
+ className: "pb-2 font-medium",
2743
+ children: "Branch"
2744
+ }),
2745
+ /* @__PURE__ */ jsx("th", {
2746
+ className: "pb-2 font-medium",
2747
+ children: "Path"
2748
+ }),
2749
+ /* @__PURE__ */ jsx("th", {
2750
+ className: "pb-2 font-medium",
2751
+ children: "Status"
2752
+ })
2753
+ ]
2754
+ }) }), /* @__PURE__ */ jsx("tbody", { children: rows.map((parts, i) => {
2755
+ const real = parts.length >= 5;
2756
+ const status = parts[2];
2757
+ return /* @__PURE__ */ jsxs("tr", {
2758
+ className: "border-b border-po-border-soft",
2759
+ children: [
2760
+ /* @__PURE__ */ jsxs("td", {
2761
+ className: "py-3 pr-3 font-mono text-po-text",
2762
+ children: [real ? parts[1] : parts[0], real && /* @__PURE__ */ jsx("span", {
2763
+ className: "mt-1 block text-po-dim",
2764
+ children: parts[0]
2765
+ })]
2766
+ }),
2767
+ /* @__PURE__ */ jsx("td", {
2768
+ className: "py-3 pr-3 font-mono text-po-dim",
2769
+ children: real ? parts.slice(4).join(" ") : parts[1]
2770
+ }),
2771
+ /* @__PURE__ */ jsxs("td", { children: [/* @__PURE__ */ jsx(StatusBadge, {
2772
+ tone: status === "orphaned" ? "neutral" : status === "unowned" ? "info" : "ok",
2773
+ size: "xs",
2774
+ className: status === "orphaned" ? "bg-po-tint-yellow text-po-yellow" : "",
2775
+ children: /* @__PURE__ */ jsx("span", {
2776
+ className: "inline-block first-letter:uppercase",
2777
+ children: status
2778
+ })
2779
+ }), real && /* @__PURE__ */ jsx("span", {
2780
+ className: "ml-2 text-po-dim",
2781
+ children: parts[3]
2782
+ })] })
2783
+ ]
2784
+ }, i);
2785
+ }) })]
2786
+ }) : /* @__PURE__ */ jsx("pre", {
2787
+ className: `whitespace-pre-wrap font-mono text-sm ${isError ? "text-po-red" : "text-po-dim"}`,
2788
+ children: output
2789
+ }) })
2790
+ ] })
2710
2791
  });
2711
2792
  }
2712
2793
  //#endregion