@sealant/mend 0.35.1 → 0.36.0-next.628

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/{chunk-SMH65F6Z.js → chunk-OTYH35OK.js} +581 -4
  2. package/dist/{dashboard-MGR4YUFD.js → dashboard-P73HKQOT.js} +225 -98
  3. package/dist/main.js +2495 -490
  4. package/man/mend-accounts.1 +1 -1
  5. package/man/mend-adopt.1 +1 -1
  6. package/man/mend-attach.1 +1 -1
  7. package/man/mend-codex.1 +7 -7
  8. package/man/mend-completions.1 +1 -1
  9. package/man/mend-connect-pi.1 +28 -0
  10. package/man/mend-connect.1 +4 -2
  11. package/man/mend-continue.1 +1 -1
  12. package/man/mend-doctor.1 +1 -1
  13. package/man/mend-dotfiles-repo.1 +1 -1
  14. package/man/mend-dotfiles-sync.1 +1 -1
  15. package/man/mend-dotfiles.1 +1 -1
  16. package/man/mend-env-cluster.1 +1 -1
  17. package/man/mend-env-load.1 +1 -1
  18. package/man/mend-env-show.1 +1 -1
  19. package/man/mend-folder-create.1 +1 -1
  20. package/man/mend-folder-list.1 +1 -1
  21. package/man/mend-folder-push.1 +1 -1
  22. package/man/mend-folder-rm.1 +1 -1
  23. package/man/mend-git-author.1 +1 -1
  24. package/man/mend-help.1 +1 -1
  25. package/man/mend-invite.1 +1 -1
  26. package/man/mend-keys-init.1 +1 -1
  27. package/man/mend-keys-mode.1 +1 -1
  28. package/man/mend-keys-share.1 +1 -1
  29. package/man/mend-keys-show.1 +1 -1
  30. package/man/mend-land.1 +1 -1
  31. package/man/mend-login.1 +1 -1
  32. package/man/mend-logout.1 +1 -1
  33. package/man/mend-man.1 +1 -1
  34. package/man/mend-members.1 +1 -1
  35. package/man/mend-memory-import.1 +23 -0
  36. package/man/mend-memory-rm.1 +15 -0
  37. package/man/mend-memory-show.1 +15 -0
  38. package/man/mend-memory.1 +32 -0
  39. package/man/mend-models.1 +22 -0
  40. package/man/mend-operator-exposure.1 +1 -1
  41. package/man/mend-operator-gate.1 +1 -1
  42. package/man/mend-operator-grant-owner.1 +1 -1
  43. package/man/mend-operator-org-create.1 +1 -1
  44. package/man/mend-operator-org-invite-owner.1 +1 -1
  45. package/man/mend-operator-org-list.1 +1 -1
  46. package/man/mend-operator-org-rename.1 +1 -1
  47. package/man/mend-operator-reset-link.1 +1 -1
  48. package/man/mend-pair.1 +1 -1
  49. package/man/mend-projects.1 +1 -1
  50. package/man/mend-pull.1 +1 -1
  51. package/man/mend-refresh.1 +1 -1
  52. package/man/mend-rejoin.1 +1 -1
  53. package/man/mend-resume.1 +1 -1
  54. package/man/mend-run.1 +1 -1
  55. package/man/mend-secrets-add.1 +24 -0
  56. package/man/mend-secrets-rm.1 +14 -0
  57. package/man/mend-secrets.1 +18 -0
  58. package/man/mend-server-logs.1 +1 -1
  59. package/man/mend-server-restart.1 +1 -1
  60. package/man/mend-server-setup.1 +27 -3
  61. package/man/mend-server-start.1 +1 -1
  62. package/man/mend-server-status.1 +5 -3
  63. package/man/mend-server-stop.1 +3 -3
  64. package/man/mend-server-upgrade.1 +7 -2
  65. package/man/mend-server.1 +1 -1
  66. package/man/mend-service-add.1 +1 -1
  67. package/man/mend-service-connect.1 +1 -1
  68. package/man/mend-service-init.1 +1 -1
  69. package/man/mend-service-list.1 +1 -1
  70. package/man/mend-service-logs.1 +1 -1
  71. package/man/mend-service-restart.1 +1 -1
  72. package/man/mend-service-run.1 +1 -1
  73. package/man/mend-service-stop.1 +1 -1
  74. package/man/mend-session-share.1 +2 -2
  75. package/man/mend-sessions.1 +1 -1
  76. package/man/mend-shell.1 +1 -1
  77. package/man/mend-skills-push.1 +1 -1
  78. package/man/mend-skills.1 +1 -1
  79. package/man/mend-snake.1 +1 -1
  80. package/man/mend-ssh-setup.1 +1 -1
  81. package/man/mend-ssh.1 +1 -1
  82. package/man/mend-stop.1 +1 -1
  83. package/man/mend-ui.1 +1 -1
  84. package/man/mend-uninstall.1 +1 -1
  85. package/man/mend-version.1 +1 -1
  86. package/man/mend-worktrees-rm.1 +29 -0
  87. package/man/mend-worktrees.1 +2 -2
  88. package/man/mend.1 +35 -5
  89. package/package.json +19 -38
@@ -9,13 +9,15 @@ import {
9
9
  cwdFacts,
10
10
  eventFamilies,
11
11
  isPendingId,
12
+ launchPhaseOf,
12
13
  matchProjectByCwd,
13
14
  mayStillBeWorking,
14
15
  normalizeProjectName,
15
16
  pendingId,
16
17
  repositoryCloneUrlIssue,
17
- servicesHoldLine
18
- } from "./chunk-SMH65F6Z.js";
18
+ servicesHoldLine,
19
+ withoutLaunchPhase
20
+ } from "./chunk-OTYH35OK.js";
19
21
 
20
22
  // src/dashboard.tsx
21
23
  import { CliRenderEvents, createCliRenderer } from "@opentui/core";
@@ -127,8 +129,9 @@ var deriveProjects = (data) => (data?.projects ?? []).map((project) => {
127
129
  const detail = data?.details.get(project.id);
128
130
  const sessions = detail?.sessions ?? [];
129
131
  const live = sessions.filter((s) => LIVE_STATUSES.has(s.status)).length;
132
+ const stopping = sessions.filter((s) => s.status === "stopping").length;
130
133
  const open = (detail?.annotations ?? []).reduce((sum, a) => sum + a.openComments, 0);
131
- return { project, total: sessions.length, live, open };
134
+ return { project, total: sessions.length, live, stopping, open };
132
135
  });
133
136
  var bySessionRecency = (a, b) => {
134
137
  const aLive = LIVE_STATUSES.has(a.status) ? 1 : 0;
@@ -142,7 +145,10 @@ var toSessionItem = (data, detail) => (session) => ({
142
145
  services: data.servicesBySession.get(session.id) ?? [],
143
146
  processes: data.processesBySession.get(session.id) ?? []
144
147
  });
145
- var isDeadEnd = (session) => !LIVE_STATUSES.has(session.status) && session.hasTranscript === false;
148
+ var isDeadEnd = (session) => (
149
+ // A stopping session may still be saving, or kept for recovery: never hidden (story S23).
150
+ !LIVE_STATUSES.has(session.status) && session.status !== "stopping" && session.hasTranscript === false
151
+ );
146
152
  var deriveWorktrees = (data, projectId) => {
147
153
  if (data === void 0 || projectId === null) return [];
148
154
  const detail = data.details.get(projectId);
@@ -199,13 +205,13 @@ var deriveHarnesses = (resuming) => {
199
205
  {
200
206
  harness: null,
201
207
  label: resuming.harness,
202
- hint: "same harness \u2014 native resume, conversation intact"
208
+ hint: "same harness \xB7 native resume, conversation intact"
203
209
  },
204
210
  ...others.map(
205
211
  (harness) => ({
206
212
  harness,
207
213
  label: harness,
208
- hint: harness === "shell" ? "a bash in the worktree \u2014 resume either agent from inside" : "the conversation crosses as a distilled prompt"
214
+ hint: harness === "shell" ? "a bash in the worktree \xB7 resume either agent from inside" : "the conversation crosses as a distilled prompt"
209
215
  })
210
216
  )
211
217
  ];
@@ -214,7 +220,7 @@ var deriveHarnesses = (resuming) => {
214
220
  (harness) => ({
215
221
  harness,
216
222
  label: harness,
217
- hint: harness === "shell" ? "a plain bash session \u2014 new worktree, recorded" : `mend ${harness} \u2014 new worktree, recorded session`
223
+ hint: harness === "shell" ? "a plain bash session \xB7 new worktree, recorded" : `mend ${harness} \xB7 new worktree, recorded session`
218
224
  })
219
225
  );
220
226
  };
@@ -233,11 +239,81 @@ var groupActivityAt = (group) => group.sessions.reduce(
233
239
  group.createdAt
234
240
  );
235
241
  var foldGroupStatus = (group) => {
236
- const statuses = group.sessions.map((item) => item.session.status);
237
- if (statuses.includes("waiting")) return "waiting";
238
- if (statuses.includes("running") || statuses.includes("starting")) return "running";
239
- if (statuses.includes("idle")) return "idle";
240
- return statuses[0] ?? "idle";
242
+ const statuses = new Set(group.sessions.map((item) => item.session.status));
243
+ for (const word of ["waiting", "running", "starting", "stopping", "idle"]) {
244
+ if (statuses.has(word)) return word;
245
+ }
246
+ return "settled";
247
+ };
248
+ var liveCountWords = (live, stopping) => {
249
+ const words = [
250
+ ...live > 0 ? [`${live} live`] : [],
251
+ ...stopping > 0 ? [`${stopping} stopping`] : []
252
+ ];
253
+ return words.length === 0 ? "settled" : words.join(" \xB7 ");
254
+ };
255
+ var elapsedWords = (iso, now = Date.now()) => {
256
+ const ms = now - new Date(iso).getTime();
257
+ if (!Number.isFinite(ms) || ms < 0) return null;
258
+ const minutes = Math.floor(ms / 6e4);
259
+ if (minutes < 1) return "<1m";
260
+ if (minutes < 60) return `${minutes}m`;
261
+ const hours = Math.floor(minutes / 60);
262
+ if (hours < 24) return `${hours}h`;
263
+ return `${Math.floor(hours / 24)}d`;
264
+ };
265
+ var startingWordsOf = (session) => {
266
+ if (isPendingId(session.id)) return "launching";
267
+ const phase = launchPhaseOf(session.summary);
268
+ if (phase === null) return "launching";
269
+ switch (phase.kind) {
270
+ case "waiting-previous":
271
+ return phase.words.endsWith("is saving") ? "waiting for the previous save" : "waiting for the previous session";
272
+ case "booting":
273
+ return "booting";
274
+ case "preparing":
275
+ return "preparing the workspace";
276
+ }
277
+ };
278
+ var startingExplanationOf = (session) => {
279
+ const phase = isPendingId(session.id) ? null : launchPhaseOf(session.summary);
280
+ if (phase === null) return "launching";
281
+ switch (phase.kind) {
282
+ case "waiting-previous":
283
+ return phase.words.endsWith("is saving") ? "the previous session in this worktree is still saving \xB7 this one starts from its save" : phase.words;
284
+ case "booting":
285
+ return "the workspace is booting \xB7 the agent starts when it is up";
286
+ case "preparing":
287
+ return "the workspace has no runtime yet \xB7 after an update, building its image takes about 8 minutes";
288
+ }
289
+ };
290
+ var sessionTimeWords = (item, now = Date.now()) => {
291
+ const { session } = item;
292
+ if (session.status === "starting" || session.status === "stopping") return null;
293
+ const agent = item.annotation?.currentAgent ?? null;
294
+ const created = elapsedWords(session.createdAt, now);
295
+ const createdWords = created === null ? null : `created ${agoWords(created)}`;
296
+ if (LIVE_STATUSES.has(session.status)) {
297
+ if (agent === null || agent.createdAt === void 0) return createdWords;
298
+ if (agent.exitedAt !== null) {
299
+ const ended2 = elapsedWords(agent.exitedAt, now);
300
+ return ended2 === null ? createdWords : `agent ended ${agoWords(ended2)}`;
301
+ }
302
+ const up = elapsedWords(agent.createdAt, now);
303
+ if (up === null) return createdWords;
304
+ if (agent.kind === "agent-external") return `activity seen ${agoWords(up)}`;
305
+ return session.harness === "shell" ? `shell up ${up}` : `up ${up}`;
306
+ }
307
+ const settledAt = session.settledAt ?? null;
308
+ const ended = settledAt === null ? null : elapsedWords(settledAt, now);
309
+ return ended === null ? createdWords : `ended ${agoWords(ended)}`;
310
+ };
311
+ var agoWords = (elapsed) => elapsed === "<1m" ? "just now" : `${elapsed} ago`;
312
+ var sessionStateWords = (item, now = Date.now()) => {
313
+ const hold = sessionHold(item);
314
+ if (hold !== null) return hold;
315
+ if (item.session.status === "starting") return startingWordsOf(item.session);
316
+ return sessionTimeWords(item, now);
241
317
  };
242
318
  var liveShellOf = (processes) => processes.findLast((process2) => process2.kind === "shell" && process2.exitedAt === null) ?? null;
243
319
  var liveProtocolOf = (processes) => processes.find((process2) => process2.kind === "agent-protocol" && process2.exitedAt === null) ?? null;
@@ -276,7 +352,14 @@ var markSessionStopped = (data, sessionId) => {
276
352
  return {
277
353
  ...mapWorkbenchSessions(
278
354
  data,
279
- (session) => session.id === sessionId ? { ...session, status: "stopped" } : session
355
+ (session) => (
356
+ // Asked, not observed (story S28): `stopping` until the server says how it ended. A stop
357
+ // leaves Services running, so a session with Services reads as stopped with what holds it.
358
+ session.id === sessionId ? {
359
+ ...session,
360
+ status: (data.servicesBySession.get(sessionId) ?? []).length > 0 ? "stopped" : "stopping"
361
+ } : session
362
+ )
280
363
  ),
281
364
  processesBySession
282
365
  };
@@ -290,6 +373,13 @@ var AGENT_WORKING = /* @__PURE__ */ new Set(["starting", "running", "waiting"]);
290
373
  var sessionHold = (item) => {
291
374
  const capture = captureLineOf(item.session);
292
375
  if (capture !== null) return capture;
376
+ if (item.session.status === "stopping") {
377
+ const agent = item.annotation?.currentAgent ?? null;
378
+ return agent !== null && agent.exitedAt !== null ? "agent ended \xB7 workspace end not confirmed" : "workspace end not confirmed";
379
+ }
380
+ return servicesHoldOf(item);
381
+ };
382
+ var servicesHoldOf = (item) => {
293
383
  const agent = item.annotation?.currentAgent ?? null;
294
384
  const agentProcessLive = item.processes.some(
295
385
  (process2) => process2.kind !== "shell" && process2.exitedAt === null
@@ -300,6 +390,24 @@ var sessionHold = (item) => {
300
390
  liveServices: Math.max(item.annotation?.liveServices ?? 0, item.services.length)
301
391
  });
302
392
  };
393
+ var sessionRowFacts = (item, width, now = Date.now()) => {
394
+ const agents = item.processes.filter((process2) => process2.kind !== "shell").length;
395
+ const shells = item.processes.filter((process2) => process2.kind === "shell").length;
396
+ const services = item.services.length;
397
+ const state = sessionStateWords(item, now);
398
+ const stateWidth = width > 40 ? width - 9 : width - 2;
399
+ return fitHints(
400
+ [
401
+ ...state === null ? [] : [cut(state, stateWidth)],
402
+ item.session.harness,
403
+ ...shells > 0 ? [`${shells} shell`] : [],
404
+ ...agents > 0 ? [`${agents} agent`] : [],
405
+ ...services > 0 && servicesHoldOf(item) === null ? [`${services} service`] : []
406
+ ],
407
+ width
408
+ );
409
+ };
410
+ var cut = (text, width) => text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}\u2026`;
303
411
  var removeWorktreeGroup = (data, projectId, group) => {
304
412
  const memberIds = new Set(group.sessions.map((item) => item.session.id));
305
413
  const detail = data.details.get(projectId);
@@ -344,12 +452,12 @@ var filterBranches = (branches, query) => {
344
452
  };
345
453
  var baseStepNotice = (state) => {
346
454
  if (state.branchError !== null) {
347
- return `branches unreadable \u2014 ${state.branchError} \xB7 enter uses the default`;
455
+ return `branches unreadable \xB7 ${state.branchError} \xB7 enter uses the default`;
348
456
  }
349
457
  if (state.branches === null) return "reading branches\u2026";
350
- if (state.branches.length === 0) return "no branches read \u2014 enter uses the default branch";
458
+ if (state.branches.length === 0) return "no branches read \xB7 enter uses the default branch";
351
459
  if (filterBranches(state.branches, state.query).length === 0) {
352
- return "no branch matches \u2014 enter uses the default branch";
460
+ return "no branch matches \xB7 enter uses the default branch";
353
461
  }
354
462
  return null;
355
463
  };
@@ -2155,7 +2263,9 @@ var STATUS_COLOR = {
2155
2263
  idle: INK_2,
2156
2264
  completed: FAINT,
2157
2265
  failed: ERROR,
2158
- stopped: FAINT
2266
+ stopped: FAINT,
2267
+ stopping: INK_2,
2268
+ settled: FAINT
2159
2269
  };
2160
2270
  var AGENT_LIVE_STATUSES = /* @__PURE__ */ new Set(["starting", "running", "waiting"]);
2161
2271
  var timeAgo = (iso) => {
@@ -2168,7 +2278,13 @@ var timeAgo = (iso) => {
2168
2278
  if (hours < 24) return `${hours}h ago`;
2169
2279
  return `${Math.floor(hours / 24)}d ago`;
2170
2280
  };
2171
- var shortAge = (iso) => timeAgo(iso).replace(" ago", "");
2281
+ var lastSessionWords = (group) => {
2282
+ const at = group.sessions.length === 0 ? group.createdAt : groupActivityAt(group);
2283
+ const elapsed = elapsedWords(at);
2284
+ if (elapsed === null) return [];
2285
+ const ago = elapsed === "<1m" ? "just now" : `${elapsed} ago`;
2286
+ return [group.sessions.length === 0 ? `created ${ago}` : `last session created ${ago}`];
2287
+ };
2172
2288
  var fit = (text, width) => text.length <= width ? text : `${text.slice(0, Math.max(0, width - 1))}\u2026`;
2173
2289
  var COLUMN_TITLE = {
2174
2290
  projects: "projects",
@@ -2234,7 +2350,7 @@ var ProjectRow = ({
2234
2350
  return /* @__PURE__ */ jsx3("box", { height: 1, flexShrink: 0, backgroundColor: selected ? WASH : "transparent", children: /* @__PURE__ */ jsxs3("text", { height: 1, bg: "transparent", children: [
2235
2351
  /* @__PURE__ */ jsx3(Gutter, { selected }),
2236
2352
  /* @__PURE__ */ jsx3("span", { fg: INK, children: fit(item.project.name, nameWidth).padEnd(nameWidth + 1) }),
2237
- /* @__PURE__ */ jsx3("span", { fg: item.live > 0 ? MUTED : FAINT, children: item.live > 0 ? `${item.live}/${item.total}` : String(item.total) })
2353
+ /* @__PURE__ */ jsx3("span", { fg: item.live + item.stopping > 0 ? MUTED : FAINT, children: item.live + item.stopping > 0 ? `${item.live + item.stopping}/${item.total}` : String(item.total) })
2238
2354
  ] }) });
2239
2355
  };
2240
2356
  var WorktreeRow = ({
@@ -2242,18 +2358,16 @@ var WorktreeRow = ({
2242
2358
  selected,
2243
2359
  width
2244
2360
  }) => {
2245
- const folded = foldGroupStatus(group);
2246
- const live = group.live > 0;
2247
- const status = live ? folded : "settled";
2248
- const color = live ? STATUS_COLOR[folded] ?? MUTED : FAINT;
2361
+ const status = foldGroupStatus(group);
2362
+ const color = STATUS_COLOR[status] ?? MUTED;
2249
2363
  const open = group.annotation?.openComments ?? 0;
2250
2364
  const sessions = group.sessions.length;
2251
2365
  const nameWidth = Math.max(6, width - 5 - status.length);
2252
2366
  const factWidth = Math.max(8, width - 3);
2253
2367
  const facts = fitHints(
2254
2368
  [
2255
- fit(groupBaseLabel(group), factWidth),
2256
- shortAge(groupActivityAt(group)),
2369
+ fit(`from ${groupBaseLabel(group)}`, Math.max(12, Math.floor(factWidth / 2))),
2370
+ ...lastSessionWords(group),
2257
2371
  ...open > 0 ? [`${open} open`] : [],
2258
2372
  ...sessions === 0 ? ["empty"] : sessions === 1 ? [] : [`${sessions} sessions`]
2259
2373
  ],
@@ -2273,25 +2387,12 @@ var SessionRow = ({
2273
2387
  selected,
2274
2388
  width
2275
2389
  }) => {
2276
- const { session, processes, services } = item;
2390
+ const { session } = item;
2277
2391
  const status = session.status;
2278
2392
  const color = STATUS_COLOR[status] ?? MUTED;
2279
- const agents = processes.filter((process2) => process2.kind !== "shell").length;
2280
- const shells = processes.filter((process2) => process2.kind === "shell").length;
2281
2393
  const nameWidth = Math.max(6, width - 5 - status.length);
2282
2394
  const factWidth = Math.max(8, width - 3);
2283
- const hold = sessionHold(item);
2284
- const facts = fitHints(
2285
- [
2286
- ...hold === null ? [] : [hold],
2287
- session.harness,
2288
- shortAge(session.createdAt),
2289
- ...shells > 0 ? [`${shells} shell`] : [],
2290
- ...agents > 0 ? [`${agents} agent`] : [],
2291
- ...hold === null && services.length > 0 ? [`${services.length} service`] : []
2292
- ],
2293
- factWidth
2294
- );
2395
+ const facts = sessionRowFacts(item, factWidth);
2295
2396
  return /* @__PURE__ */ jsxs3("box", { flexShrink: 0, flexDirection: "column", backgroundColor: selected ? WASH : "transparent", children: [
2296
2397
  /* @__PURE__ */ jsx3("box", { height: 1, flexShrink: 0, backgroundColor: "transparent", children: /* @__PURE__ */ jsxs3("text", { height: 1, bg: "transparent", children: [
2297
2398
  /* @__PURE__ */ jsx3(Gutter, { selected }),
@@ -2341,7 +2442,10 @@ var SessionFacts = ({
2341
2442
  const { session, annotation, services } = item;
2342
2443
  const color = STATUS_COLOR[session.status] ?? MUTED;
2343
2444
  const hold = sessionHold(item);
2344
- const summary = session.summary?.split("\n")[0] ?? null;
2445
+ const starting = session.status === "starting";
2446
+ const phase = starting ? startingWordsOf(session) : null;
2447
+ const summary = (starting ? withoutLaunchPhase(session.summary) : session.summary)?.split("\n")[0] ?? null;
2448
+ const when = starting ? null : sessionTimeWords(item);
2345
2449
  const change = annotation ?? group?.annotation;
2346
2450
  const lines = [
2347
2451
  /* @__PURE__ */ jsxs3("text", { height: 1, bg: "transparent", children: [
@@ -2352,6 +2456,10 @@ var SessionFacts = ({
2352
2456
  /* @__PURE__ */ jsxs3("text", { height: 1, bg: "transparent", children: [
2353
2457
  /* @__PURE__ */ jsx3("span", { children: " " }),
2354
2458
  /* @__PURE__ */ jsx3("span", { fg: color, children: session.status }),
2459
+ phase === null ? null : /* @__PURE__ */ jsxs3(Fragment3, { children: [
2460
+ /* @__PURE__ */ jsx3("span", { fg: FAINT, children: " \xB7 " }),
2461
+ /* @__PURE__ */ jsx3("span", { fg: INK_2, children: phase })
2462
+ ] }),
2355
2463
  hold === null ? null : /* @__PURE__ */ jsxs3(Fragment3, { children: [
2356
2464
  /* @__PURE__ */ jsx3("span", { fg: FAINT, children: " \xB7 " }),
2357
2465
  /* @__PURE__ */ jsx3("span", { fg: INK_2, children: hold })
@@ -2360,7 +2468,11 @@ var SessionFacts = ({
2360
2468
  /* @__PURE__ */ jsx3("span", { fg: MUTED, children: session.branch }),
2361
2469
  /* @__PURE__ */ jsx3("span", { fg: FAINT, children: session.baseSha === "" ? "" : ` vs ${session.baseRef ?? session.baseSha.slice(0, 12)}` })
2362
2470
  ] }, "status"),
2363
- /* @__PURE__ */ jsx3("text", { height: 1, bg: "transparent", fg: FAINT, children: ` started ${timeAgo(session.createdAt)} \xB7 ${isPendingId(session.id) ? "provisioning" : session.id.slice(0, 8)}` }, "started"),
2471
+ /* @__PURE__ */ jsx3("text", { height: 1, bg: "transparent", fg: FAINT, children: [
2472
+ ...timeAgo(session.createdAt) === "" ? [] : [`created ${timeAgo(session.createdAt)}`],
2473
+ isPendingId(session.id) ? "provisioning" : session.id.slice(0, 8),
2474
+ ...when === null || when.startsWith("created ") ? [] : [when]
2475
+ ].join(" \xB7 ").replace(/^/, " ") }, "started"),
2364
2476
  /* @__PURE__ */ jsxs3("text", { height: 1, bg: "transparent", children: [
2365
2477
  /* @__PURE__ */ jsx3("span", { children: " " }),
2366
2478
  services.length === 0 ? /* @__PURE__ */ jsx3("span", { fg: FAINT, children: "no services running" }) : services.slice(0, 2).map((service, index) => {
@@ -2409,7 +2521,7 @@ var StatusLine = ({
2409
2521
  }
2410
2522
  return void 0;
2411
2523
  }, [busy, status]);
2412
- const text = busy === null ? status !== null && Date.now() - status.at < 5e3 ? ` ${status.text}` : "" : ` ${busy} ${Math.max(0, Math.round((now - busyStarted) / 1e3))}s`;
2524
+ const text = busy === null ? status !== null && Date.now() - status.at < 5e3 ? ` ${status.text}` : "" : ` ${busy} ${Math.max(0, Math.round((now - busyStarted) / 1e3))}s elapsed`;
2413
2525
  return /* @__PURE__ */ jsx3("text", { height: 1, fg: INK_2, bg: "transparent", children: text });
2414
2526
  };
2415
2527
  var errorText = (error) => error instanceof Error ? error.message : String(error);
@@ -2421,7 +2533,7 @@ var startedOrStarting = async (start, starting) => {
2421
2533
  throw error;
2422
2534
  }
2423
2535
  };
2424
- var startedLine = (session) => session.status === "starting" ? `still starting \xB7 ${sessionDisplayName(session)} \u2014 a attaches once the row reads running` : `started \xB7 ${sessionDisplayName(session)} \u2014 a attaches`;
2536
+ var startedLine = (session) => session.status === "starting" ? `still starting \xB7 ${sessionDisplayName(session)} \xB7 a attaches once the row reads running` : `started \xB7 ${sessionDisplayName(session)} \xB7 a attaches`;
2425
2537
  var keepSpanVisible = (scroll, top, height, viewRows) => {
2426
2538
  if (scroll === null || viewRows <= 0) return;
2427
2539
  const viewH = Math.max(1, viewRows);
@@ -2563,9 +2675,9 @@ var App = ({ ctx, onQuit }) => {
2563
2675
  return share.subscribe((event) => {
2564
2676
  if (event === null) return;
2565
2677
  if (event.kind === "sign-requested") {
2566
- say(`\u270E signature requested (${event.context}) \u2014 touch your key if it blinks`);
2678
+ say(`\u270E signature requested \xB7 ${event.context} \xB7 touch your key if it blinks`);
2567
2679
  } else if (event.kind === "signed") say(`\u2713 signed (${event.seconds.toFixed(1)}s)`);
2568
- else if (event.kind === "not-signed") say(`\u2717 not signed \u2014 ${event.message}`);
2680
+ else if (event.kind === "not-signed") say(`\u2717 not signed \xB7 ${event.message}`);
2569
2681
  });
2570
2682
  }, [ctx.agentShare]);
2571
2683
  useEffect3(() => {
@@ -2622,7 +2734,7 @@ ${tunneled}
2622
2734
  const liveProtocol = liveProtocolOf(detail.processes);
2623
2735
  const existing = liveShellOf(detail.processes);
2624
2736
  if (liveProtocol !== null) {
2625
- process.stdout.write(`taking over from the protocol session \u2014 same conversation\u2026
2737
+ process.stdout.write(`taking over from the protocol session \xB7 same conversation\u2026
2626
2738
 
2627
2739
  `);
2628
2740
  await ctx.api("POST", `/sessions/${session.id}/handoff`, { to: "pty" });
@@ -2632,12 +2744,12 @@ ${tunneled}
2632
2744
  "POST",
2633
2745
  `/sessions/${session.id}/shell`
2634
2746
  );
2635
- process.stdout.write(`no live terminal \u2014 opened a shell in the workspace
2747
+ process.stdout.write(`no live terminal \xB7 opened a shell in the workspace
2636
2748
 
2637
2749
  `);
2638
2750
  outcome = await ctx.attachTty(session.id, "shell", shell.id);
2639
2751
  } else {
2640
- process.stdout.write(`no live terminal \u2014 rejoining the open shell
2752
+ process.stdout.write(`no live terminal \xB7 rejoining the open shell
2641
2753
 
2642
2754
  `);
2643
2755
  outcome = await ctx.attachTty(session.id, "shell", existing.id);
@@ -2651,7 +2763,7 @@ ${tunneled}
2651
2763
  lockRef.current = false;
2652
2764
  }
2653
2765
  say(
2654
- outcome === "unavailable" ? "attach unavailable \u2014 could not connect" : outcome === "no-answer" ? `no answer \u2014 the server did not open ${short}'s terminal; it keeps running \xB7 a tries again` : outcome === "detached" || outcome === "interrupted" ? `detached \u2014 ${short} keeps running` : outcome === "dropped" ? `disconnected \xB7 ${short} \u2014 refreshing session status` : `terminal ended \xB7 ${short} \u2014 refreshing session status`
2766
+ outcome === "unavailable" ? "attach unavailable \xB7 could not connect" : outcome === "no-answer" ? `no answer \xB7 the server did not open ${short}'s terminal; it keeps running \xB7 a tries again` : outcome === "detached" || outcome === "interrupted" ? `detached \xB7 ${short} keeps running` : outcome === "dropped" ? `disconnected \xB7 ${short} \xB7 refreshing session status` : `terminal ended \xB7 ${short} \xB7 refreshing session status`
2655
2767
  );
2656
2768
  refetch();
2657
2769
  };
@@ -2665,8 +2777,9 @@ ${tunneled}
2665
2777
  };
2666
2778
  const launchMutation = useMutation2({
2667
2779
  mutationFn: async (vars) => {
2668
- const argv = HARNESS_COMMANDS[vars.harness];
2669
- if (argv === void 0) throw new Error(`unknown harness "${vars.harness}"`);
2780
+ if (HARNESS_COMMANDS[vars.harness] === void 0) {
2781
+ throw new Error(`unknown harness "${vars.harness}"`);
2782
+ }
2670
2783
  const session = await ctx.api("POST", `/projects/${vars.projectId}/sessions`, {
2671
2784
  harness: vars.harness,
2672
2785
  label: null,
@@ -2674,7 +2787,7 @@ ${tunneled}
2674
2787
  base: vars.base
2675
2788
  });
2676
2789
  return startedOrStarting(
2677
- ctx.api("POST", `/sessions/${session.id}/launch`, { argv }),
2790
+ ctx.api("POST", `/sessions/${session.id}/launch`, {}),
2678
2791
  session
2679
2792
  );
2680
2793
  },
@@ -2695,7 +2808,7 @@ ${tunneled}
2695
2808
  );
2696
2809
  setWorktreeKey(vars.pendingKey);
2697
2810
  selectSession(vars.pendingKey);
2698
- setBusy(`provisioning ${vars.harness} workspace \u2014 a first launch builds the harness image \xB7`);
2811
+ setBusy(`provisioning ${vars.harness} workspace \xB7`);
2699
2812
  setBusyStarted(Date.now());
2700
2813
  },
2701
2814
  onError: (error, vars) => {
@@ -2736,7 +2849,7 @@ ${tunneled}
2736
2849
  );
2737
2850
  selectSession(vars.session.id);
2738
2851
  setBusy(
2739
- `resuming ${sessionDisplayName(vars.session)} \u2014 a fresh workspace restores the saved state \xB7`
2852
+ `resuming ${sessionDisplayName(vars.session)} \xB7 a fresh workspace restores the saved state \xB7`
2740
2853
  );
2741
2854
  setBusyStarted(Date.now());
2742
2855
  },
@@ -2751,7 +2864,7 @@ ${tunneled}
2751
2864
  );
2752
2865
  setBusy(null);
2753
2866
  say(
2754
- resumed.status === "starting" ? `still resuming \xB7 ${sessionDisplayName(resumed)} \u2014 a attaches once the row reads running` : `resumed \xB7 ${sessionDisplayName(resumed)} \u2014 a attaches`
2867
+ resumed.status === "starting" ? `still resuming \xB7 ${sessionDisplayName(resumed)} \xB7 a attaches once the row reads running` : `resumed \xB7 ${sessionDisplayName(resumed)} \xB7 a attaches`
2755
2868
  );
2756
2869
  },
2757
2870
  onSettled: (_data, _error, vars) => {
@@ -2768,7 +2881,7 @@ ${tunneled}
2768
2881
  {
2769
2882
  mode: "mend-key",
2770
2883
  label: "mend-key",
2771
- hint: "the machine's Mend deploy key \u2014 add its public half on the git host"
2884
+ hint: "the machine's Mend deploy key \xB7 add its public half on the git host"
2772
2885
  },
2773
2886
  {
2774
2887
  mode: "bridge",
@@ -2784,7 +2897,7 @@ ${tunneled}
2784
2897
  }),
2785
2898
  onMutate: (offer) => {
2786
2899
  setAdoptOffer(null);
2787
- setBusy(`adopting ${offer.name} \u2014 cloning into the store \xB7`);
2900
+ setBusy(`adopting ${offer.name} \xB7 cloning into the store \xB7`);
2788
2901
  setBusyStarted(Date.now());
2789
2902
  },
2790
2903
  onError: (error) => {
@@ -2798,7 +2911,7 @@ ${tunneled}
2798
2911
  return;
2799
2912
  }
2800
2913
  const project = result.project;
2801
- say(`adopted \xB7 ${project.name} \u2014 w starts a worktree`);
2914
+ say(`adopted \xB7 ${project.name} \xB7 w starts a worktree`);
2802
2915
  setProjectKey(project.id);
2803
2916
  setWorktreeKey(null);
2804
2917
  setSessionKey(null);
@@ -2833,7 +2946,7 @@ ${tunneled}
2833
2946
  },
2834
2947
  onSuccess: (result, session) => {
2835
2948
  say(
2836
- `stopped ${result.stopped} service${result.stopped === 1 ? "" : "s"} \xB7 ${sessionDisplayName(session)} \u2014 the workspace ends once nothing is live`
2949
+ `stopped ${result.stopped} service${result.stopped === 1 ? "" : "s"} \xB7 ${sessionDisplayName(session)} \xB7 the workspace ends once nothing is live`
2837
2950
  );
2838
2951
  },
2839
2952
  onError: (error) => {
@@ -2847,7 +2960,7 @@ ${tunneled}
2847
2960
  onMutate: async (session) => {
2848
2961
  await queryClient.cancelQueries({ queryKey: WORKBENCH_KEY });
2849
2962
  patchWorkbench((current) => markSessionStopped(current, session.id));
2850
- say(`stopped \xB7 ${sessionDisplayName(session)} \u2014 the record and review remain`);
2963
+ say(`stopped \xB7 ${sessionDisplayName(session)} \xB7 the record and review remain`);
2851
2964
  },
2852
2965
  onError: (error) => {
2853
2966
  say(errorText(error));
@@ -2857,14 +2970,15 @@ ${tunneled}
2857
2970
  });
2858
2971
  const launchInWorktreeMutation = useMutation2({
2859
2972
  mutationFn: async (vars) => {
2860
- const argv = HARNESS_COMMANDS[vars.harness];
2861
- if (argv === void 0) throw new Error(`unknown harness "${vars.harness}"`);
2973
+ if (HARNESS_COMMANDS[vars.harness] === void 0) {
2974
+ throw new Error(`unknown harness "${vars.harness}"`);
2975
+ }
2862
2976
  const session = await ctx.api("POST", `/worktrees/${vars.worktreeId}/sessions`, {
2863
2977
  harness: vars.harness,
2864
2978
  label: null
2865
2979
  });
2866
2980
  return startedOrStarting(
2867
- ctx.api("POST", `/sessions/${session.id}/launch`, { argv }),
2981
+ ctx.api("POST", `/sessions/${session.id}/launch`, {}),
2868
2982
  session
2869
2983
  );
2870
2984
  },
@@ -2944,7 +3058,7 @@ ${tunneled}
2944
3058
  refetch();
2945
3059
  },
2946
3060
  onSuccess: (_result, session) => {
2947
- say(`removed \xB7 ${sessionDisplayName(session)} \u2014 the worktree remains`);
3061
+ say(`removed \xB7 ${sessionDisplayName(session)} \xB7 the worktree remains`);
2948
3062
  refetch();
2949
3063
  },
2950
3064
  onSettled: settleRefetch
@@ -2956,7 +3070,7 @@ ${tunneled}
2956
3070
  if (group === null || group.sessions.some((item) => isPendingId(item.session.id))) return;
2957
3071
  if (group.live > 0) {
2958
3072
  say(
2959
- `${group.live} session${group.live === 1 ? "" : "s"} live \u2014 stop them first (\u21E7K) \xB7 ${group.name}`
3073
+ `${group.live} session${group.live === 1 ? "" : "s"} live \xB7 stop them first (\u21E7K) \xB7 ${group.name}`
2960
3074
  );
2961
3075
  return;
2962
3076
  }
@@ -2967,13 +3081,13 @@ ${tunneled}
2967
3081
  }
2968
3082
  setRemoveArmed(`wt:${group.key}`);
2969
3083
  const facts = group.sessions.length === 1 ? "its session and change go with it" : `${group.sessions.length} sessions and the change go with it`;
2970
- say(`press \u21E7D again to remove worktree \xB7 ${group.name} \u2014 ${facts}`);
3084
+ say(`press \u21E7D again to remove worktree \xB7 ${group.name} \xB7 ${facts}`);
2971
3085
  return;
2972
3086
  }
2973
3087
  const session = selectedSession;
2974
3088
  if (session === null || isPendingId(session.id)) return;
2975
3089
  if (AGENT_LIVE_STATUSES.has(session.status)) {
2976
- say(`the agent is still working \u2014 stop it first (\u21E7K) \xB7 ${sessionDisplayName(session)}`);
3090
+ say(`the agent is still working \xB7 stop it first (\u21E7K) \xB7 ${sessionDisplayName(session)}`);
2977
3091
  return;
2978
3092
  }
2979
3093
  if (removeArmed === session.id && confirmationVisible("press \u21E7D again")) {
@@ -2983,7 +3097,7 @@ ${tunneled}
2983
3097
  }
2984
3098
  setRemoveArmed(session.id);
2985
3099
  say(
2986
- `press \u21E7D again to remove session \xB7 ${sessionDisplayName(session)} \u2014 its record goes, the worktree stays`
3100
+ `press \u21E7D again to remove session \xB7 ${sessionDisplayName(session)} \xB7 its record goes, the worktree stays`
2987
3101
  );
2988
3102
  };
2989
3103
  const armStop = () => {
@@ -2992,7 +3106,7 @@ ${tunneled}
2992
3106
  if (group === null) return;
2993
3107
  const live = group.sessions.filter((item) => LIVE_STATUSES.has(item.session.status));
2994
3108
  if (live.length === 0) {
2995
- say("nothing to stop \u2014 the worktree is settled");
3109
+ say("nothing to stop \xB7 the worktree is settled");
2996
3110
  return;
2997
3111
  }
2998
3112
  if (stopArmed === `wt:${group.key}` && confirmationVisible("press \u21E7K again")) {
@@ -3008,7 +3122,7 @@ ${tunneled}
3008
3122
  }
3009
3123
  const session = selectedSession;
3010
3124
  if (session === null || isPendingId(session.id)) return;
3011
- const hold = selectedItem === null ? null : sessionHold(selectedItem);
3125
+ const hold = selectedItem === null ? null : servicesHoldOf(selectedItem);
3012
3126
  if (hold !== null) {
3013
3127
  if (stopArmed === `svc:${session.id}` && confirmationVisible("press \u21E7K again")) {
3014
3128
  setStopArmed(null);
@@ -3016,11 +3130,16 @@ ${tunneled}
3016
3130
  return;
3017
3131
  }
3018
3132
  setStopArmed(`svc:${session.id}`);
3019
- say(`press \u21E7K again to stop the services \xB7 ${sessionDisplayName(session)} \u2014 ${hold}`);
3133
+ say(`press \u21E7K again to stop the services \xB7 ${sessionDisplayName(session)} \xB7 ${hold}`);
3134
+ return;
3135
+ }
3136
+ if (session.status === "stopping") {
3137
+ const saving = selectedItem === null ? null : sessionHold(selectedItem);
3138
+ say(`already stopping${saving === null ? "" : ` \xB7 ${saving}`}`);
3020
3139
  return;
3021
3140
  }
3022
3141
  if (!LIVE_STATUSES.has(session.status)) {
3023
- say("nothing to stop \u2014 the session is settled");
3142
+ say("nothing to stop \xB7 the session is settled");
3024
3143
  return;
3025
3144
  }
3026
3145
  if (stopArmed === session.id && confirmationVisible("press \u21E7K again")) {
@@ -3072,7 +3191,7 @@ ${tunneled}
3072
3191
  if (creatingRef.current?.projectId !== projectId || request !== branchRequest.current)
3073
3192
  return;
3074
3193
  const message = errorText(error);
3075
- say(`could not read branches \u2014 ${message}`);
3194
+ say(`could not read branches \xB7 ${message}`);
3076
3195
  setCreating(
3077
3196
  (current) => current?.projectId === projectId ? { ...current, branches: [], branchError: message } : current
3078
3197
  );
@@ -3107,7 +3226,7 @@ ${tunneled}
3107
3226
  const gateKey = `launch:${current.projectId}:${current.name}`;
3108
3227
  if (!gate.take(gateKey)) {
3109
3228
  setCreating(null);
3110
- say("that worktree is already starting \u2014 wait for it to finish before starting another");
3229
+ say("that worktree is already starting \xB7 wait for it to finish before starting another");
3111
3230
  return;
3112
3231
  }
3113
3232
  setCreating(null);
@@ -3138,11 +3257,11 @@ ${tunneled}
3138
3257
  return;
3139
3258
  case "starting":
3140
3259
  say(
3141
- `still starting \xB7 ${sessionDisplayName(plan.session)} \u2014 a attaches once the row reads running`
3260
+ `still starting \xB7 ${sessionDisplayName(plan.session)} \xB7 a attaches once the row reads running`
3142
3261
  );
3143
3262
  return;
3144
3263
  case "settled":
3145
- say(`settled \xB7 ${sessionDisplayName(plan.session)} \u2014 r resumes it, \u21E7D removes it`);
3264
+ say(`settled \xB7 ${sessionDisplayName(plan.session)} \xB7 r resumes it, \u21E7D removes it`);
3146
3265
  return;
3147
3266
  case "stopping":
3148
3267
  say(
@@ -3150,10 +3269,10 @@ ${tunneled}
3150
3269
  );
3151
3270
  return;
3152
3271
  case "pending":
3153
- say("still provisioning \u2014 the row fills in when the workspace answers");
3272
+ say("still provisioning \xB7 the row fills in when the workspace answers");
3154
3273
  return;
3155
3274
  case "none":
3156
- say("no session selected \u2014 n starts one");
3275
+ say("no session selected \xB7 n starts one");
3157
3276
  return;
3158
3277
  }
3159
3278
  };
@@ -3168,13 +3287,13 @@ ${tunneled}
3168
3287
  openPicker(plan.session);
3169
3288
  return;
3170
3289
  case "live":
3171
- say(`already live \xB7 ${sessionDisplayName(plan.session)} \u2014 a attaches`);
3290
+ say(`already live \xB7 ${sessionDisplayName(plan.session)} \xB7 a attaches`);
3172
3291
  return;
3173
3292
  case "pending":
3174
- say("still provisioning \u2014 nothing to resume yet");
3293
+ say("still provisioning \xB7 nothing to resume yet");
3175
3294
  return;
3176
3295
  case "none":
3177
- say("no session selected \u2014 n starts one");
3296
+ say("no session selected \xB7 n starts one");
3178
3297
  return;
3179
3298
  }
3180
3299
  };
@@ -3399,7 +3518,7 @@ ${tunneled}
3399
3518
  }
3400
3519
  if (key.ctrl === true) return;
3401
3520
  if (focus === "projects" && ["attach", "resume", "stop", "remove", "rename", "openWeb", "review"].includes(verb ?? "")) {
3402
- say("select a session first \u2014 \u2192 opens worktrees");
3521
+ say("select a session first \xB7 \u2192 opens worktrees");
3403
3522
  return;
3404
3523
  }
3405
3524
  switch (verb) {
@@ -3452,7 +3571,7 @@ ${tunneled}
3452
3571
  const session = selectedSession;
3453
3572
  if (session === null) return;
3454
3573
  if (isPendingId(session.id)) {
3455
- say("still provisioning \u2014 nothing to review yet");
3574
+ say("still provisioning \xB7 nothing to review yet");
3456
3575
  return;
3457
3576
  }
3458
3577
  const target = reviewTargetForSession(
@@ -3472,6 +3591,7 @@ ${tunneled}
3472
3591
  }
3473
3592
  });
3474
3593
  const liveTotal = projectItems.reduce((sum, item) => sum + item.live, 0);
3594
+ const stoppingTotal = projectItems.reduce((sum, item) => sum + item.stopping, 0);
3475
3595
  const columnTitle = (column) => {
3476
3596
  if (column === "worktrees") {
3477
3597
  return selectedProject === null ? "worktrees" : `worktrees \xB7 ${worktreeGroups.length}${selectedProject.live > 0 ? ` \xB7 ${selectedProject.live} live` : ""}`;
@@ -3485,7 +3605,7 @@ ${tunneled}
3485
3605
  }
3486
3606
  return COLUMN_TITLE[column];
3487
3607
  };
3488
- const pickerTitle = picker === null ? "" : picker.worktree === void 0 ? picker.session === null ? "new session \u2014 pick a harness" : `resume ${sessionDisplayName(picker.session)} \u2014 pick a harness` : `new session in ${picker.worktree.name} \u2014 pick a harness`;
3608
+ const pickerTitle = picker === null ? "" : picker.worktree === void 0 ? picker.session === null ? "new session \xB7 pick a harness" : `resume ${sessionDisplayName(picker.session)} \xB7 pick a harness` : `new session in ${picker.worktree.name} \xB7 pick a harness`;
3489
3609
  const creatingHeight = 2 + 1 + 1 + 6 + 1 + deriveHarnesses(null).length;
3490
3610
  const footerText = adoptOffer === null ? editing === null ? creating === null ? picker === null ? ` ${fitHints(verbHints(focus), Math.max(12, terminalCols - 2))}` : " \u2191\u2193 move \xB7 enter start \xB7 esc cancel" : creating.step === "name" ? " enter continue \xB7 esc cancel" : creating.step === "base" ? " type to filter \xB7 \u2191\u2193 move \xB7 enter choose base \xB7 esc back" : " \u2191\u2193 move \xB7 enter launch \xB7 esc back" : " enter save \xB7 esc cancel" : " enter adopt \xB7 \u2190\u2192 auth mode \xB7 esc not now";
3491
3611
  const loadFailure = data === void 0 && failureReason !== null ? errorText(failureReason) : null;
@@ -3513,11 +3633,11 @@ ${tunneled}
3513
3633
  const sectionFact = (section) => {
3514
3634
  if (section === "projects") {
3515
3635
  if (selectedProject === null) return "";
3516
- return selectedProject.live > 0 ? `${selectedProject.live} live` : "settled";
3636
+ return liveCountWords(selectedProject.live, selectedProject.stopping);
3517
3637
  }
3518
3638
  if (section === "worktrees") {
3519
3639
  if (selectedGroup === null) return "";
3520
- return selectedGroup.live > 0 ? foldGroupStatus(selectedGroup) : "settled";
3640
+ return foldGroupStatus(selectedGroup);
3521
3641
  }
3522
3642
  return selectedSession?.status ?? "";
3523
3643
  };
@@ -3604,7 +3724,7 @@ ${tunneled}
3604
3724
  },
3605
3725
  group.key
3606
3726
  )),
3607
- data !== void 0 && worktreeGroups.length === 0 ? /* @__PURE__ */ jsx3(EmptyNote, { text: "no worktrees \u2014 w starts one" }) : null
3727
+ data !== void 0 && worktreeGroups.length === 0 ? /* @__PURE__ */ jsx3(EmptyNote, { text: "no worktrees \xB7 w starts one" }) : null
3608
3728
  ]
3609
3729
  }
3610
3730
  )
@@ -3639,10 +3759,10 @@ ${tunneled}
3639
3759
  data !== void 0 && sessionItems.length === 0 ? /* @__PURE__ */ jsx3(
3640
3760
  EmptyNote,
3641
3761
  {
3642
- text: selectedGroup === null ? "no worktree selected" : "no sessions \u2014 n starts one"
3762
+ text: selectedGroup === null ? "no worktree selected" : "no sessions \xB7 n starts one"
3643
3763
  }
3644
3764
  ) : null,
3645
- loadFailure === null ? null : /* @__PURE__ */ jsx3("text", { height: 1, fg: INK_2, bg: "transparent", children: ` ${loadFailure} \u2014 retrying` })
3765
+ loadFailure === null ? null : /* @__PURE__ */ jsx3("text", { height: 1, fg: INK_2, bg: "transparent", children: ` ${loadFailure} \xB7 retrying` })
3646
3766
  ]
3647
3767
  }
3648
3768
  )
@@ -3651,7 +3771,7 @@ ${tunneled}
3651
3771
  );
3652
3772
  }
3653
3773
  };
3654
- const renderDetail = () => /* @__PURE__ */ jsx3(Pane, { title: paneTitle("detail", detailWidth + 2), focused: focus === "detail", grow: true, children: selectedItem === null ? /* @__PURE__ */ jsx3(EmptyNote, { text: "no session selected \u2014 n starts one" }) : /* @__PURE__ */ jsxs3(Fragment3, { children: [
3774
+ const renderDetail = () => /* @__PURE__ */ jsx3(Pane, { title: paneTitle("detail", detailWidth + 2), focused: focus === "detail", grow: true, children: selectedItem === null ? /* @__PURE__ */ jsx3(EmptyNote, { text: "no session selected \xB7 n starts one" }) : /* @__PURE__ */ jsxs3(Fragment3, { children: [
3655
3775
  /* @__PURE__ */ jsx3(
3656
3776
  SessionFacts,
3657
3777
  {
@@ -3666,10 +3786,10 @@ ${tunneled}
3666
3786
  /* @__PURE__ */ jsxs3("text", { height: 1, bg: "transparent", children: [
3667
3787
  /* @__PURE__ */ jsx3("span", { children: " " }),
3668
3788
  /* @__PURE__ */ jsx3("span", { fg: INK_2, children: "starting" }),
3669
- /* @__PURE__ */ jsx3("span", { fg: FAINT, children: " \xB7 the image builds, then the session boots \xB7 a first build on a new setup takes about 7 minutes" })
3789
+ /* @__PURE__ */ jsx3("span", { fg: FAINT, children: ` \xB7 ${selectedSession === null ? "launching" : startingExplanationOf(selectedSession)}` })
3670
3790
  ] }),
3671
3791
  snakeShown ? /* @__PURE__ */ jsx3(SnakeBoard, { handle: snake, focused: focus === "detail" }) : /* @__PURE__ */ jsx3(EmptyNote, { text: "snake is put away \xB7 space brings it back" })
3672
- ] }) : previewSessionId === null ? /* @__PURE__ */ jsx3(EmptyNote, { text: "provisioning \u2014 no record yet" }) : selectedSession?.harness === "shell" ? /* @__PURE__ */ jsx3(EmptyNote, { text: "shell \u2014 no conversation record; a attaches if live" }) : transcript.isPending ? /* @__PURE__ */ jsx3(EmptyNote, { text: "reading the record\u2026" }) : transcript.error === null ? previewView.lines.length === 0 ? /* @__PURE__ */ jsx3(EmptyNote, { text: "no conversation recorded yet" }) : previewView.lines.map((line, index) => /* @__PURE__ */ jsx3(PreviewRow, { line }, `${index}-${line.kind}`)) : /* @__PURE__ */ jsx3("text", { height: 1, fg: INK_2, bg: "transparent", children: ` could not read the record \u2014 ${errorText(transcript.error)}` }) })
3792
+ ] }) : previewSessionId === null ? /* @__PURE__ */ jsx3(EmptyNote, { text: "provisioning \xB7 no record yet" }) : selectedSession?.harness === "shell" ? /* @__PURE__ */ jsx3(EmptyNote, { text: "shell \xB7 no conversation record; a attaches if live" }) : transcript.isPending ? /* @__PURE__ */ jsx3(EmptyNote, { text: "reading the record\u2026" }) : transcript.error === null ? previewView.lines.length === 0 ? /* @__PURE__ */ jsx3(EmptyNote, { text: "no conversation recorded yet" }) : previewView.lines.map((line, index) => /* @__PURE__ */ jsx3(PreviewRow, { line }, `${index}-${line.kind}`)) : /* @__PURE__ */ jsx3("text", { height: 1, fg: INK_2, bg: "transparent", children: ` could not read the record \xB7 ${errorText(transcript.error)}` }) })
3673
3793
  ] }) });
3674
3794
  const offNav = layout.offscreen.filter(isNavSection);
3675
3795
  const detailOffscreen = layout.offscreen.includes("detail");
@@ -3688,6 +3808,13 @@ ${tunneled}
3688
3808
  liveTotal,
3689
3809
  " live"
3690
3810
  ] }),
3811
+ stoppingTotal > 0 ? /* @__PURE__ */ jsxs3(Fragment3, { children: [
3812
+ /* @__PURE__ */ jsx3("span", { fg: FAINT, children: " \xB7 " }),
3813
+ /* @__PURE__ */ jsxs3("span", { fg: INK_2, children: [
3814
+ stoppingTotal,
3815
+ " stopping"
3816
+ ] })
3817
+ ] }) : null,
3691
3818
  gate.count() > 0 ? /* @__PURE__ */ jsxs3(Fragment3, { children: [
3692
3819
  /* @__PURE__ */ jsx3("span", { fg: FAINT, children: " \xB7 " }),
3693
3820
  /* @__PURE__ */ jsxs3("span", { fg: INK_2, children: [
@@ -3784,7 +3911,7 @@ ${tunneled}
3784
3911
  border: true,
3785
3912
  borderStyle: "rounded",
3786
3913
  borderColor: ACCENT,
3787
- title: ` label \u2014 ${editing.harness} ${editing.id.slice(0, 8)} `,
3914
+ title: ` label \xB7 ${editing.harness} ${editing.id.slice(0, 8)} `,
3788
3915
  titleAlignment: "left",
3789
3916
  backgroundColor: SURFACE,
3790
3917
  height: 3,
@@ -3856,7 +3983,7 @@ ${tunneled}
3856
3983
  border: true,
3857
3984
  borderStyle: "rounded",
3858
3985
  borderColor: ACCENT,
3859
- title: ` new worktree \u2014 ${selectedProject?.project.name ?? "project"} `,
3986
+ title: ` new worktree \xB7 ${selectedProject?.project.name ?? "project"} `,
3860
3987
  titleAlignment: "left",
3861
3988
  backgroundColor: SURFACE,
3862
3989
  flexDirection: "column",
@@ -3904,7 +4031,7 @@ ${tunneled}
3904
4031
  {
3905
4032
  focused: true,
3906
4033
  value: "",
3907
- placeholder: "type to filter (enter = highlighted; empty = default)",
4034
+ placeholder: "type to filter \xB7 enter takes the highlighted branch, or the default when empty",
3908
4035
  backgroundColor: SURFACE,
3909
4036
  focusedBackgroundColor: SURFACE,
3910
4037
  textColor: INK,
@@ -3950,7 +4077,7 @@ ${tunneled}
3950
4077
  /* @__PURE__ */ jsx3(Gutter, { selected: highlighted }),
3951
4078
  /* @__PURE__ */ jsx3("span", { fg: active ? INK : FAINT, children: branch.name.slice(0, 40).padEnd(41) }),
3952
4079
  /* @__PURE__ */ jsx3("span", { fg: FAINT, children: branch.sha.slice(0, 8).padEnd(10) }),
3953
- /* @__PURE__ */ jsx3("span", { fg: active ? MUTED : FAINT, children: shortAge(branch.committedAt).padEnd(6) }),
4080
+ /* @__PURE__ */ jsx3("span", { fg: active ? MUTED : FAINT, children: timeAgo(branch.committedAt).padEnd(9) }),
3954
4081
  branch.isDefault ? /* @__PURE__ */ jsx3("span", { fg: FAINT, children: "default" }) : null
3955
4082
  ] })
3956
4083
  },
@@ -3983,7 +4110,7 @@ ${tunneled}
3983
4110
  ]
3984
4111
  }
3985
4112
  ),
3986
- loadFailure === null ? /* @__PURE__ */ jsx3(StatusLine, { busy, busyStarted, status }) : /* @__PURE__ */ jsx3("text", { height: 1, fg: ERROR, bg: "transparent", children: ` could not load workbench \u2014 ${loadFailure} \xB7 retrying` }),
4113
+ loadFailure === null ? /* @__PURE__ */ jsx3(StatusLine, { busy, busyStarted, status }) : /* @__PURE__ */ jsx3("text", { height: 1, fg: ERROR, bg: "transparent", children: ` could not load workbench \xB7 ${loadFailure} \xB7 retrying` }),
3987
4114
  /* @__PURE__ */ jsx3("text", { height: 1, fg: FAINT, bg: "transparent", children: footerText })
3988
4115
  ] });
3989
4116
  };