@paradow/ledge 0.40.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -116,6 +116,12 @@ function backendOf(s) {
116
116
  function isArchived(s) {
117
117
  return s.archivedAt !== void 0;
118
118
  }
119
+ function isOmpSeat(s) {
120
+ return s.source === "rig" && s.tool === "omp" && s.backend === void 0;
121
+ }
122
+ function isStoppedOmpSeat(s) {
123
+ return isOmpSeat(s) && s.state === "exited" && !isArchived(s);
124
+ }
119
125
  var ToolSchema, OmpApprovalSchema, BackendSchema, SessionStateSchema, AskQuestionSchema, PendingSchema, ReplySchema, HelperSchema, TopicSourceSchema, SessionSchema;
120
126
  var init_session = __esm({
121
127
  "../protocol/src/session.ts"() {
@@ -359,11 +365,15 @@ function pendingKey(p) {
359
365
  const s = JSON.stringify([p.kind, p.text, p.body ?? null, p.tool ?? null, p.options ?? null, p.optionDetails ?? null, p.helperId ?? null]);
360
366
  return cyrb53(s, 0) + cyrb53(s, 1);
361
367
  }
362
- var DecisionSchema, NOTE_MAX, NOTE_NOT_TYPED, DECISION_OFFERS;
368
+ var DecisionSchema, DecidedBySchema, NOTE_MAX, NOTE_NOT_TYPED, DECISION_OFFERS;
363
369
  var init_decisions = __esm({
364
370
  "../protocol/src/decisions.ts"() {
365
371
  "use strict";
366
372
  DecisionSchema = z6.enum(["accept", "deny", "always"]);
373
+ DecidedBySchema = z6.discriminatedUnion("kind", [
374
+ z6.object({ kind: z6.literal("pc") }),
375
+ z6.object({ kind: z6.literal("device"), id: z6.string().min(1).max(100), name: z6.string().min(1).max(60) })
376
+ ]);
367
377
  NOTE_MAX = 4e3;
368
378
  NOTE_NOT_TYPED = "note_not_typed";
369
379
  DECISION_OFFERS = {
@@ -2050,6 +2060,7 @@ var init_thread = __esm({
2050
2060
  init_tasks();
2051
2061
  init_attachments();
2052
2062
  init_rigMessages();
2063
+ init_decisions();
2053
2064
  ToolStatusSchema = z22.enum(["running", "success", "failed", "unknown"]);
2054
2065
  FileChangeSchema = z22.object({ path: z22.string().min(1), from: z22.string().min(1).optional(), change: z22.enum(["add", "edit", "delete", "write"]), diff: z22.string().optional() });
2055
2066
  base = { id: z22.string().min(1), at: z22.string().datetime(), turn: z22.number().int().nonnegative(), helperId: z22.string().optional() };
@@ -2075,8 +2086,8 @@ var init_thread = __esm({
2075
2086
  imagesDropped: z22.literal(true).optional()
2076
2087
  }),
2077
2088
  /** `unknown`: answered in the terminal, the daemon only saw the prompt go away; `always`: allowed and not asked again;
2078
- * `byClose`: denied by Ledge closing the session. */
2079
- z22.object({ kind: z22.literal("permission"), ...base, tool: z22.string(), text: z22.string(), decision: z22.enum(["accept", "deny", "always", "unknown"]).optional(), byClose: z22.literal(true).optional() }),
2089
+ * `byClose`: denied by Ledge closing the session; `by`: who answered it from Ledge (the PC or a device, its name then). */
2090
+ z22.object({ kind: z22.literal("permission"), ...base, tool: z22.string(), text: z22.string(), decision: z22.enum(["accept", "deny", "always", "unknown"]).optional(), byClose: z22.literal(true).optional(), by: DecidedBySchema.optional() }),
2080
2091
  z22.object({ kind: z22.literal("reply"), ...base, text: z22.string() }),
2081
2092
  z22.object({ kind: z22.literal("helper"), ...base, task: z22.string(), agentType: z22.string().optional(), state: z22.enum(["working", "exited"]) }),
2082
2093
  /** A message this session sent to another agent; `id` is the tool_use_id, `to` the address it used, `peer` the Ledge session id, `received` the paired item's id in the peer's thread, `via` set when Ledge's mailbox carries it. */
@@ -2135,7 +2146,10 @@ var init_thread = __esm({
2135
2146
 
2136
2147
  // ../protocol/src/mobile.ts
2137
2148
  import { z as z23 } from "zod";
2138
- var DEFAULT_RELAY_URL, RelayUrlSchema, MobileConfigSchema, MobileLinkStateSchema, MobilePairingSchema, MobileStateSchema;
2149
+ function deviceKindOfLabel(label2) {
2150
+ return /\b(iPad|tablet|tablette)\b/i.test(label2) ? "tablet" : "phone";
2151
+ }
2152
+ var DEFAULT_RELAY_URL, RelayUrlSchema, MobileConfigSchema, MobileLinkStateSchema, DeviceKindSchema, DEVICE_NAME_MAX, DeviceNameSchema, MobilePairingSchema, MobileStateSchema;
2139
2153
  var init_mobile = __esm({
2140
2154
  "../protocol/src/mobile.ts"() {
2141
2155
  "use strict";
@@ -2166,6 +2180,9 @@ var init_mobile = __esm({
2166
2180
  /** The mobile service did not start (a broken mobile.json, a missing key store): nothing is linked until the daemon restarts. */
2167
2181
  z23.object({ state: z23.literal("unavailable"), error: z23.string().max(200) })
2168
2182
  ]);
2183
+ DeviceKindSchema = z23.enum(["phone", "tablet"]);
2184
+ DEVICE_NAME_MAX = 40;
2185
+ DeviceNameSchema = z23.string().trim().min(1).max(DEVICE_NAME_MAX).regex(/^[^\u0000-\u001f\u007f]+$/);
2169
2186
  MobilePairingSchema = z23.object({
2170
2187
  id: z23.string(),
2171
2188
  label: z23.string(),
@@ -2181,7 +2198,16 @@ var init_mobile = __esm({
2181
2198
  /** The last sign-in that did not end with a session; cleared by the next one. */
2182
2199
  signinError: z23.enum(["expired", "denied", "not_allowed", "failed"]).nullable(),
2183
2200
  link: MobileLinkStateSchema,
2184
- devices: z23.array(z23.object({ id: z23.string(), label: z23.string(), approvedAt: z23.string(), online: z23.boolean(), lastSeenAt: z23.string().optional() })),
2201
+ devices: z23.array(z23.object({
2202
+ id: z23.string(),
2203
+ label: z23.string(),
2204
+ approvedAt: z23.string(),
2205
+ online: z23.boolean(),
2206
+ lastSeenAt: z23.string().optional(),
2207
+ /** Absent from an older daemon; `name` absent: the user gave none, the front derives one from the label. */
2208
+ kind: DeviceKindSchema.optional(),
2209
+ name: z23.string().optional()
2210
+ })),
2185
2211
  pairings: z23.array(MobilePairingSchema),
2186
2212
  /** « Taper dans les terminaux depuis le téléphone »: absent from an older daemon, which has no phone terminal. */
2187
2213
  terminalInput: z23.boolean().optional()
@@ -2273,6 +2299,8 @@ var init_messages = __esm({
2273
2299
  decision: DecisionSchema.optional(),
2274
2300
  /** Decided: the request was refused by Ledge closing the session (its interrupt keys), not by the user's answer. */
2275
2301
  byClose: z24.literal(true).optional(),
2302
+ /** Decided: who answered, when the daemon typed it (absent: answered in the terminal, or an older daemon). */
2303
+ by: DecidedBySchema.optional(),
2276
2304
  /** The agent's own session id when the hook names it, so the daemon can resume that conversation. */
2277
2305
  agentSessionId: z24.string().optional()
2278
2306
  });
@@ -2549,7 +2577,9 @@ var init_messages = __esm({
2549
2577
  note: z24.string().trim().min(1).max(NOTE_MAX).optional(),
2550
2578
  /** The request the user saw when deciding: refused (the request changed) unless it is still the pending one. Left out by older fronts and the island.
2551
2579
  * `key` (pendingKey) covers its whole content; older fronts send kind and text only. */
2552
- expect: PendingSchema.pick({ kind: true, text: true }).extend({ key: z24.string().max(64).optional() }).optional()
2580
+ expect: PendingSchema.pick({ kind: true, text: true }).extend({ key: z24.string().max(64).optional() }).optional(),
2581
+ /** Who decides: set by the phone gateway for a device (over anything the device sent); left out by a local front, which means the PC. */
2582
+ by: DecidedBySchema.optional()
2553
2583
  }),
2554
2584
  z24.object({ type: z24.literal("session.focus"), id: z24.string() }),
2555
2585
  z24.object({ type: z24.literal("session.kill"), id: z24.string() }),
@@ -2603,10 +2633,13 @@ var init_messages = __esm({
2603
2633
  z24.object({ type: z24.literal("mobile.signout") }),
2604
2634
  /** While the link is offline: try again now. */
2605
2635
  z24.object({ type: z24.literal("mobile.retry") }),
2606
- z24.object({ type: z24.literal("mobile.pairing.approve"), id: z24.string().min(1).max(100) }),
2636
+ /** `replaces`: an approved device this one takes the place of (the same app reinstalled), revoked once this one is stored. */
2637
+ z24.object({ type: z24.literal("mobile.pairing.approve"), id: z24.string().min(1).max(100), replaces: z24.string().min(1).max(100).optional() }),
2607
2638
  z24.object({ type: z24.literal("mobile.pairing.deny"), id: z24.string().min(1).max(100) }),
2608
2639
  /** `id`: the device's fingerprint. */
2609
2640
  z24.object({ type: z24.literal("mobile.device.revoke"), id: z24.string().min(1).max(100) }),
2641
+ /** Names a device (`id`: its fingerprint); the device sees its new name in its next snapshot. */
2642
+ z24.object({ type: z24.literal("mobile.device.rename"), id: z24.string().min(1).max(100), name: DeviceNameSchema }),
2610
2643
  /** « Taper dans les terminaux depuis le téléphone »; turned off, every phone loses the hand. */
2611
2644
  z24.object({ type: z24.literal("mobile.terminal"), input: z24.boolean() }),
2612
2645
  /** « Reprendre la main »: the phone typing into the session's terminal goes back to reading it. */
@@ -2863,7 +2896,9 @@ var init_push = __esm({
2863
2896
  });
2864
2897
  PushPayloadSchema = z27.object({
2865
2898
  v: z27.literal(1),
2866
- kind: z27.enum(["request", "question", "finished", "chat", "decision"]),
2899
+ /** `resolved`: a request or question answered (anywhere), replacing its notification under the same tag; only sent to a
2900
+ * device whose subscription says its service worker knows it (`push.subscribe` `resolved`). */
2901
+ kind: z27.enum(["request", "question", "finished", "chat", "decision", "resolved"]),
2867
2902
  title: z27.string().max(80),
2868
2903
  body: z27.string().max(120),
2869
2904
  url: z27.string().regex(/^\/#\/[A-Za-z0-9/%._~-]*$/).max(300),
@@ -2896,6 +2931,7 @@ var init_phone = __esm({
2896
2931
  init_messages();
2897
2932
  init_broadcast();
2898
2933
  init_push();
2934
+ init_mobile();
2899
2935
  init_models();
2900
2936
  init_commands();
2901
2937
  init_openrig();
@@ -3167,8 +3203,11 @@ var init_phone = __esm({
3167
3203
  z28.object({ type: z28.literal("openrig.row"), qitem: QitemIdSchema }),
3168
3204
  /** The phone drops its passkey from the daemon. */
3169
3205
  z28.object({ type: z28.literal("device.forget"), op: OperationIdSchema }),
3170
- /** The phone's push subscription, with the VAPID key it was made with (so a key change is seen). */
3171
- z28.object({ type: z28.literal("push.subscribe"), op: OperationIdSchema, subscription: PushSubscriptionSchema, key: B64U2.length(87) }),
3206
+ /** The device names itself (on the PC and in its next snapshot); applied to the sender only. */
3207
+ z28.object({ type: z28.literal("device.rename"), op: OperationIdSchema, name: DeviceNameSchema }),
3208
+ /** The phone's push subscription, with the VAPID key it was made with (so a key change is seen). `resolved`: its service
3209
+ * worker shows a `resolved` push, which replaces a request's notification once it is answered elsewhere. */
3210
+ z28.object({ type: z28.literal("push.subscribe"), op: OperationIdSchema, subscription: PushSubscriptionSchema, key: B64U2.length(87), resolved: z28.literal(true).optional() }),
3172
3211
  z28.object({ type: z28.literal("push.unsubscribe"), op: OperationIdSchema }),
3173
3212
  /** The phone's own chat with Ledge. */
3174
3213
  z28.object({ type: z28.literal("chat.ask"), qid: OperationIdSchema, text: z28.string().trim().max(MESSAGE_MAX), attachments: z28.array(AttachmentIdSchema).max(ATTACHMENTS_MAX).optional() }),
@@ -3260,7 +3299,16 @@ var init_phone = __esm({
3260
3299
  openrig: z28.literal(true).optional(),
3261
3300
  /** The daemon takes `session.broadcast` (« Écrire au groupe »). */
3262
3301
  broadcast: z28.literal(true).optional()
3263
- }), device: z28.object({ confirm: z28.boolean(), push: z28.enum(["on", "off", "stale", "unavailable"]).optional(), chat: z28.literal(true).optional(), contextNear: ContextPrefsSchema.shape.near.optional() }).optional() }),
3302
+ }), device: z28.object({
3303
+ confirm: z28.boolean(),
3304
+ push: z28.enum(["on", "off", "stale", "unavailable"]).optional(),
3305
+ chat: z28.literal(true).optional(),
3306
+ contextNear: ContextPrefsSchema.shape.near.optional(),
3307
+ /** This device's id (its fingerprint, as decisions name it in `by`), its kind, and the name the user gave it (absent: none given). */
3308
+ id: z28.string().optional(),
3309
+ kind: DeviceKindSchema.optional(),
3310
+ name: z28.string().optional()
3311
+ }).optional() }),
3264
3312
  /** « À toi » on this PC: the answer to `openrig.asks`, then each change, whole. */
3265
3313
  z28.object({ type: z28.literal("openrig.asks"), channel: HumanChannelSchema, asks: z28.array(HumanAskSchema), partial: z28.literal(true).optional(), truncated: z28.literal(true).optional() }),
3266
3314
  /** An answer's end, as on the desktop (`openrig.answered`), under the phone's `op`. `rejected`: the device check refused it (`reason`). */
@@ -3788,11 +3836,11 @@ function stripNote(text7) {
3788
3836
  }
3789
3837
  function formatLedgeMessage(m) {
3790
3838
  const from = attr(m.from);
3791
- const open5 = `<ledge-message from="${from}" tool="${m.tool}"${m.id ? ` id="${attr(m.id)}"` : ""}>${messageNote(from, m.to)} `;
3792
- if (!m.id || inline(m.text)) return `${open5}${ledgeBody(m.text)}${LEDGE_CLOSE}`;
3839
+ const open6 = `<ledge-message from="${from}" tool="${m.tool}"${m.id ? ` id="${attr(m.id)}"` : ""}>${messageNote(from, m.to)} `;
3840
+ if (!m.id || inline(m.text)) return `${open6}${ledgeBody(m.text)}${LEDGE_CLOSE}`;
3793
3841
  const first = m.summary?.trim() || m.text.trim().split(/\r\n|\r|\n/)[0].trim();
3794
3842
  const head2 = ledgeBody(first.length > POINTER_HEAD ? `${first.slice(0, POINTER_HEAD)}\u2026` : first);
3795
- return `${open5}${head2} \u2026 [message de ${m.text.length} caract\xE8res, ${lineCount(m.text)} lignes : lis-le en entier avec ledge read ${attr(m.id)}]${LEDGE_CLOSE}`;
3843
+ return `${open6}${head2} \u2026 [message de ${m.text.length} caract\xE8res, ${lineCount(m.text)} lignes : lis-le en entier avec ledge read ${attr(m.id)}]${LEDGE_CLOSE}`;
3796
3844
  }
3797
3845
  function ledgeMessages(text7) {
3798
3846
  const out = [];
@@ -4947,7 +4995,7 @@ __export(who_exports, {
4947
4995
  });
4948
4996
  import { basename as basename5 } from "path";
4949
4997
  function formatWho(sessions, self, projects) {
4950
- const live3 = sessions.filter((s) => s.state !== "exited" && !isArchived(s));
4998
+ const live3 = sessions.filter((s) => s.state !== "exited" && !isArchived(s) || isStoppedOmpSeat(s));
4951
4999
  if (live3.length === 0) return "aucune autre session";
4952
5000
  const rows = live3.map((s) => [
4953
5001
  s.id === self ? "*" : " ",
@@ -5195,8 +5243,8 @@ function underCodexDaemon(read = (p) => readFileSync5(p, "utf8")) {
5195
5243
  let pid = process.pid;
5196
5244
  for (let depth = 0; depth < 8; depth++) {
5197
5245
  try {
5198
- const stat7 = read(`/proc/${pid}/stat`);
5199
- pid = Number(stat7.slice(stat7.lastIndexOf(")") + 2).split(" ")[1]);
5246
+ const stat8 = read(`/proc/${pid}/stat`);
5247
+ pid = Number(stat8.slice(stat8.lastIndexOf(")") + 2).split(" ")[1]);
5200
5248
  if (!pid || pid <= 1) return false;
5201
5249
  if (read(`/proc/${pid}/cmdline`).split("\0").includes("app-server")) return true;
5202
5250
  } catch {
@@ -6557,7 +6605,7 @@ var init_history = __esm({
6557
6605
  return q ? this.put(id, { ...q, status: "unknown", byClose: true }) : void 0;
6558
6606
  }
6559
6607
  const perm = [...this.items(id)].reverse().find((i) => i.kind === "permission" && i.decision === void 0);
6560
- return perm && perm.kind === "permission" ? this.put(id, { ...perm, decision: ev.decision, ...ev.byClose ? { byClose: true } : {} }) : void 0;
6608
+ return perm && perm.kind === "permission" ? this.put(id, { ...perm, decision: ev.decision, ...ev.byClose ? { byClose: true } : {}, ...ev.by ? { by: ev.by } : {} }) : void 0;
6561
6609
  }
6562
6610
  case "AgentMessage": {
6563
6611
  if (!ev.messageId || !ev.text) return void 0;
@@ -6764,9 +6812,9 @@ ${done.output}` : command;
6764
6812
  const peer = (m.sender ? this.peerOf(m.sender) : void 0) ?? this.peerOf(m.from) ?? (m.name ? this.peerOf(m.name) : void 0);
6765
6813
  if (!peer) return this.put(id, recv);
6766
6814
  const head2 = (t) => ledgeBody(t).trim().slice(0, 200);
6767
- const open5 = this.items(peer).filter((i) => i.kind === "sent" && i.peer === id && i.status !== "read");
6768
- const byId2 = m.id ? open5.find((i) => mailId(i.id) === m.id) : void 0;
6769
- const sent = byId2 ?? [...open5].reverse().find((i) => head2(i.text) === head2(recv.text)) ?? open5.at(-1);
6815
+ const open6 = this.items(peer).filter((i) => i.kind === "sent" && i.peer === id && i.status !== "read");
6816
+ const byId2 = m.id ? open6.find((i) => mailId(i.id) === m.id) : void 0;
6817
+ const sent = byId2 ?? [...open6].reverse().find((i) => head2(i.text) === head2(recv.text)) ?? open6.at(-1);
6770
6818
  if (sent) this.put(peer, { ...sent, status: "read", received: recv.id });
6771
6819
  return this.put(id, { ...recv, ...byId2 ? { text: byId2.text } : {}, peer, ...sent ? { sent: sent.id } : {} });
6772
6820
  }
@@ -7838,6 +7886,203 @@ var init_verify = __esm({
7838
7886
  }
7839
7887
  });
7840
7888
 
7889
+ // src/daemon/omp/seats.ts
7890
+ import { homedir as homedir5 } from "os";
7891
+ import { join as join10 } from "path";
7892
+ import { open, readFile as readFile7, stat as stat2 } from "fs/promises";
7893
+ function entryState(raw) {
7894
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
7895
+ const e = raw;
7896
+ const at = typeof e.timestamp === "string" && !Number.isNaN(Date.parse(e.timestamp)) ? new Date(e.timestamp).toISOString() : void 0;
7897
+ const of = (state2) => ({ state: state2, ...at ? { at } : {} });
7898
+ if (e.type === "custom_message") return of("working");
7899
+ if (e.type === "custom") return e.customType === "tool_execution_start" ? of("working") : null;
7900
+ if (e.type !== "message" || !e.message || typeof e.message !== "object") return null;
7901
+ const m = e.message;
7902
+ if (m.role === "user" || m.role === "toolResult") return of("working");
7903
+ if (m.role !== "assistant") return null;
7904
+ return typeof m.stopReason === "string" ? of(m.stopReason === "toolUse" ? "working" : "idle") : null;
7905
+ }
7906
+ async function nextNewline(fh, from, size) {
7907
+ const buf2 = Buffer.alloc(BLOCK3);
7908
+ for (let at = from; at < size; ) {
7909
+ const { bytesRead } = await fh.read(buf2, 0, Math.min(BLOCK3, size - at), at);
7910
+ if (bytesRead === 0) return -1;
7911
+ const nl = buf2.subarray(0, bytesRead).indexOf(10);
7912
+ if (nl !== -1) return at + nl;
7913
+ at += bytesRead;
7914
+ }
7915
+ return -1;
7916
+ }
7917
+ function createOmpSeats(deps) {
7918
+ const { store } = deps;
7919
+ const root2 = deps.root ?? ompSeatsRoot();
7920
+ const follows = /* @__PURE__ */ new Map();
7921
+ let running = false, timer2 = null;
7922
+ async function journalOf(seat2) {
7923
+ try {
7924
+ const file = JSON.parse(await readFile7(join10(root2, seat2, "runner-state.json"), "utf8")).sessionFile;
7925
+ return typeof file === "string" && file ? file : null;
7926
+ } catch {
7927
+ return null;
7928
+ }
7929
+ }
7930
+ async function followOne(s) {
7931
+ const file = await journalOf(s.rig.seat);
7932
+ if (!file) return;
7933
+ let size;
7934
+ try {
7935
+ size = (await stat2(file)).size;
7936
+ } catch {
7937
+ return;
7938
+ }
7939
+ let f = follows.get(s.id);
7940
+ if (!f || f.file !== file) {
7941
+ const known2 = await lastState(file, size);
7942
+ f = { file, offset: known2.end };
7943
+ follows.set(s.id, f);
7944
+ if (known2.state) apply(s.id, known2.state);
7945
+ return;
7946
+ }
7947
+ if (size < f.offset) {
7948
+ f.offset = size;
7949
+ return;
7950
+ }
7951
+ if (size === f.offset) return;
7952
+ const fh = await open(file, "r");
7953
+ let chunk, end2;
7954
+ try {
7955
+ const readAt2 = async (want) => {
7956
+ const b3 = Buffer.alloc(want);
7957
+ return b3.subarray(0, (await fh.read(b3, 0, want, f.offset)).bytesRead);
7958
+ };
7959
+ chunk = await readAt2(Math.min(size - f.offset, READ_BYTES));
7960
+ end2 = chunk.lastIndexOf(10);
7961
+ if (end2 === -1 && chunk.length < size - f.offset) {
7962
+ chunk = await readAt2(Math.min(size - f.offset, BACK_BYTES));
7963
+ end2 = chunk.lastIndexOf(10);
7964
+ }
7965
+ if (end2 === -1 && chunk.length === BACK_BYTES) {
7966
+ const next4 = await nextNewline(fh, f.offset + chunk.length, size);
7967
+ if (next4 !== -1) {
7968
+ log(`omp seats: an entry of ${s.rig.seat} over ${BACK_BYTES} bytes was skipped`);
7969
+ f.offset = next4 + 1;
7970
+ }
7971
+ return;
7972
+ }
7973
+ } finally {
7974
+ await fh.close();
7975
+ }
7976
+ if (end2 === -1) return;
7977
+ const lines = chunk.subarray(0, end2).toString("utf8").split("\n");
7978
+ f.offset += end2 + 1;
7979
+ let last = null;
7980
+ for (const line2 of lines) {
7981
+ if (!line2.trim()) continue;
7982
+ let entry2;
7983
+ try {
7984
+ entry2 = JSON.parse(line2);
7985
+ } catch {
7986
+ continue;
7987
+ }
7988
+ last = entryState(entry2) ?? last;
7989
+ }
7990
+ if (last) apply(s.id, last);
7991
+ }
7992
+ function apply(id, last) {
7993
+ const cur = store.get(id);
7994
+ if (!cur || cur.state === "exited") return;
7995
+ const at = last.at ?? (/* @__PURE__ */ new Date()).toISOString();
7996
+ if (cur.state !== last.state || at > cur.lastActivityAt) store.upsert({ ...cur, state: last.state, lastActivityAt: at > cur.lastActivityAt ? at : cur.lastActivityAt });
7997
+ }
7998
+ async function lastState(file, size) {
7999
+ const fh = await open(file, "r");
8000
+ try {
8001
+ const newlineBefore = async (before2) => {
8002
+ const buf2 = Buffer.alloc(BLOCK3);
8003
+ for (let to = before2; to > 0; ) {
8004
+ if (size - to > BACK_BYTES) return null;
8005
+ const from = Math.max(0, to - BLOCK3);
8006
+ const { bytesRead } = await fh.read(buf2, 0, to - from, from);
8007
+ const at = buf2.subarray(0, bytesRead).lastIndexOf(10);
8008
+ if (at !== -1) return from + at;
8009
+ to = from;
8010
+ }
8011
+ return -1;
8012
+ };
8013
+ const last = await newlineBefore(size);
8014
+ if (last === null || last === -1) return { state: null, end: last === -1 ? 0 : size };
8015
+ const end2 = last + 1;
8016
+ for (let lineEnd = last; lineEnd > 0; ) {
8017
+ const start = await newlineBefore(lineEnd);
8018
+ if (start === null) break;
8019
+ const from = start + 1;
8020
+ const line2 = Buffer.alloc(lineEnd - from);
8021
+ await fh.read(line2, 0, line2.length, from);
8022
+ let entry2 = null;
8023
+ try {
8024
+ entry2 = JSON.parse(line2.toString("utf8"));
8025
+ } catch {
8026
+ }
8027
+ const state2 = entryState(entry2);
8028
+ if (state2) return { state: state2, end: end2 };
8029
+ if (start === -1) break;
8030
+ lineEnd = start;
8031
+ }
8032
+ return { state: null, end: end2 };
8033
+ } finally {
8034
+ await fh.close();
8035
+ }
8036
+ }
8037
+ async function tick() {
8038
+ if (running) return;
8039
+ running = true;
8040
+ try {
8041
+ const live3 = /* @__PURE__ */ new Set();
8042
+ for (const s of store.all()) {
8043
+ if (!isOmpSeat2(s) || !s.rig || s.state === "exited" || isArchived(s)) continue;
8044
+ live3.add(s.id);
8045
+ try {
8046
+ await followOne(s);
8047
+ } catch (e) {
8048
+ log(`omp seats: journal of ${s.rig.seat} not read: ${e.message}`);
8049
+ }
8050
+ }
8051
+ for (const id of follows.keys()) if (!live3.has(id)) follows.delete(id);
8052
+ } finally {
8053
+ running = false;
8054
+ }
8055
+ }
8056
+ return {
8057
+ tick,
8058
+ start() {
8059
+ if (!timer2) {
8060
+ timer2 = setInterval(() => {
8061
+ void tick();
8062
+ }, deps.intervalMs ?? 1e3);
8063
+ timer2.unref?.();
8064
+ }
8065
+ },
8066
+ stop() {
8067
+ if (timer2) clearInterval(timer2);
8068
+ timer2 = null;
8069
+ }
8070
+ };
8071
+ }
8072
+ var ompSeatsRoot, BACK_BYTES, BLOCK3, READ_BYTES, isOmpSeat2;
8073
+ var init_seats = __esm({
8074
+ "src/daemon/omp/seats.ts"() {
8075
+ "use strict";
8076
+ init_src();
8077
+ init_log();
8078
+ ompSeatsRoot = (env = process.env) => join10(env.OPENRIG_HOME?.trim() || join10(homedir5(), ".openrig"), "state", "omp");
8079
+ BACK_BYTES = 16 * 1024 * 1024;
8080
+ BLOCK3 = 64 * 1024;
8081
+ READ_BYTES = 4 * 1024 * 1024;
8082
+ isOmpSeat2 = (s) => s.source === "rig" && s.tool === "omp" && s.backend === void 0;
8083
+ }
8084
+ });
8085
+
7841
8086
  // src/daemon/poller.ts
7842
8087
  import { createHash as createHash4 } from "crypto";
7843
8088
  function startPoller(opts) {
@@ -7902,7 +8147,7 @@ function startPoller(opts) {
7902
8147
  }
7903
8148
  if (s.state === "exited") {
7904
8149
  forget(s.id);
7905
- if (!s.archivedAt && s.exitedAt) {
8150
+ if (!s.archivedAt && s.exitedAt && !isOmpSeat2(s)) {
7906
8151
  const since = Math.max(Date.parse(s.exitedAt), s.restoredAt ? Date.parse(s.restoredAt) : -Infinity);
7907
8152
  if (t - since > retireMs) store.archive(s.id, now2());
7908
8153
  }
@@ -7919,6 +8164,7 @@ function startPoller(opts) {
7919
8164
  forget(s.id);
7920
8165
  return;
7921
8166
  }
8167
+ if (isOmpSeat2(s)) return;
7922
8168
  const full = await tmux.capture(s.tmuxSession, PREVIEW_CAPTURE_LINES);
7923
8169
  if (full === null) return;
7924
8170
  const pane = full.split("\n").slice(-DETECT_LINES).join("\n");
@@ -7983,6 +8229,7 @@ var init_poller = __esm({
7983
8229
  init_log();
7984
8230
  init_git();
7985
8231
  init_transcript();
8232
+ init_seats();
7986
8233
  init_adapters();
7987
8234
  DETECT_LINES = 60;
7988
8235
  REST_MS = 2e3;
@@ -8528,7 +8775,7 @@ var init_project = __esm({
8528
8775
 
8529
8776
  // src/daemon/omp/host.ts
8530
8777
  import { randomBytes as randomBytes4 } from "crypto";
8531
- import { basename as basename7, join as join10 } from "path";
8778
+ import { basename as basename7, join as join11 } from "path";
8532
8779
  import { mkdir as mkdir7 } from "fs/promises";
8533
8780
  function ompArgs(o) {
8534
8781
  return ["--mode", "rpc-ui", "--session-dir", o.dir, "--cwd", o.cwd, "--approval-mode", o.approval ?? APPROVAL_MODE, ...ompModelArgs(o.model, o.effort), ...(o.dirs ?? []).flatMap((d) => ["--add-dir", d]), ...o.resumeFile ? ["--resume", o.resumeFile] : []];
@@ -8540,7 +8787,7 @@ function createOmpHost(deps) {
8540
8787
  const live3 = /* @__PURE__ */ new Map();
8541
8788
  let generations = 0;
8542
8789
  async function startProcess(id, o) {
8543
- const dir = join10(deps.dir, id);
8790
+ const dir = join11(deps.dir, id);
8544
8791
  await mkdir7(dir, { recursive: true });
8545
8792
  if (await reapOwner(dir)) log(`omp ${id}: ended the process a previous daemon left`);
8546
8793
  const key = sessionKey(id);
@@ -8720,15 +8967,15 @@ function createOmpHost(deps) {
8720
8967
  async answer(id, text7) {
8721
8968
  const l = live3.get(id);
8722
8969
  if (!l) return "the session is not running";
8723
- const open5 = l.tracking.requests[0];
8724
- if (!open5) return "nothing to answer";
8725
- const a = answerFor(open5, text7);
8970
+ const open6 = l.tracking.requests[0];
8971
+ if (!open6) return "nothing to answer";
8972
+ const a = answerFor(open6, text7);
8726
8973
  if ("refused" in a) return a.refused;
8727
8974
  l.tracking.requests.shift();
8728
8975
  try {
8729
8976
  await l.rpc.write(a.frame);
8730
8977
  } catch (e) {
8731
- l.tracking.requests.unshift(open5);
8978
+ l.tracking.requests.unshift(open6);
8732
8979
  return e.message;
8733
8980
  }
8734
8981
  store.apply(id, { name: "Decided", at: now2(), source: "daemon" });
@@ -8777,15 +9024,15 @@ function createOmpHost(deps) {
8777
9024
  async answerAsk(id, requestId3, answers, cancel = false) {
8778
9025
  const l = live3.get(id);
8779
9026
  if (!l) return "the session is not running";
8780
- const open5 = l.tracking.requests[0];
8781
- if (!open5 || open5.id !== requestId3) return "the request changed, look again";
8782
- const a = cancel ? { frame: { type: "extension_ui_response", id: open5.id, cancelled: true } } : askAnswerFor(open5, answers);
9027
+ const open6 = l.tracking.requests[0];
9028
+ if (!open6 || open6.id !== requestId3) return "the request changed, look again";
9029
+ const a = cancel ? { frame: { type: "extension_ui_response", id: open6.id, cancelled: true } } : askAnswerFor(open6, answers);
8783
9030
  if ("refused" in a) return a.refused;
8784
9031
  l.tracking.requests.shift();
8785
9032
  try {
8786
9033
  await l.rpc.write(a.frame);
8787
9034
  } catch (e) {
8788
- l.tracking.requests.unshift(open5);
9035
+ l.tracking.requests.unshift(open6);
8789
9036
  return e.message;
8790
9037
  }
8791
9038
  store.apply(id, { name: "Decided", at: now2(), source: "daemon" });
@@ -8871,9 +9118,9 @@ var init_catalog = __esm({
8871
9118
  // src/daemon/repos.ts
8872
9119
  import { execFile as execFile2 } from "child_process";
8873
9120
  import { constants } from "fs";
8874
- import { open, stat as stat2 } from "fs/promises";
8875
- import { homedir as homedir5 } from "os";
8876
- import { join as join11, resolve as resolve2 } from "path";
9121
+ import { open as open2, stat as stat3 } from "fs/promises";
9122
+ import { homedir as homedir6 } from "os";
9123
+ import { join as join12, resolve as resolve2 } from "path";
8877
9124
  function runGit(cwd, args) {
8878
9125
  return new Promise((res) => {
8879
9126
  execFile2("git", ["-C", cwd, ...args], { timeout: GIT_TIMEOUT_MS, windowsHide: true }, (err, out) => res(err ? null : out.trim() || null));
@@ -8891,7 +9138,7 @@ async function slot(job) {
8891
9138
  }
8892
9139
  }
8893
9140
  async function readGitFile(path3, max = 64 * 1024) {
8894
- const fh = await open(path3, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
9141
+ const fh = await open2(path3, constants.O_RDONLY | (constants.O_NONBLOCK ?? 0));
8895
9142
  try {
8896
9143
  if (!(await fh.stat()).isFile()) throw new Error(`not a regular file: ${path3}`);
8897
9144
  const buf2 = Buffer.alloc(max + 1);
@@ -8905,7 +9152,7 @@ async function readGitFile(path3, max = 64 * 1024) {
8905
9152
  async function headBranch(dotGit) {
8906
9153
  let head2;
8907
9154
  try {
8908
- head2 = (await readGitFile(join11(dotGit, "HEAD"), MAX_HEAD_BYTES)).trim();
9155
+ head2 = (await readGitFile(join12(dotGit, "HEAD"), MAX_HEAD_BYTES)).trim();
8909
9156
  } catch {
8910
9157
  return null;
8911
9158
  }
@@ -8914,23 +9161,23 @@ async function headBranch(dotGit) {
8914
9161
  return /^[0-9a-f]{7,}$/i.test(head2) ? head2.slice(0, 7) : null;
8915
9162
  }
8916
9163
  async function inspectRepos(paths2, opts = {}) {
8917
- const home2 = opts.home ?? homedir5();
9164
+ const home2 = opts.home ?? homedir6();
8918
9165
  const git = opts.git ?? runGit;
8919
9166
  return Promise.all(paths2.map((raw) => slot(async () => {
8920
9167
  const input = raw.trim();
8921
9168
  const path3 = resolve2(input === "~" ? home2 : input.startsWith("~/") ? resolve2(home2, input.slice(2)) : input);
8922
9169
  let isDir2 = false;
8923
9170
  try {
8924
- isDir2 = (await stat2(path3)).isDirectory();
9171
+ isDir2 = (await stat3(path3)).isDirectory();
8925
9172
  } catch {
8926
9173
  }
8927
9174
  if (!isDir2) return { path: raw, exists: false, branch: null };
8928
9175
  let dotDir = false;
8929
9176
  try {
8930
- dotDir = (await stat2(join11(path3, ".git"))).isDirectory();
9177
+ dotDir = (await stat3(join12(path3, ".git"))).isDirectory();
8931
9178
  } catch {
8932
9179
  }
8933
- if (dotDir) return { path: raw, exists: true, branch: await headBranch(join11(path3, ".git")) };
9180
+ if (dotDir) return { path: raw, exists: true, branch: await headBranch(join12(path3, ".git")) };
8934
9181
  const deadline = Date.now() + GIT_TIMEOUT_MS;
8935
9182
  const bounded = (args) => new Promise((res) => {
8936
9183
  const t = setTimeout(() => res(null), Math.max(0, deadline - Date.now()));
@@ -8962,7 +9209,7 @@ var init_repos2 = __esm({
8962
9209
 
8963
9210
  // src/daemon/scan.ts
8964
9211
  import { readdir as fsReaddir } from "fs/promises";
8965
- import { dirname as dirname4, isAbsolute, join as join12, resolve as resolve3, sep as sep2 } from "path";
9212
+ import { dirname as dirname4, isAbsolute, join as join13, resolve as resolve3, sep as sep2 } from "path";
8966
9213
  function extraRoots(home2, cwds) {
8967
9214
  const h = resolve3(home2);
8968
9215
  const out = [];
@@ -9008,19 +9255,19 @@ function originUrl(config) {
9008
9255
  }
9009
9256
  return url || void 0;
9010
9257
  }
9011
- async function remoteOf(dir, dotIsFile, readFile19) {
9258
+ async function remoteOf(dir, dotIsFile, readFile20) {
9012
9259
  try {
9013
- let gitDir = join12(dir, ".git");
9260
+ let gitDir = join13(dir, ".git");
9014
9261
  if (dotIsFile) {
9015
- const m = /^gitdir:\s*(.+)$/m.exec(await readFile19(gitDir));
9262
+ const m = /^gitdir:\s*(.+)$/m.exec(await readFile20(gitDir));
9016
9263
  if (!m) return void 0;
9017
9264
  gitDir = resolve3(dir, m[1].trim());
9018
9265
  try {
9019
- gitDir = resolve3(gitDir, (await readFile19(join12(gitDir, "commondir"))).trim());
9266
+ gitDir = resolve3(gitDir, (await readFile20(join13(gitDir, "commondir"))).trim());
9020
9267
  } catch {
9021
9268
  }
9022
9269
  }
9023
- const url = originUrl(await readFile19(join12(gitDir, "config")));
9270
+ const url = originUrl(await readFile20(join13(gitDir, "config")));
9024
9271
  return url ? repositoryId(url) : void 0;
9025
9272
  } catch {
9026
9273
  return void 0;
@@ -9037,18 +9284,18 @@ function manifestName(file, text7) {
9037
9284
  } else name = /<artifactId>\s*([^<]+?)\s*<\/artifactId>/.exec(text7.replace(/<!--[\s\S]*?-->/g, "").replace(/<parent>[\s\S]*?<\/parent>/g, ""))?.[1];
9038
9285
  return typeof name === "string" && name.trim() && name.length <= 200 ? name.trim() : void 0;
9039
9286
  }
9040
- async function describe(dir, dotIsFile, home2, readFile19, manifest) {
9287
+ async function describe(dir, dotIsFile, home2, readFile20, manifest) {
9041
9288
  const [branch, remote, named] = await Promise.all([
9042
- dotIsFile ? inspectRepos([dir], { home: home2 }).then((r) => r[0]?.branch ?? null) : headBranch(join12(dir, ".git")),
9043
- remoteOf(dir, dotIsFile, readFile19),
9044
- manifest ? readFile19(join12(dir, manifest)).then((text7) => manifestName(manifest, text7), () => void 0) : void 0
9289
+ dotIsFile ? inspectRepos([dir], { home: home2 }).then((r) => r[0]?.branch ?? null) : headBranch(join13(dir, ".git")),
9290
+ remoteOf(dir, dotIsFile, readFile20),
9291
+ manifest ? readFile20(join13(dir, manifest)).then((text7) => manifestName(manifest, text7), () => void 0) : void 0
9045
9292
  ]);
9046
9293
  return { path: dir, branch, ...remote ? { remote } : {}, ...named ? { manifest: named } : {} };
9047
9294
  }
9048
9295
  async function scanRepos(opts) {
9049
9296
  const now2 = opts.now ?? Date.now;
9050
9297
  const readdir5 = opts.readdir ?? ((p) => fsReaddir(p, { withFileTypes: true }));
9051
- const readFile19 = opts.readFile ?? ((p) => readGitFile(p));
9298
+ const readFile20 = opts.readFile ?? ((p) => readGitFile(p));
9052
9299
  const depth = opts.limits?.depth ?? SCAN_DEPTH, maxDirs = opts.limits?.dirs ?? SCAN_DIRS, ms = opts.limits?.ms ?? SCAN_MS, maxRepos = opts.limits?.repos ?? SCAN_REPOS;
9053
9300
  const deadline = now2() + ms;
9054
9301
  let timer2;
@@ -9083,11 +9330,11 @@ async function scanRepos(opts) {
9083
9330
  if (!entries) return;
9084
9331
  const dot = entries.find((e) => e.name === ".git" && (e.isDirectory() || e.isFile()));
9085
9332
  const manifest = dot ? MANIFESTS.find((m) => entries.some((e) => e.name === m && e.isFile())) : void 0;
9086
- if (dot) found.push(describe(d.path, dot.isFile(), opts.home, readFile19, manifest));
9333
+ if (dot) found.push(describe(d.path, dot.isFile(), opts.home, readFile20, manifest));
9087
9334
  if (dot && d.path !== home2 || d.depth >= depth) return;
9088
9335
  const names = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SKIP.has(e.name)).map((e) => e.name).sort();
9089
9336
  for (const name of names) {
9090
- const path3 = join12(d.path, name);
9337
+ const path3 = join13(d.path, name);
9091
9338
  if (!seen.has(path3)) {
9092
9339
  seen.add(path3);
9093
9340
  queue.push({ path: path3, depth: d.depth + 1 });
@@ -9131,23 +9378,23 @@ var init_scan2 = __esm({
9131
9378
  });
9132
9379
 
9133
9380
  // src/daemon/folders.ts
9134
- import { mkdir as mkdir8, readdir as readdir3, stat as stat3 } from "fs/promises";
9135
- import { homedir as homedir6 } from "os";
9136
- import { dirname as dirname5, isAbsolute as isAbsolute2, join as join13, resolve as resolve4 } from "path";
9381
+ import { mkdir as mkdir8, readdir as readdir3, stat as stat4 } from "fs/promises";
9382
+ import { homedir as homedir7 } from "os";
9383
+ import { dirname as dirname5, isAbsolute as isAbsolute2, join as join14, resolve as resolve4 } from "path";
9137
9384
  async function resolveExact(raw, home2) {
9138
9385
  const expanded = raw === "~" ? home2 : raw.startsWith("~/") ? resolve4(home2, raw.slice(2)) : raw;
9139
9386
  if (!isAbsolute2(expanded)) throw new Error(`not an absolute path: ${raw}`);
9140
9387
  const path3 = resolve4(expanded);
9141
9388
  let s;
9142
9389
  try {
9143
- s = await stat3(path3);
9390
+ s = await stat4(path3);
9144
9391
  } catch {
9145
9392
  throw new Error(`no such folder: ${path3}`);
9146
9393
  }
9147
9394
  if (!s.isDirectory()) throw new Error(`not a folder: ${path3}`);
9148
9395
  return path3;
9149
9396
  }
9150
- async function resolveFolder(input, home2 = homedir6()) {
9397
+ async function resolveFolder(input, home2 = homedir7()) {
9151
9398
  return resolveExact(input.trim(), home2);
9152
9399
  }
9153
9400
  async function subfolders(path3) {
@@ -9156,7 +9403,7 @@ async function subfolders(path3) {
9156
9403
  if (d.isDirectory()) return d.name;
9157
9404
  if (!d.isSymbolicLink()) return null;
9158
9405
  try {
9159
- return (await stat3(join13(path3, d.name))).isDirectory() ? d.name : null;
9406
+ return (await stat4(join14(path3, d.name))).isDirectory() ? d.name : null;
9160
9407
  } catch {
9161
9408
  return null;
9162
9409
  }
@@ -9164,42 +9411,42 @@ async function subfolders(path3) {
9164
9411
  return found.filter((n) => n !== null).sort(byName.compare);
9165
9412
  }
9166
9413
  async function gitBranchOf(dir, git) {
9167
- const dot = join13(dir, ".git");
9414
+ const dot = join14(dir, ".git");
9168
9415
  let s;
9169
9416
  try {
9170
- s = await stat3(dot);
9417
+ s = await stat4(dot);
9171
9418
  } catch {
9172
9419
  return null;
9173
9420
  }
9174
9421
  if (s.isFile()) return (await inspectRepos([dir], { git }))[0]?.branch ?? null;
9175
9422
  return s.isDirectory() ? headBranch(dot) : null;
9176
9423
  }
9177
- async function listFolder(input, home2 = homedir6(), requestId3, git) {
9424
+ async function listFolder(input, home2 = homedir7(), requestId3, git) {
9178
9425
  const tag = requestId3 === void 0 ? {} : { requestId: requestId3 };
9179
9426
  const asked2 = input || "~";
9180
9427
  try {
9181
9428
  const path3 = await resolveExact(asked2, home2);
9182
9429
  const parent = dirname5(path3);
9183
9430
  const entries = await subfolders(path3);
9184
- const branches = await Promise.all(entries.map((name) => gitBranchOf(join13(path3, name), git)));
9431
+ const branches = await Promise.all(entries.map((name) => gitBranchOf(join14(path3, name), git)));
9185
9432
  const repos = entries.flatMap((name, i) => branches[i] === null ? [] : [{ name, branch: branches[i] }]);
9186
9433
  return { type: "folder.listing", path: path3, parent: parent === path3 ? null : parent, entries, repos, ...tag };
9187
9434
  } catch (e) {
9188
9435
  return { type: "folder.listing", path: asked2, parent: null, entries: [], error: e.message, ...tag };
9189
9436
  }
9190
9437
  }
9191
- async function createFolder(parent, name, home2 = homedir6(), requestId3) {
9438
+ async function createFolder(parent, name, home2 = homedir7(), requestId3) {
9192
9439
  const at = await listFolder(parent, home2, requestId3);
9193
9440
  if (at.error) return at;
9194
9441
  const clean = name.trim();
9195
9442
  if (!clean || clean === "." || clean === ".." || /[/\0]/.test(clean)) return { ...at, error: `invalid folder name: ${name}` };
9196
9443
  try {
9197
- await mkdir8(join13(at.path, clean));
9444
+ await mkdir8(join14(at.path, clean));
9198
9445
  } catch (e) {
9199
9446
  const code3 = e.code;
9200
9447
  return { ...at, error: code3 === "EEXIST" ? `already exists: ${clean}` : `cannot create ${clean}: ${e.message}` };
9201
9448
  }
9202
- return listFolder(join13(at.path, clean), home2, requestId3);
9449
+ return listFolder(join14(at.path, clean), home2, requestId3);
9203
9450
  }
9204
9451
  var byName;
9205
9452
  var init_folders = __esm({
@@ -9263,7 +9510,7 @@ function taskOf2(ref, ctx) {
9263
9510
  return taskClosed(t) ? no("closed", `task ${t.id} is closed`) : t;
9264
9511
  }
9265
9512
  function writable(s) {
9266
- return s.source === "hook_only" ? no("read_only", `${s.name} is read-only`) : s.state === "exited" ? no("exited", `${s.name} has exited`) : null;
9513
+ return s.source === "hook_only" || isOmpSeat2(s) ? no("read_only", `${s.name} is read-only`) : s.state === "exited" ? no("exited", `${s.name} has exited`) : null;
9267
9514
  }
9268
9515
  function cardRefusal(row3) {
9269
9516
  if (row3.action !== "session.decide" || !row3.detailCut || row3.args.decision !== "accept") return null;
@@ -9316,6 +9563,7 @@ var init_actions2 = __esm({
9316
9563
  "src/daemon/actions.ts"() {
9317
9564
  "use strict";
9318
9565
  init_src();
9566
+ init_seats();
9319
9567
  DECIDABLE = ["permission", "diff", "plan"];
9320
9568
  KIND = { permission: "permission", diff: "diff", plan: "plan" };
9321
9569
  no = (code3, refused) => ({ refused, code: code3 });
@@ -9435,13 +9683,13 @@ ${p.body}` : p.text) }
9435
9683
  });
9436
9684
 
9437
9685
  // src/chat/guide.ts
9438
- import { access, constants as constants2, readFile as readFile7, writeFile as writeFile8 } from "fs/promises";
9439
- import { delimiter, join as join14 } from "path";
9686
+ import { access, constants as constants2, readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
9687
+ import { delimiter, join as join15 } from "path";
9440
9688
  import { fileURLToPath as fileURLToPath2 } from "url";
9441
9689
  async function readGuide(files = GUIDE_FILES) {
9442
9690
  for (const f of files) {
9443
9691
  try {
9444
- return await readFile7(f, "utf8");
9692
+ return await readFile8(f, "utf8");
9445
9693
  } catch {
9446
9694
  }
9447
9695
  }
@@ -9453,7 +9701,7 @@ async function installedAgents(path3) {
9453
9701
  for (const tool of ToolSchema.options) {
9454
9702
  for (const d of dirs) {
9455
9703
  try {
9456
- await access(join14(d, AGENT_BIN2[tool]), constants2.X_OK);
9704
+ await access(join15(d, AGENT_BIN2[tool]), constants2.X_OK);
9457
9705
  found.push(tool);
9458
9706
  break;
9459
9707
  } catch {
@@ -9485,7 +9733,7 @@ function ledgeMd(info) {
9485
9733
  ].join("\n");
9486
9734
  }
9487
9735
  async function writeLedgeMd(dir, info) {
9488
- await writeFile8(join14(dir, LEDGE_MD), ledgeMd(info));
9736
+ await writeFile8(join15(dir, LEDGE_MD), ledgeMd(info));
9489
9737
  }
9490
9738
  var LEDGE_MD, GUIDE_FILES, AGENT_BIN2, RISK;
9491
9739
  var init_guide = __esm({
@@ -9500,8 +9748,8 @@ var init_guide = __esm({
9500
9748
  });
9501
9749
 
9502
9750
  // src/chat/notes.ts
9503
- import { open as open2, readdir as readdir4, stat as stat4, writeFile as writeFile9 } from "fs/promises";
9504
- import { join as join15 } from "path";
9751
+ import { open as open3, readdir as readdir4, stat as stat5, writeFile as writeFile9 } from "fs/promises";
9752
+ import { join as join16 } from "path";
9505
9753
  function scalarValue(raw) {
9506
9754
  const v2 = raw.trim();
9507
9755
  const q = v2[0];
@@ -9533,7 +9781,7 @@ function copyName(file, taken) {
9533
9781
  return name;
9534
9782
  }
9535
9783
  async function head(path3, max) {
9536
- const fh = await open2(path3, "r");
9784
+ const fh = await open3(path3, "r");
9537
9785
  try {
9538
9786
  const buf2 = Buffer.alloc(max);
9539
9787
  const { bytesRead } = await fh.read(buf2, 0, max, 0);
@@ -9552,12 +9800,12 @@ async function writeNotes(dir, from, steps, max = NOTE_MAX2) {
9552
9800
  if (!files.length) lines.push(`- (none: the user can add Markdown files to \`${from}\`, with an optional front matter \`title\` and \`when\`)`);
9553
9801
  const taken = /* @__PURE__ */ new Set();
9554
9802
  for (const file of files.slice(0, NOTES_MAX)) {
9555
- const path3 = join15(from, file);
9803
+ const path3 = join16(from, file);
9556
9804
  let text7;
9557
9805
  let size;
9558
9806
  let mtime;
9559
9807
  try {
9560
- const st = await stat4(path3);
9808
+ const st = await stat5(path3);
9561
9809
  size = st.size;
9562
9810
  mtime = st.mtime.toISOString();
9563
9811
  text7 = await head(path3, max);
@@ -9574,15 +9822,15 @@ async function writeNotes(dir, from, steps, max = NOTE_MAX2) {
9574
9822
  const when = fields.when?.trim() ? `when: ${line(fields.when)}` : `first line: ${line(body.split("\n").find((l) => l.trim()) ?? "") || "(empty)"}`;
9575
9823
  const truncated = size > max;
9576
9824
  const name = copyName(file, taken);
9577
- await writeFile9(join15(dir, name), truncated ? `${text7}
9825
+ await writeFile9(join16(dir, name), truncated ? `${text7}
9578
9826
 
9579
9827
  [truncated by Ledge: the first ${max} of ${size} bytes]
9580
9828
  ` : text7);
9581
- steps.set(name, { kind: "repo", name: file, path: join15(from, file) });
9829
+ steps.set(name, { kind: "repo", name: file, path: join16(from, file) });
9582
9830
  lines.push(`- ${name} \u2014 \xAB ${title3} \xBB \u2014 ${when} \u2014 modified ${mtime}${truncated ? ` \u2014 TRUNCATED: only the first ${max} of ${size} bytes are copied` : ""}`);
9583
9831
  }
9584
9832
  if (files.length > NOTES_MAX) lines.push(`- (${files.length - NOTES_MAX} more notes not copied: Ledge copies ${NOTES_MAX} at most)`);
9585
- await writeFile9(join15(dir, NOTES_INDEX), [...lines, ""].join("\n"));
9833
+ await writeFile9(join16(dir, NOTES_INDEX), [...lines, ""].join("\n"));
9586
9834
  }
9587
9835
  var NOTE_MAX2, NOTES_MAX, NOTES_INDEX, unquote, line;
9588
9836
  var init_notes = __esm({
@@ -9601,8 +9849,8 @@ var init_notes = __esm({
9601
9849
 
9602
9850
  // src/chat/context.ts
9603
9851
  import { mkdir as mkdir9, rm as rm2, writeFile as writeFile10 } from "fs/promises";
9604
- import { homedir as homedir7 } from "os";
9605
- import { basename as basename8, join as join16, resolve as resolve5 } from "path";
9852
+ import { homedir as homedir8 } from "os";
9853
+ import { basename as basename8, join as join17, resolve as resolve5 } from "path";
9606
9854
  async function writeContext(dir, input) {
9607
9855
  await rm2(dir, { recursive: true, force: true });
9608
9856
  await mkdir9(dir, { recursive: true });
@@ -9631,29 +9879,29 @@ async function writeContext(dir, input) {
9631
9879
  const pane = await input.capture(s).catch(() => null);
9632
9880
  if (pane?.length) {
9633
9881
  const f = `pane-${s.id}.txt`;
9634
- await writeFile10(join16(dir, f), pane.join("\n"));
9882
+ await writeFile10(join17(dir, f), pane.join("\n"));
9635
9883
  steps.set(f, { kind: "pane", id: s.id, name: s.name });
9636
9884
  lines.push(`- terminal, last lines: ${f}`);
9637
9885
  }
9638
9886
  if (s.lastReply?.text.trim()) {
9639
9887
  const f = `reply-${s.id}.md`;
9640
- await writeFile10(join16(dir, f), s.lastReply.text);
9888
+ await writeFile10(join17(dir, f), s.lastReply.text);
9641
9889
  steps.set(f, { kind: "reply", id: s.id, name: s.name });
9642
9890
  lines.push(`- last reply (${s.lastReply.at}): ${f}`);
9643
9891
  }
9644
9892
  const g = await input.git(s.cwd).catch(() => null);
9645
9893
  if (g) {
9646
9894
  const f = `git-${s.id}.txt`;
9647
- await writeFile10(join16(dir, f), g);
9895
+ await writeFile10(join17(dir, f), g);
9648
9896
  steps.set(f, { kind: "git", id: s.id, name: s.name });
9649
9897
  lines.push(`- git status, log and diff stat: ${f}`);
9650
9898
  }
9651
9899
  lines.push("");
9652
9900
  }
9653
- await writeFile10(join16(dir, "sessions.md"), lines.join("\n"));
9901
+ await writeFile10(join17(dir, "sessions.md"), lines.join("\n"));
9654
9902
  steps.set("sessions.md", { kind: "sessions" });
9655
9903
  const recents = input.recents ?? [];
9656
- await writeFile10(join16(dir, "folders.md"), [
9904
+ await writeFile10(join17(dir, "folders.md"), [
9657
9905
  "# Recent folders",
9658
9906
  "",
9659
9907
  "The folders the user started sessions in, most recent first.",
@@ -9661,7 +9909,7 @@ async function writeContext(dir, input) {
9661
9909
  ...recents.length ? recents.map((r) => `- ${r.project}: ${r.cwd} (last used ${r.lastAt})`) : ["- (none yet)"],
9662
9910
  ""
9663
9911
  ].join("\n"));
9664
- if (withProjects) await writeFile10(join16(dir, "projects.md"), [
9912
+ if (withProjects) await writeFile10(join17(dir, "projects.md"), [
9665
9913
  "# Projects",
9666
9914
  "",
9667
9915
  ...projects.projects.length ? projects.projects.flatMap((p) => [`## ${oneLine(p.name)}`, ...p.description.trim() ? [oneLine(p.description)] : [], "", ...p.repos.map((r) => `- ${r.machine}: ${r.path}`), ""]) : ["No projects.", ""],
@@ -9679,7 +9927,7 @@ async function writeContext(dir, input) {
9679
9927
  async function writeTasksContext(dir, taskList, steps) {
9680
9928
  const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
9681
9929
  const shown = taskList.filter((t) => !taskClosed(t) || t.updatedAt.slice(0, 10) === day);
9682
- await writeFile10(join16(dir, "tasks.md"), [
9930
+ await writeFile10(join17(dir, "tasks.md"), [
9683
9931
  "# Tasks",
9684
9932
  "",
9685
9933
  "Open tasks and those closed today. A giver `Ledge` is the user, through you.",
@@ -9691,7 +9939,7 @@ async function writeTasksContext(dir, taskList, steps) {
9691
9939
  }
9692
9940
  function coversHome(path3) {
9693
9941
  const p = resolve5(path3);
9694
- const home2 = resolve5(homedir7());
9942
+ const home2 = resolve5(homedir8());
9695
9943
  return p === home2 || home2.startsWith(p.endsWith("/") ? p : `${p}/`);
9696
9944
  }
9697
9945
  function stepFor(path3, ctx) {
@@ -9730,14 +9978,14 @@ var LEDGE_VERSION;
9730
9978
  var init_version = __esm({
9731
9979
  "src/version.ts"() {
9732
9980
  "use strict";
9733
- LEDGE_VERSION = true ? "0.40.0" : "0.0.0-dev";
9981
+ LEDGE_VERSION = true ? "0.42.0" : "0.0.0-dev";
9734
9982
  }
9735
9983
  });
9736
9984
 
9737
9985
  // src/chat/forge.ts
9738
9986
  import { execFile as execFile4 } from "child_process";
9739
9987
  import { writeFile as writeFile11 } from "fs/promises";
9740
- import { join as join17 } from "path";
9988
+ import { join as join18 } from "path";
9741
9989
  function forgeOf(remote) {
9742
9990
  const id = repositoryId(remote);
9743
9991
  if (!id) return null;
@@ -9802,12 +10050,12 @@ function createForge(deps = {}) {
9802
10050
  return;
9803
10051
  }
9804
10052
  const parts2 = await Promise.all(local.map((r) => section(r.path)));
9805
- await writeFile11(join17(ctx.dir, file), parts2.flatMap((s) => s.lines).join("\n"));
10053
+ await writeFile11(join18(ctx.dir, file), parts2.flatMap((s) => s.lines).join("\n"));
9806
10054
  const sources = parts2.flatMap((s) => s.source ? [s.source] : []);
9807
10055
  ctx.steps.set(file, { kind: "repo", name, path: sources.join(", ") || file });
9808
10056
  index2.push(`- ${name}: ${file}`);
9809
10057
  }));
9810
- await writeFile11(join17(ctx.dir, "forge.md"), [...index2, ""].join("\n"));
10058
+ await writeFile11(join18(ctx.dir, "forge.md"), [...index2, ""].join("\n"));
9811
10059
  }
9812
10060
  };
9813
10061
  }
@@ -10074,7 +10322,7 @@ var init_codex2 = __esm({
10074
10322
  // src/chat/cursor.ts
10075
10323
  import { spawn as nodeSpawn5 } from "child_process";
10076
10324
  import { copyFileSync, writeFileSync } from "fs";
10077
- import { extname, join as join18 } from "path";
10325
+ import { extname, join as join19 } from "path";
10078
10326
  function cursorArgs(o) {
10079
10327
  return [
10080
10328
  "-p",
@@ -10116,10 +10364,10 @@ function parseCursorLine(line2) {
10116
10364
  }
10117
10365
  function runCursor(o, onEvent, spawnImpl = nodeSpawn5) {
10118
10366
  const [file = "cursor-agent", ...pre] = o.bin ?? ["cursor-agent"];
10119
- writeFileSync(join18(o.cwd, "AGENTS.md"), `${o.persona ?? buildPersona("cursor")}
10367
+ writeFileSync(join19(o.cwd, "AGENTS.md"), `${o.persona ?? buildPersona("cursor")}
10120
10368
  `);
10121
10369
  const copies = (o.images ?? []).map((img, n) => {
10122
- const to = join18(o.cwd, `image-${n + 1}${extname(img.path)}`);
10370
+ const to = join19(o.cwd, `image-${n + 1}${extname(img.path)}`);
10123
10371
  copyFileSync(img.path, to);
10124
10372
  return to;
10125
10373
  });
@@ -10203,9 +10451,9 @@ var init_omp = __esm({
10203
10451
 
10204
10452
  // src/chat/service.ts
10205
10453
  import { randomUUID as randomUUID7 } from "crypto";
10206
- import { mkdir as mkdir10, readFile as readFile8, rm as rm3, writeFile as writeFile12 } from "fs/promises";
10207
- import { homedir as homedir8 } from "os";
10208
- import { basename as basename9, dirname as dirname6, join as join19, resolve as resolve6 } from "path";
10454
+ import { mkdir as mkdir10, readFile as readFile9, rm as rm3, writeFile as writeFile12 } from "fs/promises";
10455
+ import { homedir as homedir9 } from "os";
10456
+ import { basename as basename9, dirname as dirname6, join as join20, resolve as resolve6 } from "path";
10209
10457
  function droppedNote(total, kept, reasons, counted) {
10210
10458
  const dropped = total - kept;
10211
10459
  if (dropped <= 0) return "";
@@ -10226,7 +10474,7 @@ function createChat(deps) {
10226
10474
  const now2 = deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
10227
10475
  const today = deps.today ?? (() => now2().slice(0, 10));
10228
10476
  const uuid = deps.uuid ?? randomUUID7;
10229
- const dayFile = join19(deps.home, "chat.json");
10477
+ const dayFile = join20(deps.home, "chat.json");
10230
10478
  let turns2 = [];
10231
10479
  let turnsDay = "";
10232
10480
  let busy = false;
@@ -10249,7 +10497,7 @@ function createChat(deps) {
10249
10497
  const day = today();
10250
10498
  if (!fresh && !freshNext) {
10251
10499
  try {
10252
- const saved = JSON.parse(await readFile8(dayFile, "utf8"));
10500
+ const saved = JSON.parse(await readFile9(dayFile, "utf8"));
10253
10501
  if (saved.day === day && saved.sessionId && (saved.tool ?? "claude") === tool) return { sessionId: saved.sessionId, resume: true };
10254
10502
  } catch {
10255
10503
  }
@@ -10265,7 +10513,7 @@ function createChat(deps) {
10265
10513
  const p = await deps.tmux.capture(s.tmuxSession, PANE_LINES);
10266
10514
  return p === null ? null : p.split("\n");
10267
10515
  };
10268
- const contextDir = join19(deps.home, "chat", "context");
10516
+ const contextDir = join20(deps.home, "chat", "context");
10269
10517
  async function proposalFrom(raw, tool, qid) {
10270
10518
  const parsed = parseActionBlock("session", raw);
10271
10519
  if ("error" in parsed) {
@@ -10342,7 +10590,7 @@ function createChat(deps) {
10342
10590
  for (const r of block2.repos) {
10343
10591
  try {
10344
10592
  const path3 = await resolveFolder(r.path, deps.userHome);
10345
- const wide = tooWide(path3, resolve6(deps.userHome ?? homedir8()));
10593
+ const wide = tooWide(path3, resolve6(deps.userHome ?? homedir9()));
10346
10594
  if (wide) {
10347
10595
  log(`chat ${qid}: project path dropped: ${wide}: ${path3}`);
10348
10596
  continue;
@@ -10458,7 +10706,7 @@ function createChat(deps) {
10458
10706
  let resultError = null;
10459
10707
  const handle2 = runTool(tool, { question: mine.told.text ? `${mine.told.text}
10460
10708
 
10461
- ${turn.question}` : turn.question, cwd: ctx.dir, addDirs: ctx.repoPaths, sessionId: sessionId2, resume, env, bin: deps.bins?.[tool] ?? deps.bin, ...images.length > 0 ? { images } : {}, ...tool === "omp" ? { sessionDir: join19(deps.home, "chat", "omp", sessionId2) } : {} }, (e) => {
10709
+ ${turn.question}` : turn.question, cwd: ctx.dir, addDirs: ctx.repoPaths, sessionId: sessionId2, resume, env, bin: deps.bins?.[tool] ?? deps.bin, ...images.length > 0 ? { images } : {}, ...tool === "omp" ? { sessionDir: join20(deps.home, "chat", "omp", sessionId2) } : {} }, (e) => {
10462
10710
  if (e.type === "tool") {
10463
10711
  const step3 = e.path ? stepFor(e.path, ctx) : null;
10464
10712
  if (step3 && !turn.steps.some((s) => sameStep(s, step3))) {
@@ -10745,8 +10993,8 @@ var init_service = __esm({
10745
10993
  });
10746
10994
 
10747
10995
  // src/chat/threads.ts
10748
- import { readFile as readFile9, rm as rm4 } from "fs/promises";
10749
- import { join as join20 } from "path";
10996
+ import { readFile as readFile10, rm as rm4 } from "fs/promises";
10997
+ import { join as join21 } from "path";
10750
10998
  function createSlots(max = 3) {
10751
10999
  let used = 0;
10752
11000
  return {
@@ -10767,7 +11015,7 @@ function createChatThreads(deps) {
10767
11015
  const devices = /* @__PURE__ */ new Map();
10768
11016
  const emitters = /* @__PURE__ */ new Map();
10769
11017
  const dropping = /* @__PURE__ */ new Map();
10770
- const deviceHome = (id) => join20(deps.home, "mobile", "chat", id);
11018
+ const deviceHome = (id) => join21(deps.home, "mobile", "chat", id);
10771
11019
  const device = (id) => {
10772
11020
  checkId(id);
10773
11021
  if (dropping.has(id)) throw new Error("device thread is being dropped");
@@ -10796,7 +11044,7 @@ function createChatThreads(deps) {
10796
11044
  /** The PC's tool of the day file, whatever its day: `claude` without one. */
10797
11045
  async deviceTool() {
10798
11046
  try {
10799
- const t = JSON.parse(await readFile9(join20(deps.home, "chat.json"), "utf8")).tool;
11047
+ const t = JSON.parse(await readFile10(join21(deps.home, "chat.json"), "utf8")).tool;
10800
11048
  return t === "claude" || t === "codex" || t === "cursor" || t === "omp" ? t : "claude";
10801
11049
  } catch {
10802
11050
  return "claude";
@@ -10963,7 +11211,7 @@ var init_island2 = __esm({
10963
11211
 
10964
11212
  // src/daemon/documents.ts
10965
11213
  import { constants as constants3 } from "fs";
10966
- import { open as open3, realpath } from "fs/promises";
11214
+ import { open as open4, realpath } from "fs/promises";
10967
11215
  import { isAbsolute as isAbsolute3, dirname as dirname7, extname as extname2, resolve as resolve7 } from "path";
10968
11216
  async function readDocument(request2, context) {
10969
11217
  const source2 = request2.path;
@@ -10984,10 +11232,10 @@ async function readDocument(request2, context) {
10984
11232
  async function readAt(request2, path3) {
10985
11233
  let file;
10986
11234
  try {
10987
- file = await open3(path3, constants3.O_RDONLY | constants3.O_NONBLOCK);
10988
- const stat7 = await file.stat();
10989
- if (!stat7.isFile()) return failure(request2, "not_file");
10990
- if (stat7.size > DOCUMENT_MAX_BYTES) return failure(request2, "too_large");
11235
+ file = await open4(path3, constants3.O_RDONLY | constants3.O_NONBLOCK);
11236
+ const stat8 = await file.stat();
11237
+ if (!stat8.isFile()) return failure(request2, "not_file");
11238
+ if (stat8.size > DOCUMENT_MAX_BYTES) return failure(request2, "too_large");
10991
11239
  const chunk = Buffer.allocUnsafe(64 * 1024);
10992
11240
  const parts2 = [];
10993
11241
  let bytes4 = 0;
@@ -11010,7 +11258,7 @@ async function readAt(request2, path3) {
11010
11258
  path: await realpath(path3),
11011
11259
  text: text7,
11012
11260
  bytes: bytes4,
11013
- modifiedAt: stat7.mtime.toISOString()
11261
+ modifiedAt: stat8.mtime.toISOString()
11014
11262
  };
11015
11263
  } catch (e) {
11016
11264
  return failure(request2, errorCode(e));
@@ -11039,9 +11287,9 @@ var init_documents2 = __esm({
11039
11287
 
11040
11288
  // src/daemon/models.ts
11041
11289
  import { execFile as execFile5 } from "child_process";
11042
- import { readFile as readFile10 } from "fs/promises";
11043
- import { homedir as homedir9 } from "os";
11044
- import { join as join21 } from "path";
11290
+ import { readFile as readFile11 } from "fs/promises";
11291
+ import { homedir as homedir10 } from "os";
11292
+ import { join as join22 } from "path";
11045
11293
  function claudeModels(settingsModel) {
11046
11294
  const efforts = [...CLAUDE_EFFORTS];
11047
11295
  const models = CLAUDE_MODELS.map((m) => ({ ...m, efforts, ...m.id === settingsModel ? { isDefault: true } : {} }));
@@ -11085,15 +11333,15 @@ function codexConfig(toml) {
11085
11333
  function createModels(deps = {}) {
11086
11334
  const env = deps.env ?? (async () => process.env);
11087
11335
  const run4 = deps.run ?? execRun2(env);
11088
- const readText2 = deps.readText ?? ((p) => readFile10(p, "utf8").catch(() => ""));
11336
+ const readText2 = deps.readText ?? ((p) => readFile11(p, "utf8").catch(() => ""));
11089
11337
  const now2 = deps.now ?? Date.now, ttl = deps.ttlMs ?? MODELS_TTL_MS;
11090
11338
  const cache = /* @__PURE__ */ new Map();
11091
11339
  const flying = /* @__PURE__ */ new Map();
11092
11340
  async function ask3(tool) {
11093
11341
  const e = await env();
11094
- const home2 = e.HOME ?? homedir9();
11342
+ const home2 = e.HOME ?? homedir10();
11095
11343
  if (tool === "claude") {
11096
- const settings = await readText2(join21(e.CLAUDE_CONFIG_DIR ?? join21(home2, ".claude"), "settings.json"));
11344
+ const settings = await readText2(join22(e.CLAUDE_CONFIG_DIR ?? join22(home2, ".claude"), "settings.json"));
11097
11345
  let model;
11098
11346
  try {
11099
11347
  model = settings ? JSON.parse(settings).model : void 0;
@@ -11109,7 +11357,7 @@ function createModels(deps = {}) {
11109
11357
  if (r.code === null) throw new Error("not installed");
11110
11358
  if (r.code !== 0) throw new Error(r.stderr.trim().split("\n").pop() || `exit ${r.code}`);
11111
11359
  if (tool === "cursor") return parseCursorModels(r.stdout);
11112
- return parseCodexModels(r.stdout, codexConfig(await readText2(join21(e.CODEX_HOME ?? join21(home2, ".codex"), "config.toml"))));
11360
+ return parseCodexModels(r.stdout, codexConfig(await readText2(join22(e.CODEX_HOME ?? join22(home2, ".codex"), "config.toml"))));
11113
11361
  }
11114
11362
  return {
11115
11363
  list(tool) {
@@ -11160,9 +11408,9 @@ var init_models2 = __esm({
11160
11408
 
11161
11409
  // src/daemon/commands.ts
11162
11410
  import { spawn as nodeSpawn7 } from "child_process";
11163
- import { stat as stat5 } from "fs/promises";
11164
- import { homedir as homedir10 } from "os";
11165
- import { join as join22 } from "path";
11411
+ import { stat as stat6 } from "fs/promises";
11412
+ import { homedir as homedir11 } from "os";
11413
+ import { join as join23 } from "path";
11166
11414
  function claudeSlashRefusal(text7) {
11167
11415
  const m = /^\/(\S+)(\s+\S[\s\S]*)?$/.exec(text7.trim());
11168
11416
  if (!m) return null;
@@ -11271,13 +11519,13 @@ function probeClaude(o, spawnImpl = nodeSpawn7) {
11271
11519
  function createCommands(deps = {}) {
11272
11520
  const env = deps.env ?? (async () => process.env);
11273
11521
  const claude = deps.claude ?? ((cwd, e) => probeClaude({ ...deps.claudeBin ? { bin: deps.claudeBin } : {}, cwd, env: e }));
11274
- const mtime = deps.mtime ?? ((p) => stat5(p).then((s) => s.mtimeMs, () => null));
11522
+ const mtime = deps.mtime ?? ((p) => stat6(p).then((s) => s.mtimeMs, () => null));
11275
11523
  const now2 = deps.now ?? Date.now, ttl = deps.ttlMs ?? COMMANDS_TTL_MS;
11276
11524
  const cache = /* @__PURE__ */ new Map();
11277
11525
  const flying = /* @__PURE__ */ new Map();
11278
11526
  async function mark(cwd, e) {
11279
- const home2 = e.CLAUDE_CONFIG_DIR ?? join22(e.HOME ?? homedir10(), ".claude");
11280
- const paths2 = [join22(cwd, ".claude", "commands"), join22(cwd, ".claude", "skills"), join22(home2, "commands"), join22(home2, "skills"), join22(home2, "plugins", "installed_plugins.json")];
11527
+ const home2 = e.CLAUDE_CONFIG_DIR ?? join23(e.HOME ?? homedir11(), ".claude");
11528
+ const paths2 = [join23(cwd, ".claude", "commands"), join23(cwd, ".claude", "skills"), join23(home2, "commands"), join23(home2, "skills"), join23(home2, "plugins", "installed_plugins.json")];
11281
11529
  return (await Promise.all(paths2.map(mtime))).join(",");
11282
11530
  }
11283
11531
  return {
@@ -11455,7 +11703,7 @@ var init_rigSpec = __esm({
11455
11703
  });
11456
11704
 
11457
11705
  // src/daemon/openrigRoles.ts
11458
- import { basename as basename10, dirname as dirname8, isAbsolute as isAbsolute5, join as join23, resolve as resolve9, sep as sep3 } from "path";
11706
+ import { basename as basename10, dirname as dirname8, isAbsolute as isAbsolute5, join as join24, resolve as resolve9, sep as sep3 } from "path";
11459
11707
  import { parse as parseYaml, stringify as stringify2, YAMLParseError } from "yaml";
11460
11708
  function readLibrary(listed2) {
11461
11709
  const out = [];
@@ -11487,15 +11735,15 @@ function parseSpec(text7) {
11487
11735
  }
11488
11736
  }
11489
11737
  function specPath(p, specDir, home2, openrigHome) {
11490
- if (p.startsWith("openrig-home:")) return join23(openrigHome, p.slice("openrig-home:".length));
11738
+ if (p.startsWith("openrig-home:")) return join24(openrigHome, p.slice("openrig-home:".length));
11491
11739
  if (p === "~") return home2;
11492
- if (p.startsWith("~/")) return join23(home2, p.slice(2));
11740
+ if (p.startsWith("~/")) return join24(home2, p.slice(2));
11493
11741
  return isAbsolute5(p) ? resolve9(p) : resolve9(specDir, p);
11494
11742
  }
11495
11743
  function importPath(ref, specDir) {
11496
11744
  const p = ref.startsWith("local:") ? resolve9(specDir, ref.slice(6)) : ref.startsWith("path:") ? resolve9(ref.slice(5)) : null;
11497
11745
  if (!p) return null;
11498
- return /\.ya?ml$/i.test(p) ? p : join23(p, "agent.yaml");
11746
+ return /\.ya?ml$/i.test(p) ? p : join24(p, "agent.yaml");
11499
11747
  }
11500
11748
  function declared(spec, ctx, ns, origin) {
11501
11749
  const out = [];
@@ -11724,7 +11972,7 @@ function layerOf(ownerRoot, path3, c) {
11724
11972
  if (c.agentDir && root2 === c.agentDir) return c.profileFiles.has(path3) ? "profile" : "agent";
11725
11973
  if (c.rigRoot && root2 === c.rigRoot) return path3 === c.cultureFile ? "culture" : c.memberFiles.has(path3) ? "member" : c.podFiles.has(path3) ? "pod" : "rig";
11726
11974
  if (/[/\\]@openrig[/\\]cli[/\\]/.test(root2) || /[/\\]daemon[/\\]assets$/.test(root2)) return "openrig";
11727
- if (under3(root2, join23(c.openrigHome, "state", "seats"))) return "seat";
11975
+ if (under3(root2, join24(c.openrigHome, "state", "seats"))) return "seat";
11728
11976
  return "unknown";
11729
11977
  }
11730
11978
  function layerOrigin(layer, c, path3) {
@@ -11734,7 +11982,7 @@ function layerOrigin(layer, c, path3) {
11734
11982
  case "profile":
11735
11983
  return { kind: "here", key: "profiles.startup.files" };
11736
11984
  case "culture":
11737
- return { kind: "inherited", from: "rig", ref: c.rig, key: "culture_file", ...c.rigRoot ? { path: join23(c.rigRoot, path3) } : {} };
11985
+ return { kind: "inherited", from: "rig", ref: c.rig, key: "culture_file", ...c.rigRoot ? { path: join24(c.rigRoot, path3) } : {} };
11738
11986
  case "rig":
11739
11987
  return { kind: "inherited", from: "rig", ref: c.rig };
11740
11988
  case "pod":
@@ -11762,12 +12010,12 @@ async function seatView(s, spec, profile, io, openrigHome, plugins) {
11762
12010
  const cwdV = str10(node2.cwd) ?? str10(shown.cwd);
11763
12011
  const cwd = cwdV ? { value: cwdV, origin: memberOrigin("cwd") } : { value: null, origin: { kind: "unknown", why: "not_exposed" } };
11764
12012
  const cultureV = str10(s.rigDoc.culture_file) ?? str10(s.rigDoc.cultureFile);
11765
- const cultureFile = sourced(cultureV, { kind: "inherited", from: "rig", ref: s.rig, key: "culture_file", ...cultureV && s.rigRoot ? { path: join23(s.rigRoot, cultureV) } : {} });
12013
+ const cultureFile = sourced(cultureV, { kind: "inherited", from: "rig", ref: s.rig, key: "culture_file", ...cultureV && s.rigRoot ? { path: join24(s.rigRoot, cultureV) } : {} });
11766
12014
  const base3 = s.rigRoot ?? (s.rigSpecPath ? dirname8(s.rigSpecPath) : "/");
11767
12015
  const memberStartup = writtenStartup(m.startup, "startup", "member", base3, (key) => memberOrigin(key));
11768
12016
  const pod = s.written?.pod ?? {};
11769
12017
  const effectiveRuntime = str10(node2.runtime) ?? runtime.value ?? void 0;
11770
- const into = cwdV ? join23(cwdV, guidanceFile(effectiveRuntime, s.rigDoc)) : void 0;
12018
+ const into = cwdV ? join24(cwdV, guidanceFile(effectiveRuntime, s.rigDoc)) : void 0;
11771
12019
  const profileName = str10(m.profile) ?? str10(shown.profile) ?? "default";
11772
12020
  const lc = {
11773
12021
  agentDir: s.agentDir,
@@ -12003,10 +12251,14 @@ var init_openrigRoles2 = __esm({
12003
12251
  import { execFile as execFile6 } from "child_process";
12004
12252
  import { get as httpGet } from "http";
12005
12253
  import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
12006
- import { cp, mkdir as mkdir11, open as openFile, readFile as readFile11, stat as statFile, writeFile as writeFile13 } from "fs/promises";
12254
+ import { cp, mkdir as mkdir11, open as openFile, readFile as readFile12, stat as statFile, writeFile as writeFile13 } from "fs/promises";
12007
12255
  import { parse as parseYaml2 } from "yaml";
12008
- import { homedir as homedir11 } from "os";
12009
- import { basename as basename11, delimiter as delimiter2, dirname as dirname9, isAbsolute as isAbsolute6, join as join24, resolve as resolve10, sep as sep4 } from "path";
12256
+ import { homedir as homedir12 } from "os";
12257
+ import { basename as basename11, delimiter as delimiter2, dirname as dirname9, isAbsolute as isAbsolute6, join as join25, resolve as resolve10, sep as sep4 } from "path";
12258
+ function specSource(raw) {
12259
+ const s = str11(raw);
12260
+ return s === "builtin" || s?.startsWith("builtin_") ? "builtin" : "user";
12261
+ }
12010
12262
  function toSeat(raw) {
12011
12263
  const n = obj9(raw);
12012
12264
  const logicalId = str11(n.logicalId), session = str11(n.canonicalSessionName);
@@ -12079,17 +12331,17 @@ function findRig(env, exists = existsSync2, list3 = (dir) => {
12079
12331
  return [];
12080
12332
  }
12081
12333
  }) {
12082
- for (const dir of (env.PATH ?? "").split(delimiter2)) if (dir && exists(join24(dir, "rig"))) return dir;
12083
- const home2 = env.HOME ?? homedir11();
12084
- const managed = (root2, sub) => list3(root2).filter((v2) => /^v?\d/.test(v2)).sort(byVersionDesc).map((v2) => join24(root2, v2, sub));
12334
+ for (const dir of (env.PATH ?? "").split(delimiter2)) if (dir && exists(join25(dir, "rig"))) return dir;
12335
+ const home2 = env.HOME ?? homedir12();
12336
+ const managed = (root2, sub) => list3(root2).filter((v2) => /^v?\d/.test(v2)).sort(byVersionDesc).map((v2) => join25(root2, v2, sub));
12085
12337
  const candidates = [
12086
- ...managed(join24(home2, ".local", "share", "nvm"), "bin"),
12087
- ...managed(join24(env.NVM_DIR ?? join24(home2, ".nvm"), "versions", "node"), "bin"),
12088
- join24(home2, ".volta", "bin"),
12089
- join24(home2, ".npm-global", "bin"),
12090
- join24(home2, ".local", "share", "pnpm")
12338
+ ...managed(join25(home2, ".local", "share", "nvm"), "bin"),
12339
+ ...managed(join25(env.NVM_DIR ?? join25(home2, ".nvm"), "versions", "node"), "bin"),
12340
+ join25(home2, ".volta", "bin"),
12341
+ join25(home2, ".npm-global", "bin"),
12342
+ join25(home2, ".local", "share", "pnpm")
12091
12343
  ];
12092
- return candidates.find((dir) => exists(join24(dir, "rig"))) ?? null;
12344
+ return candidates.find((dir) => exists(join25(dir, "rig"))) ?? null;
12093
12345
  }
12094
12346
  function readCheck(output) {
12095
12347
  const j = obj9(parse2(output));
@@ -12118,11 +12370,11 @@ function localGet(port, path3, timeoutMs) {
12118
12370
  function createOpenRig(deps = {}) {
12119
12371
  const run4 = deps.run ?? execRun3(deps.env ?? (async () => process.env));
12120
12372
  const files = deps.fs ?? realFs;
12121
- const home2 = deps.home ?? homedir11();
12122
- const rigsDir = deps.rigsDir ?? join24(home2, ".ledge", "rigs");
12373
+ const home2 = deps.home ?? homedir12();
12374
+ const rigsDir = deps.rigsDir ?? join25(home2, ".ledge", "rigs");
12123
12375
  const now2 = deps.now ?? Date.now;
12124
12376
  const roleIo = deps.roleIo ?? realRoleIo;
12125
- const openrigHome = deps.openrigHome ?? process.env.OPENRIG_HOME ?? join24(home2, ".openrig");
12377
+ const openrigHome = deps.openrigHome ?? process.env.OPENRIG_HOME ?? join25(home2, ".openrig");
12126
12378
  const http = deps.http ?? (async (path3) => {
12127
12379
  const port = (await state2()).daemon.port;
12128
12380
  if (!port) return void 0;
@@ -12206,7 +12458,7 @@ function createOpenRig(deps = {}) {
12206
12458
  const e = obj9(raw), name = str11(e.name), path3 = str11(e.resolvedSourcePath) ?? str11(e.sourcePath);
12207
12459
  if (e.kind !== "rig" || !name || !path3 || name === KERNEL_RIG) continue;
12208
12460
  const text7 = await files.read(path3).catch(() => null);
12209
- const spec = text7 === null ? null : readSpec(text7, { name, source: e.sourceType === "builtin" ? "builtin" : "user", path: path3, ...str11(e.summary) ? { summary: str11(e.summary).trim() } : {} });
12461
+ const spec = text7 === null ? null : readSpec(text7, { name, source: specSource(e.sourceType), path: path3, ...str11(e.summary) ? { summary: str11(e.summary).trim() } : {} });
12210
12462
  if (spec) out.push(spec);
12211
12463
  }
12212
12464
  return { specs: out.sort((a, b3) => a.source === b3.source ? a.name.localeCompare(b3.name) : a.source === "user" ? -1 : 1) };
@@ -12216,7 +12468,7 @@ function createOpenRig(deps = {}) {
12216
12468
  if (acting) return { ok: false, plan: plan2, commands: [], error: "busy" };
12217
12469
  acting = true;
12218
12470
  try {
12219
- const cwd = l.cwd === "~" ? home2 : l.cwd.startsWith("~/") ? join24(home2, l.cwd.slice(2)) : l.cwd;
12471
+ const cwd = l.cwd === "~" ? home2 : l.cwd.startsWith("~/") ? join25(home2, l.cwd.slice(2)) : l.cwd;
12220
12472
  if (!isAbsolute6(cwd) || !files.exists(cwd)) return { ok: false, plan: plan2, commands: [], error: "cwd_missing" };
12221
12473
  const listed2 = await specs();
12222
12474
  if (listed2.error) return { ok: false, plan: plan2, commands: [], error: listed2.error };
@@ -12228,17 +12480,17 @@ function createOpenRig(deps = {}) {
12228
12480
  const rename7 = l.name !== spec.name;
12229
12481
  if (changesSpec(l) || l.save || rename7 && !can2.name) {
12230
12482
  const composed = composeSpec(parseYaml2(text7), dirname9(spec.path), l);
12231
- dir = join24(rigsDir, l.name);
12483
+ dir = join25(rigsDir, l.name);
12232
12484
  await files.copyDir(dirname9(spec.path), dir);
12233
- await files.write(join24(dir, "rig.yaml"), composed.yaml);
12234
- source2 = join24(dir, "rig.yaml");
12485
+ await files.write(join25(dir, "rig.yaml"), composed.yaml);
12486
+ source2 = join25(dir, "rig.yaml");
12235
12487
  yaml = composed.yaml;
12236
12488
  seats = composed.seats;
12237
12489
  } else {
12238
12490
  yaml = text7;
12239
12491
  seats = specSeats(spec);
12240
12492
  }
12241
- if (!dir && rename7 && !plan2) await files.write(join24(rigsDir, l.name, "source.json"), JSON.stringify({ spec: spec.name, path: spec.path }));
12493
+ if (!dir && rename7 && !plan2) await files.write(join25(rigsDir, l.name, "source.json"), JSON.stringify({ spec: spec.name, path: spec.path }));
12242
12494
  const commands = [["up", source2, ...!dir && rename7 ? ["--name", l.name] : [], "--cwd", cwd, ...l.nonInterruptive ? ["--non-interruptive"] : [], ...plan2 ? ["--plan"] : ["--yes"], "--json"]];
12243
12495
  if (!plan2 && l.save && dir) commands.push(["specs", "add", dir, "--json"]);
12244
12496
  const shown = commands.map(showRigCommand);
@@ -12262,9 +12514,9 @@ function createOpenRig(deps = {}) {
12262
12514
  }
12263
12515
  }
12264
12516
  async function rootOf(rig) {
12265
- const mine = join24(rigsDir, rig);
12266
- if (files.exists(join24(mine, "rig.yaml"))) return mine;
12267
- const marker = await files.read(join24(mine, "source.json")).catch(() => null);
12517
+ const mine = join25(rigsDir, rig);
12518
+ if (files.exists(join25(mine, "rig.yaml"))) return mine;
12519
+ const marker = await files.read(join25(mine, "source.json")).catch(() => null);
12268
12520
  const path3 = marker === null ? void 0 : str11(obj9(parse2(marker)).path);
12269
12521
  if (path3) return dirname9(path3);
12270
12522
  const listed2 = await specs();
@@ -12294,7 +12546,7 @@ function createOpenRig(deps = {}) {
12294
12546
  const member = memberOf(show, a.pod, a.from.slice(a.pod.length + 1));
12295
12547
  const fragment2 = member ? fragmentOf(member, a.id, root2) : null;
12296
12548
  if (!fragment2) return { ok: false, commands: [], ...none, error: "rig_root_unknown" };
12297
- const path3 = join24(rigsDir, rigName, "seats", `${a.pod}.${a.id}.json`);
12549
+ const path3 = join25(rigsDir, rigName, "seats", `${a.pod}.${a.id}.json`);
12298
12550
  await files.write(path3, JSON.stringify(fragment2, null, 2));
12299
12551
  commands.push(["add", rig.id, a.pod, path3, ...root2 ? ["--rig-root", root2] : [], "--json"]);
12300
12552
  }
@@ -12344,7 +12596,7 @@ function createOpenRig(deps = {}) {
12344
12596
  const r = await run4(["spec", "show", rig.id, "--json"], STATUS_MS);
12345
12597
  if (r.code !== 0) continue;
12346
12598
  const root2 = rig.name === KERNEL_RIG ? null : await rootOf(rig.name).catch(() => null);
12347
- const specFile = root2 ? join24(root2, "rig.yaml") : null;
12599
+ const specFile = root2 ? join25(root2, "rig.yaml") : null;
12348
12600
  const text7 = specFile ? await roleIo.read(specFile) : null;
12349
12601
  const written = parseSpec(text7);
12350
12602
  const shown = obj9(parse2(r.stdout));
@@ -12508,14 +12760,14 @@ var init_openrig2 = __esm({
12508
12760
  if (!dir) return { code: null, stdout: "", stderr: "rig: command not found" };
12509
12761
  const e = { ...base3, PATH: [dir, base3.PATH].filter(Boolean).join(delimiter2), NO_COLOR: "1", CLICOLOR: "0", FORCE_COLOR: "0" };
12510
12762
  return new Promise((res) => {
12511
- execFile6(join24(dir, "rig"), args, { env: e, timeout: timeoutMs, maxBuffer: 8 * 1024 * 1024 }, (err, stdout, stderr) => {
12763
+ execFile6(join25(dir, "rig"), args, { env: e, timeout: timeoutMs, maxBuffer: 8 * 1024 * 1024 }, (err, stdout, stderr) => {
12512
12764
  const code3 = !err ? 0 : err.code === "ENOENT" ? null : typeof err.code === "number" ? err.code : 1;
12513
12765
  res({ code: code3, stdout: String(stdout), stderr: String(stderr) || (err && code3 !== 0 ? err.message : "") });
12514
12766
  });
12515
12767
  });
12516
12768
  };
12517
12769
  realRoleIo = {
12518
- read: (p) => readFile11(p, "utf8").catch(() => null),
12770
+ read: (p) => readFile12(p, "utf8").catch(() => null),
12519
12771
  stat: (p) => statFile(p).then((st) => ({ size: st.size, mtimeMs: st.mtimeMs, file: st.isFile() })).catch(() => null),
12520
12772
  head: async (p, max) => {
12521
12773
  const h = await openFile(p, "r").catch(() => null);
@@ -12533,7 +12785,7 @@ var init_openrig2 = __esm({
12533
12785
  FILES_TTL_MS = 5 * 6e4;
12534
12786
  HTTP_MS = 3e3;
12535
12787
  realFs = {
12536
- read: (p) => readFile11(p, "utf8"),
12788
+ read: (p) => readFile12(p, "utf8"),
12537
12789
  exists: (p) => existsSync2(p),
12538
12790
  copyDir: (from, to) => cp(from, to, { recursive: true, force: true, filter: (src) => basename11(src) !== "rig.yaml" }),
12539
12791
  write: async (p, text7) => {
@@ -12553,7 +12805,7 @@ function goalOf(tags) {
12553
12805
  return t ? t.slice(GOAL_TAG.length + 1) || null : null;
12554
12806
  }
12555
12807
  function parseTeam(body) {
12556
- const m = BLOCK3.exec(body);
12808
+ const m = BLOCK4.exec(body);
12557
12809
  if (!m) return null;
12558
12810
  let raw;
12559
12811
  try {
@@ -12578,13 +12830,13 @@ function parseTeam(body) {
12578
12830
  launch: kind === "launch" ? { agents: typeof l.agents === "number" && Number.isInteger(l.agents) ? l.agents : null, runtimes: strings(l.runtimes), plan: text2(l.plan), writes: strings(l.writes), permissions: text2(l.permissions) } : null
12579
12831
  };
12580
12832
  }
12581
- var GOAL_TAG, goalTag, BLOCK3, obj10, text2, strings, roleOf3, withoutTeam;
12833
+ var GOAL_TAG, goalTag, BLOCK4, obj10, text2, strings, roleOf3, withoutTeam;
12582
12834
  var init_goalTeam = __esm({
12583
12835
  "src/daemon/goalTeam.ts"() {
12584
12836
  "use strict";
12585
12837
  GOAL_TAG = "ledge-goal";
12586
12838
  goalTag = (goalQitemId2) => `${GOAL_TAG}:${goalQitemId2}`;
12587
- BLOCK3 = /```ledge-team[^\n]*\n([\s\S]*?)```/;
12839
+ BLOCK4 = /```ledge-team[^\n]*\n([\s\S]*?)```/;
12588
12840
  obj10 = (v2) => v2 && typeof v2 === "object" && !Array.isArray(v2) ? v2 : {};
12589
12841
  text2 = (v2) => typeof v2 === "string" && v2.trim() ? v2.trim() : null;
12590
12842
  strings = (v2) => Array.isArray(v2) ? v2.filter((x) => typeof x === "string" && x.trim() !== "").map((x) => x.trim()) : [];
@@ -12592,7 +12844,7 @@ var init_goalTeam = __esm({
12592
12844
  const m = /^([A-Za-z0-9][A-Za-z0-9_]*)-([A-Za-z0-9][A-Za-z0-9_-]*)@/.exec(seat2);
12593
12845
  return m ? `${m[1]}.${m[2]}` : null;
12594
12846
  };
12595
- withoutTeam = (body) => body.replace(BLOCK3, "").trim();
12847
+ withoutTeam = (body) => body.replace(BLOCK4, "").trim();
12596
12848
  }
12597
12849
  });
12598
12850
 
@@ -13217,7 +13469,7 @@ function createRelay(d) {
13217
13469
  const now2 = d.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
13218
13470
  const typing = d.typing ?? /* @__PURE__ */ new Set();
13219
13471
  const sending = /* @__PURE__ */ new Set();
13220
- const open5 = (id) => free(d.store.get(id)) && !typing.has(id) && !d.closing?.has(id);
13472
+ const open6 = (id) => free(d.store.get(id)) && !typing.has(id) && !d.closing?.has(id);
13221
13473
  const emitTo = (thread) => (m) => {
13222
13474
  try {
13223
13475
  d.threads.emitterOf(thread)?.(m);
@@ -13261,14 +13513,14 @@ function createRelay(d) {
13261
13513
  }
13262
13514
  function kick(id) {
13263
13515
  const q = queued().find((x) => x.relay.id === id);
13264
- if (!q || !open5(id)) return;
13516
+ if (!q || !open6(id)) return;
13265
13517
  void deliverAndKick(q.thread, q.qid, q.relay).catch((e) => log(`relay ${q.qid} failed: ${e.message}`));
13266
13518
  }
13267
13519
  async function deliverAndKick(thread, qid, r) {
13268
13520
  const refused = await deliver(thread, qid, r);
13269
13521
  if (refused) {
13270
13522
  if (queued().some((q) => q.relay.id === r.id)) kick(r.id);
13271
- else if (open5(r.id)) d.next?.(r.id);
13523
+ else if (open6(r.id)) d.next?.(r.id);
13272
13524
  }
13273
13525
  return refused;
13274
13526
  }
@@ -13285,7 +13537,7 @@ function createRelay(d) {
13285
13537
  }
13286
13538
  return;
13287
13539
  }
13288
- if (open5(id)) kick(id);
13540
+ if (open6(id)) kick(id);
13289
13541
  } catch (e) {
13290
13542
  log(`relay change handler failed: ${e.message}`);
13291
13543
  }
@@ -13303,7 +13555,7 @@ function createRelay(d) {
13303
13555
  mark(thread, qid, { error: m, code: STOPPED_CODE });
13304
13556
  throw new Error(m);
13305
13557
  }
13306
- if (open5(r.id)) {
13558
+ if (open6(r.id)) {
13307
13559
  if (queued().some((q) => q.relay.id === r.id)) {
13308
13560
  markQueued(thread, qid);
13309
13561
  kick(r.id);
@@ -13324,7 +13576,7 @@ function createRelay(d) {
13324
13576
  settle(id) {
13325
13577
  try {
13326
13578
  if (queued().some((q) => q.relay.id === id)) kick(id);
13327
- else if (open5(id)) d.next?.(id);
13579
+ else if (open6(id)) d.next?.(id);
13328
13580
  } catch (e) {
13329
13581
  log(`relay settle failed: ${e.message}`);
13330
13582
  }
@@ -13553,7 +13805,7 @@ var init_act = __esm({
13553
13805
 
13554
13806
  // src/daemon/handover.ts
13555
13807
  import { mkdirSync, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
13556
- import { join as join25 } from "path";
13808
+ import { join as join26 } from "path";
13557
13809
  function reliefOf(get, s) {
13558
13810
  let cur = s;
13559
13811
  for (let hops = 0; cur && hops < 50; hops++) {
@@ -13571,7 +13823,7 @@ function createHandovers(d) {
13571
13823
  const now2 = d.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
13572
13824
  const briefMs = d.briefMs ?? BRIEF_MS;
13573
13825
  const waiting2 = /* @__PURE__ */ new Map();
13574
- const file = (id) => join25(d.dir, `${id}.md`);
13826
+ const file = (id) => join26(d.dir, `${id}.md`);
13575
13827
  const mark = (id, h) => {
13576
13828
  const cur = d.store.get(id);
13577
13829
  if (cur) d.store.upsert({ ...cur, handover: { ...h, at: now2() } });
@@ -14025,7 +14277,7 @@ var init_broadcast2 = __esm({
14025
14277
  });
14026
14278
 
14027
14279
  // src/daemon/context.ts
14028
- import { open as open4 } from "fs/promises";
14280
+ import { open as open5 } from "fs/promises";
14029
14281
  function longWindow(model) {
14030
14282
  if (!model) return false;
14031
14283
  if (model.includes("[1m]")) return true;
@@ -14033,7 +14285,7 @@ function longWindow(model) {
14033
14285
  return !!m && Number(m[1]) >= 5;
14034
14286
  }
14035
14287
  async function tail(path3) {
14036
- const f = await open4(path3, "r");
14288
+ const f = await open5(path3, "r");
14037
14289
  try {
14038
14290
  const { size } = await f.stat();
14039
14291
  const start = Math.max(0, size - TAIL_BYTES2);
@@ -14117,7 +14369,7 @@ var init_context2 = __esm({
14117
14369
  import { randomUUID as randomUUID8 } from "crypto";
14118
14370
  import { mkdtemp, rm as rm5 } from "fs/promises";
14119
14371
  import { tmpdir } from "os";
14120
- import { join as join26 } from "path";
14372
+ import { join as join27 } from "path";
14121
14373
  function parseRoute(answer, context) {
14122
14374
  const raw = answer.trim().replace(/^```(?:json)?\s*/, "").replace(/\s*```$/, "");
14123
14375
  const result = RouteResultSchema.parse(JSON.parse(raw));
@@ -14143,7 +14395,7 @@ function createRouter(deps) {
14143
14395
  let dir;
14144
14396
  let timer2;
14145
14397
  try {
14146
- dir = await mkdtemp(join26(tmpdir(), "ledge-routing-"));
14398
+ dir = await mkdtemp(join27(tmpdir(), "ledge-routing-"));
14147
14399
  const env = { ...await deps.env(), LEDGE_CHAT: "1" };
14148
14400
  delete env.ANTHROPIC_API_KEY;
14149
14401
  delete env.LEDGE_SESSION;
@@ -15173,15 +15425,15 @@ var init_operations = __esm({
15173
15425
  });
15174
15426
 
15175
15427
  // src/mobile/risk.ts
15176
- import { homedir as homedir12 } from "os";
15177
- import { resolve as resolve11, sep as sep5, join as join27 } from "path";
15428
+ import { homedir as homedir13 } from "os";
15429
+ import { resolve as resolve11, sep as sep5, join as join28 } from "path";
15178
15430
  function isSensitive(cwd, p) {
15179
15431
  if (p.includes("~")) return true;
15180
15432
  const full = resolve11(cwd, p);
15181
15433
  const segs = full.split(sep5);
15182
15434
  if (segs.includes(".git") || segs.some((s) => s.startsWith(".env"))) return true;
15183
- const home2 = homedir12();
15184
- return [join27(home2, ".ssh"), join27(home2, ".ledge")].some((d) => full === d || full.startsWith(d + sep5));
15435
+ const home2 = homedir13();
15436
+ return [join28(home2, ".ssh"), join28(home2, ".ledge")].some((d) => full === d || full.startsWith(d + sep5));
15185
15437
  }
15186
15438
  function classifyShell(command, cwd) {
15187
15439
  if (PUSH.test(command)) return high("pushes");
@@ -15394,7 +15646,7 @@ function toPhoneItem(item) {
15394
15646
  break;
15395
15647
  }
15396
15648
  case "permission":
15397
- out = { kind: "permission", ...base3, tool: item.tool, text: text7(item.text), ...pickSet(item, ["decision"]) };
15649
+ out = { kind: "permission", ...base3, tool: item.tool, text: text7(item.text), ...pickSet(item, ["decision", "by"]) };
15398
15650
  break;
15399
15651
  case "helper":
15400
15652
  out = { kind: "helper", ...base3, task: text7(item.task), state: item.state, ...pickSet(item, ["agentType"]) };
@@ -15752,6 +16004,15 @@ function createPhoneGateway(deps) {
15752
16004
  if (!p) return "off";
15753
16005
  return key !== void 0 && p.key !== key ? "stale" : "on";
15754
16006
  };
16007
+ const ownInfo = (deviceId) => {
16008
+ const d = mobile.deviceInfo?.(deviceId);
16009
+ return d ? { kind: d.kind, ...d.name ? { name: d.name } : {} } : {};
16010
+ };
16011
+ const decidedBy = (deviceId) => {
16012
+ const d = mobile.deviceInfo?.(deviceId);
16013
+ const name = (d?.name ?? d?.label ?? "").trim();
16014
+ return { kind: "device", id: deviceId, name: name ? clip(name, 60).text : "Appareil" };
16015
+ };
15755
16016
  const snapshot = (deviceId) => {
15756
16017
  const push3 = pushStatus(deviceId);
15757
16018
  const f = followers.get(deviceId);
@@ -15776,7 +16037,7 @@ function createPhoneGateway(deps) {
15776
16037
  ...deps.openrig ? { openrig: true } : {},
15777
16038
  broadcast: true
15778
16039
  },
15779
- device: { confirm: deps.credential(deviceId) !== null, push: push3, ...deps.chats ? { chat: true } : {}, ...deps.contextNear ? { contextNear: deps.contextNear() } : {} }
16040
+ device: { confirm: deps.credential(deviceId) !== null, push: push3, ...deps.chats ? { chat: true } : {}, ...deps.contextNear ? { contextNear: deps.contextNear() } : {}, id: deviceId, ...ownInfo(deviceId) }
15780
16041
  };
15781
16042
  return deps.models ? { ...message, daemon: { ...message.daemon, models: true } } : message;
15782
16043
  };
@@ -15906,10 +16167,10 @@ function createPhoneGateway(deps) {
15906
16167
  return { status: "rejected", reason: s.state === "exited" ? "ended" : "not_launched" };
15907
16168
  }
15908
16169
  };
15909
- const local = (msg) => {
16170
+ const local = (deviceId, msg) => {
15910
16171
  switch (msg.type) {
15911
16172
  case "session.decide":
15912
- return { type: "session.decide", id: msg.id, decision: msg.decision, ...msg.note !== void 0 ? { note: msg.note } : {}, expect: { kind: msg.expect.kind, text: msg.expect.text, key: msg.expect.key } };
16173
+ return { type: "session.decide", id: msg.id, decision: msg.decision, ...msg.note !== void 0 ? { note: msg.note } : {}, expect: { kind: msg.expect.kind, text: msg.expect.text, key: msg.expect.key }, by: decidedBy(deviceId) };
15913
16174
  case "session.reply":
15914
16175
  return { type: "session.reply", id: msg.id, text: msg.text, ...msg.attachments?.length ? { attachments: msg.attachments } : {} };
15915
16176
  case "session.answer":
@@ -16285,9 +16546,14 @@ function createPhoneGateway(deps) {
16285
16546
  }
16286
16547
  if (msg.type === "credential.register") return register(deviceId, msg);
16287
16548
  if (msg.type === "device.forget") return { status: "applied" };
16549
+ if (msg.type === "device.rename") {
16550
+ if (!mobile.rename) return { status: "rejected", reason: "unavailable" };
16551
+ mobile.rename(deviceId, msg.name);
16552
+ return { status: "applied" };
16553
+ }
16288
16554
  if (msg.type === "push.subscribe") {
16289
16555
  if (!pushEndpointAllowed(msg.subscription.endpoint)) return { status: "rejected", reason: "invalid" };
16290
- mobile.setPush(deviceId, { endpoint: msg.subscription.endpoint, p256dh: msg.subscription.keys.p256dh, auth: msg.subscription.keys.auth, key: msg.key });
16556
+ mobile.setPush(deviceId, { endpoint: msg.subscription.endpoint, p256dh: msg.subscription.keys.p256dh, auth: msg.subscription.keys.auth, key: msg.key, ...msg.resolved ? { resolved: true } : {} });
16291
16557
  return { status: "applied" };
16292
16558
  }
16293
16559
  if (msg.type === "push.unsubscribe") {
@@ -16306,7 +16572,7 @@ function createPhoneGateway(deps) {
16306
16572
  if (msg.type === "openrig.answer") return rigAnswer(deviceId, msg);
16307
16573
  const refused = precheck(deviceId, msg);
16308
16574
  if (refused) return refused;
16309
- const out = local(msg);
16575
+ const out = local(deviceId, msg);
16310
16576
  if (out.type === "session.model") return modelChange(out);
16311
16577
  let error;
16312
16578
  await deps.dispatch(out, (m) => {
@@ -16332,7 +16598,7 @@ function createPhoneGateway(deps) {
16332
16598
  }
16333
16599
  registry.finish(msg.op, outcome.status, outcome.reason, outcome.how, outcome.report, outcome.detail);
16334
16600
  result(deviceId, msg.op, outcome.status, outcome.reason, outcome.how, outcome.report);
16335
- if (msg.type === "credential.register" && outcome.status === "applied" && approved(deviceId)) push2(deviceId, snapshot(deviceId), "snapshot");
16601
+ if ((msg.type === "credential.register" || msg.type === "device.rename") && outcome.status === "applied" && approved(deviceId)) push2(deviceId, snapshot(deviceId), "snapshot");
16336
16602
  if (msg.type === "device.forget" && outcome.status === "applied") {
16337
16603
  let timer2;
16338
16604
  await Promise.race([outbox.flush(deviceId), new Promise((r) => {
@@ -16828,6 +17094,12 @@ function pushFor(before2, after2, all2) {
16828
17094
  }
16829
17095
  return null;
16830
17096
  }
17097
+ function resolvedFor(before2, after2, all2, by) {
17098
+ if (!before2?.pending || after2.pending) return null;
17099
+ const badge = Math.min(999, all2.filter((s) => s.pending).length);
17100
+ const body = !by ? "" : by.kind === "pc" ? "sur le PC" : cut(`sur ${by.name}`, BODY_MAX2);
17101
+ return { v: 1, kind: "resolved", title: cut(`${after2.name} \xB7 trait\xE9e`, TITLE_MAX), body, url: `/#/thread/${encodeURIComponent(after2.id)}`, badge, tag: safeTag(after2.id) };
17102
+ }
16831
17103
  function chatEndPayload(code3, badge) {
16832
17104
  return {
16833
17105
  v: 1,
@@ -16884,6 +17156,15 @@ function createNotifier(deps) {
16884
17156
  const { mobile, log: log2 } = deps;
16885
17157
  const now2 = deps.now ?? Date.now;
16886
17158
  const lastFinished = /* @__PURE__ */ new Map();
17159
+ const told = /* @__PURE__ */ new Map();
17160
+ const tell = (id, tag) => {
17161
+ let s = told.get(id);
17162
+ if (!s) {
17163
+ s = /* @__PURE__ */ new Set();
17164
+ told.set(id, s);
17165
+ }
17166
+ s.add(tag);
17167
+ };
16887
17168
  return {
16888
17169
  async notify(payload, sessionId2, only) {
16889
17170
  if (!mobile.state().enabled) return;
@@ -16908,6 +17189,7 @@ function createNotifier(deps) {
16908
17189
  if (only !== void 0 && d.id !== only) return [];
16909
17190
  const sub = mobile.pushOf(d.id);
16910
17191
  if (!sub || relayKey !== void 0 && sub.key !== relayKey || deps.presence(d.id)) return [];
17192
+ if (p.kind === "resolved" && (!sub.resolved || !told.get(d.id)?.has(p.tag))) return [];
16911
17193
  return [{ id: d.id, online: d.online, sub }];
16912
17194
  });
16913
17195
  if (targets.length === 0) return;
@@ -16918,10 +17200,12 @@ function createNotifier(deps) {
16918
17200
  endpoint: sub.endpoint,
16919
17201
  body: Buffer.from(encryptPush(plain4, { p256dh: sub.p256dh, auth: sub.auth }, { padTo: PAD_TO })).toString("base64url"),
16920
17202
  ttl: TTL_S,
16921
- urgency: p.kind === "finished" || p.kind === "chat" ? "normal" : "high",
17203
+ urgency: p.kind === "finished" || p.kind === "chat" || p.kind === "resolved" ? "normal" : "high",
16922
17204
  topic: p.tag
16923
17205
  };
16924
17206
  const r = await mobile.sendPush(msg);
17207
+ if (p.kind === "resolved") told.get(id)?.delete(p.tag);
17208
+ else if ((p.kind === "request" || p.kind === "question") && r.result === "sent") tell(id, p.tag);
16925
17209
  if (r.result === "gone") {
16926
17210
  log2(`push: subscription of device ${id} is gone, removed`);
16927
17211
  mobile.setPush(id, null);
@@ -16951,9 +17235,13 @@ var init_notifier = __esm({
16951
17235
  // src/daemon/server.ts
16952
17236
  import { createServer } from "http";
16953
17237
  import { randomUUID as randomUUID10 } from "crypto";
16954
- import { homedir as homedir13 } from "os";
16955
- import { basename as basename12, join as join28, resolve as resolve12 } from "path";
17238
+ import { homedir as homedir14 } from "os";
17239
+ import { basename as basename12, join as join29, resolve as resolve12 } from "path";
16956
17240
  import { WebSocketServer, WebSocket as WebSocket3 } from "ws";
17241
+ function withoutHarness(s) {
17242
+ const { pending: _p, lastReply: _r, transcriptPath: _t, agentSessionId: _a, agentSessionFile: _f, model: _m, effort: _e, modelNext: _n, queue: _q, backend: _b, ...rest } = s;
17243
+ return { ...rest, helpers: [] };
17244
+ }
16957
17245
  function resumeKey(s) {
16958
17246
  if (s.tool === "omp") return s.agentSessionFile;
16959
17247
  if (s.agentSessionId) return s.agentSessionId;
@@ -17003,11 +17291,12 @@ async function startServer(opts) {
17003
17291
  const agentEnv2 = opts.agentEnv ?? (async () => ({ ...process.env, ...await loginShellEnv() }));
17004
17292
  const attachments = opts.attachments ?? new AttachmentStore(paths().attachments);
17005
17293
  const reducer = new CaptureReducer();
17006
- const threads = createChatThreads({ attachments, store, tmux, home: paths().dir, notesDir: join28(paths().dir, "context"), userHome: opts.home, env: agentEnv2, bin: opts.claudeBin, now: now2, projects: () => projects, forge: createForge({ env: agentEnv2 }), tasks: () => tasks.all() });
17294
+ const threads = createChatThreads({ attachments, store, tmux, home: paths().dir, notesDir: join29(paths().dir, "context"), userHome: opts.home, env: agentEnv2, bin: opts.claudeBin, now: now2, projects: () => projects, forge: createForge({ env: agentEnv2 }), tasks: () => tasks.all() });
17007
17295
  const chat = opts.chat ?? threads.desktop;
17008
17296
  const router = createRouter({ env: agentEnv2 });
17009
- const omp2 = opts.omp ?? createOmpHost({ store, env: agentEnv2, dir: join28(paths().dir, "omp"), now: now2, attachments });
17297
+ const omp2 = opts.omp ?? createOmpHost({ store, env: agentEnv2, dir: join29(paths().dir, "omp"), now: now2, attachments });
17010
17298
  const openrig = opts.openrig ?? createOpenRig({ env: agentEnv2 });
17299
+ const ompSeatReadOnly = (s) => new Refusal(`${s.name} is the OpenRig OMP seat ${s.rig?.seat ?? ""}: Ledge follows its state only`, "read_only");
17011
17300
  const rigOwned = (s) => new Refusal(`${s.name} is the OpenRig seat ${s.rig?.seat ?? s.tmuxSession ?? ""}: stop or restart it from OpenRig`, "read_only");
17012
17301
  const adoptSeat = (id, seat2) => {
17013
17302
  const session = store.get(id);
@@ -17020,9 +17309,22 @@ async function startServer(opts) {
17020
17309
  const seatId = (seat2) => `rig-${seat2.replace(/[^A-Za-z0-9._-]/g, "-")}`;
17021
17310
  async function discoverSeats(st) {
17022
17311
  if (st.daemon.state !== "running") return;
17023
- const followed = new Set(store.all().filter((x) => x.rig && x.state !== "exited").map((x) => x.rig.seat));
17312
+ const followed = new Map(store.all().filter((x) => x.rig && x.state !== "exited").map((x) => [x.rig.seat, x.id]));
17024
17313
  for (const r of st.rigs) for (const n of r.nodes) {
17025
- if (n.runtime !== "claude-code" && n.runtime !== "codex" || n.sessionStatus !== "running" || followed.has(n.session)) continue;
17314
+ const tool = SEAT_TOOLS[n.runtime];
17315
+ if (!tool || n.sessionStatus !== "running") continue;
17316
+ const live3 = followed.get(n.session);
17317
+ if (live3 !== void 0) {
17318
+ const s = store.get(live3);
17319
+ if (s && s.tool !== tool && (s.source === "rig" || s.source === "hook_only")) {
17320
+ if (s.source === "hook_only" && !await tmux.hasSession(n.session).catch(() => false)) continue;
17321
+ const cur = store.get(live3);
17322
+ if (!cur || cur.state === "exited" || cur.tool === tool) continue;
17323
+ store.upsert({ ...withoutHarness(cur), tool, state: "idle", source: "rig", tmuxSession: n.session, lastActivityAt: now2() });
17324
+ log(`openrig: seat ${n.session} now runs ${n.runtime}`);
17325
+ }
17326
+ continue;
17327
+ }
17026
17328
  if (!await tmux.hasSession(n.session).catch(() => false)) continue;
17027
17329
  const before2 = store.all().filter((x) => x.rig?.seat === n.session).sort((a, b3) => b3.lastActivityAt.localeCompare(a.lastActivityAt))[0];
17028
17330
  const id = before2?.id ?? seatId(n.session), old = store.get(id), at = now2();
@@ -17031,13 +17333,14 @@ async function startServer(opts) {
17031
17333
  if (old) {
17032
17334
  if (old.archivedAt) store.unarchive(id, at);
17033
17335
  const { exitedAt: _e, exitCode: _c, pending: _p, ...rest } = store.get(id);
17034
- store.upsert({ ...rest, state: "idle", tmuxSession: n.session, source: "rig", rig, lastActivityAt: at });
17336
+ const same2 = rest.tool === tool ? rest : { ...withoutHarness(rest), tool };
17337
+ store.upsert({ ...same2, state: "idle", tmuxSession: n.session, source: "rig", rig, lastActivityAt: at });
17035
17338
  } else {
17036
17339
  const name = pickName([...namesInUse(store.all()), ...heldNames(store.all())]);
17037
17340
  store.upsert({
17038
17341
  id,
17039
17342
  name,
17040
- tool: n.runtime === "codex" ? "codex" : "claude",
17343
+ tool,
17041
17344
  cwd,
17042
17345
  project: basename12(cwd) || r.name,
17043
17346
  title: `${name} \xB7 ${n.logicalId}`,
@@ -17051,10 +17354,21 @@ async function startServer(opts) {
17051
17354
  });
17052
17355
  log(`openrig: seat ${n.session} followed as ${name}`);
17053
17356
  }
17054
- followed.add(n.session);
17357
+ followed.set(n.session, id);
17358
+ }
17359
+ if (!st.rigs.some((r) => r.nodes.length > 0)) return;
17360
+ const listed2 = new Set(st.rigs.flatMap((r) => r.nodes.map((n) => n.session)));
17361
+ const unsure = new Set(st.rigs.filter((r) => r.seats > 0 && r.nodes.length === 0).map((r) => r.name));
17362
+ for (const s of store.all()) {
17363
+ if (isOmpSeat2(s) && s.rig && s.state === "exited" && !isArchived(s) && !listed2.has(s.rig.seat) && !unsure.has(s.rig.name)) {
17364
+ store.archive(s.id, now2());
17365
+ log(`openrig: seat ${s.rig.seat} is gone from OpenRig`);
17366
+ }
17055
17367
  }
17056
17368
  }
17057
17369
  const SEATS_EVERY_MS = 3e4;
17370
+ const ompSeats = opts.ompSeats ?? createOmpSeats({ store });
17371
+ ompSeats.start();
17058
17372
  const seatTimer = setInterval(() => {
17059
17373
  void openrig.state().then(discoverSeats).catch((e) => log(`openrig: seats not read: ${e.message}`));
17060
17374
  }, SEATS_EVERY_MS);
@@ -17100,7 +17414,7 @@ async function startServer(opts) {
17100
17414
  }).catch((e) => log(`openrig: seat ${seat2} not read: ${e.message}`));
17101
17415
  };
17102
17416
  const commands = opts.commands ?? createCommands({ env: agentEnv2, ...opts.claudeBin ? { claudeBin: opts.claudeBin } : {} });
17103
- const models = opts.models ?? createModels({ env: agentEnv2, omp: async () => ompChoices(await omp2.catalog(opts.home ?? homedir13())) });
17417
+ const models = opts.models ?? createModels({ env: agentEnv2, omp: async () => ompChoices(await omp2.catalog(opts.home ?? homedir14())) });
17104
17418
  const token = await ensureDaemonToken();
17105
17419
  const httpServer = createServer(unauthorized);
17106
17420
  const sockets = /* @__PURE__ */ new Set();
@@ -17140,7 +17454,7 @@ async function startServer(opts) {
17140
17454
  const islandFile = opts.islandFile ?? paths().island;
17141
17455
  let island = readIsland(islandFile);
17142
17456
  const islandMsg = () => ({ type: "island", prefs: island });
17143
- const contextFile = opts.contextFile ?? join28(paths().dir, "context.json");
17457
+ const contextFile = opts.contextFile ?? join29(paths().dir, "context.json");
17144
17458
  let contextPrefs = readContextPrefs(contextFile);
17145
17459
  const contextMsg = () => ({ type: "context.prefs", prefs: contextPrefs });
17146
17460
  const mobile = opts.mobile;
@@ -17197,7 +17511,7 @@ async function startServer(opts) {
17197
17511
  }
17198
17512
  });
17199
17513
  const terminalHolders = /* @__PURE__ */ new Map();
17200
- const holdersMsg = () => ({ type: "terminal.holders", holds: [...terminalHolders].map(([id, deviceId]) => ({ id, device: mobile?.state().devices.find((d) => d.id === deviceId)?.label ?? deviceId })) });
17514
+ const holdersMsg = () => ({ type: "terminal.holders", holds: [...terminalHolders].map(([id, deviceId]) => ({ id, device: ((d) => d?.name ?? d?.label)(mobile?.state().devices.find((d) => d.id === deviceId)) ?? deviceId })) });
17201
17515
  const typing = /* @__PURE__ */ new Set();
17202
17516
  const closing = /* @__PURE__ */ new Set();
17203
17517
  const cardThreads = opts.chat ? {
@@ -17239,7 +17553,7 @@ async function startServer(opts) {
17239
17553
  tasks,
17240
17554
  launch: (req) => launch3(req),
17241
17555
  close: (s) => closeSession(s),
17242
- dir: join28(paths().dir, "handovers"),
17556
+ dir: join29(paths().dir, "handovers"),
17243
17557
  now: now2,
17244
17558
  ...opts.handoverBriefMs !== void 0 ? { briefMs: opts.handoverBriefMs } : {}
17245
17559
  });
@@ -17346,9 +17660,9 @@ async function startServer(opts) {
17346
17660
  if (!a || a.settled || !answeredBy(a.pending, ev) || !cur || pendingKey(cur) !== pendingKey(a.pending)) return;
17347
17661
  a.settled = true;
17348
17662
  a.byHook = true;
17349
- store.apply(id, { name: "Decided", at: now2(), source: "daemon", ...a.decision ? { decision: a.decision } : {} });
17663
+ store.apply(id, { name: "Decided", at: now2(), source: "daemon", ...a.decision ? { decision: a.decision } : {}, ...a.by ? { by: a.by } : {} });
17350
17664
  }
17351
- async function injectAndVerify(id, planFor, requirePrompt, decision) {
17665
+ async function injectAndVerify(id, planFor, requirePrompt, decision, by) {
17352
17666
  const s = store.get(id);
17353
17667
  if (!s) throw new Refusal(`unknown session ${id}`, "unknown_session");
17354
17668
  if (inFlight.has(id)) throw new Refusal("a decision is already in progress", "busy");
@@ -17374,7 +17688,7 @@ async function startServer(opts) {
17374
17688
  const plan2 = typeof planFor === "function" ? planFor(before2 ?? "") : planFor;
17375
17689
  const stillShown = (p) => decided && adapter.promptGone ? !adapter.promptGone(p, decided) : adapter.promptStillVisible(bottomLines(p));
17376
17690
  const tool = s.tool === "claude" && decided?.source === "hook" ? pendingTool(decided) : void 0;
17377
- const answer = tool ? { pending: decided, ...decision ? { decision } : {}, settled: false, byHook: false } : void 0;
17691
+ const answer = tool ? { pending: decided, ...decision ? { decision } : {}, ...by ? { by } : {}, settled: false, byHook: false } : void 0;
17378
17692
  const transcript = tool && !decided.helperId && s.transcriptPath ? s.transcriptPath : void 0;
17379
17693
  const call = transcript ? claudeOpenCall(transcript, tool) : void 0;
17380
17694
  const promptGone3 = async () => {
@@ -17395,7 +17709,7 @@ async function startServer(opts) {
17395
17709
  if (e instanceof NoteNotTyped) {
17396
17710
  if (await promptGone3()) {
17397
17711
  if (answer) answer.settled = true;
17398
- if (!answer?.byHook && stillTheSame()) store.apply(id, { name: "Decided", at: now2(), source: "daemon", ...decision ? { decision } : {} });
17712
+ if (!answer?.byHook && stillTheSame()) store.apply(id, { name: "Decided", at: now2(), source: "daemon", ...decision ? { decision } : {}, ...by ? { by } : {} });
17399
17713
  throw e;
17400
17714
  }
17401
17715
  e = new Refusal(NOT_TAKEN, "not_taken");
@@ -17409,7 +17723,7 @@ async function startServer(opts) {
17409
17723
  if (answer?.byHook) return true;
17410
17724
  if (!stillTheSame()) return false;
17411
17725
  if (gone) {
17412
- store.apply(id, { name: "Decided", at: now2(), source: "daemon", ...decision ? { decision } : {} });
17726
+ store.apply(id, { name: "Decided", at: now2(), source: "daemon", ...decision ? { decision } : {}, ...by ? { by } : {} });
17413
17727
  return true;
17414
17728
  }
17415
17729
  store.apply(id, { name: "InjectFailed", at: now2(), source: "daemon", text: NOT_TAKEN });
@@ -17515,6 +17829,7 @@ async function startServer(opts) {
17515
17829
  const s = store.get(id);
17516
17830
  if (!s) return unknownSession();
17517
17831
  if (s.source === "hook_only") return readOnly();
17832
+ if (isOmpSeat2(s)) return ompSeatReadOnly(s);
17518
17833
  if (closing.has(id)) return new Refusal("the session is being closed", "busy");
17519
17834
  const ids = how.attachments ?? [];
17520
17835
  const refs = [];
@@ -17848,7 +18163,7 @@ async function startServer(opts) {
17848
18163
  let scanning = null;
17849
18164
  function sharedScan() {
17850
18165
  if (scanning) return scanning;
17851
- const home2 = opts.home ?? homedir13();
18166
+ const home2 = opts.home ?? homedir14();
17852
18167
  const extra = extraRoots(home2, [...store.recents().map((r) => r.cwd), ...store.all().filter((s) => s.state !== "exited").map((s) => s.cwd)]);
17853
18168
  const walk = (opts.scan ?? scanRepos)({ home: home2, extra }).finally(() => {
17854
18169
  if (scanning === walk) scanning = null;
@@ -18103,7 +18418,7 @@ async function startServer(opts) {
18103
18418
  if (msg.decision === "always" && !offers(s.tool, pending.kind, "always")) return refuse(new Refusal(NOT_OFFERED, "not_offered"), msg.id);
18104
18419
  if (msg.note !== void 0 && (msg.decision !== "deny" || !offers(s.tool, pending.kind, "note"))) return refuse(new Refusal(NOT_OFFERED, "not_offered"), msg.id);
18105
18420
  const { decision, note } = msg;
18106
- await injectAndVerify(msg.id, (pane) => adapter.keysFor(decision, pending, pane, note), true, decision);
18421
+ await injectAndVerify(msg.id, (pane) => adapter.keysFor(decision, pending, pane, note), true, decision, msg.by ?? { kind: "pc" });
18107
18422
  return;
18108
18423
  }
18109
18424
  case "session.reply": {
@@ -18127,6 +18442,7 @@ async function startServer(opts) {
18127
18442
  return void await omp2.interruptAndRestore(s.id).then(({ entries, imagesDropped }) => reply({ type: "session.queue.restored", id: s.id, texts: entries.map((e) => e.text), entries, ...imagesDropped ? { imagesDropped } : {} }), (e) => refuse(e.message, msg.id));
18128
18443
  }
18129
18444
  if (!s?.tmuxSession) return;
18445
+ if (isOmpSeat2(s)) return refuse(ompSeatReadOnly(s), msg.id);
18130
18446
  await tmux.sendKeys(s.tmuxSession, adapterFor(s.tool).interruptKeys());
18131
18447
  return;
18132
18448
  }
@@ -18357,7 +18673,7 @@ async function startServer(opts) {
18357
18673
  relay,
18358
18674
  launchChat: (thread, qid, rows) => {
18359
18675
  const c = thread === "desktop" ? threads.desktop : threads.device(thread.slice("device:".length));
18360
- const userHome = resolve12(opts.home ?? homedir13());
18676
+ const userHome = resolve12(opts.home ?? homedir14());
18361
18677
  return launchFromChat({
18362
18678
  chat: c,
18363
18679
  emit: threads.emitterOf(thread) ?? (() => {
@@ -18438,10 +18754,22 @@ async function startServer(opts) {
18438
18754
  const badge = store.all().filter((s) => s.pending).length + tasks.all().filter(openDecision).length;
18439
18755
  void notifier.notify(decisionPayload(task, badge), void 0).catch((e) => log(`push failed (${e instanceof Error ? e.name : "error"})`));
18440
18756
  };
18441
- unsubscribePush = store.onApply((before2, after2) => {
18757
+ const answeredBy2 = /* @__PURE__ */ new Map();
18758
+ const unsubscribeDecided = store.onEvent((id, ev) => {
18759
+ if (ev.name === "Decided") answeredBy2.set(id, ev.by ?? null);
18760
+ });
18761
+ const unsubscribeApply = store.onApply((before2, after2) => {
18762
+ const failed = (e) => log(`push failed (${e instanceof Error ? e.name : "error"})`);
18763
+ const r = resolvedFor(before2, after2, store.all(), answeredBy2.get(after2.id));
18764
+ if (before2?.pending && !after2.pending) answeredBy2.delete(after2.id);
18765
+ if (r) void notifier.notify(r, after2.id).catch(failed);
18442
18766
  const p = pushFor(before2, after2, store.all());
18443
- if (p) void notifier.notify(p, after2.id).catch((e) => log(`push failed (${e instanceof Error ? e.name : "error"})`));
18767
+ if (p) void notifier.notify(p, after2.id).catch(failed);
18444
18768
  });
18769
+ unsubscribePush = () => {
18770
+ unsubscribeDecided();
18771
+ unsubscribeApply();
18772
+ };
18445
18773
  }
18446
18774
  wss.on("connection", (ws, req) => {
18447
18775
  ws.on("error", (e) => log(`socket error: ${e.message}`));
@@ -18501,6 +18829,7 @@ async function startServer(opts) {
18501
18829
  close: () => new Promise((res) => {
18502
18830
  clearInterval(sweepTimer);
18503
18831
  clearInterval(seatTimer);
18832
+ ompSeats.stop();
18504
18833
  human.stop();
18505
18834
  if (goalsTimer) clearTimeout(goalsTimer);
18506
18835
  void reducer.close();
@@ -18520,7 +18849,7 @@ async function startServer(opts) {
18520
18849
  })
18521
18850
  };
18522
18851
  }
18523
- var CONTINUE_ARGS, RESUME_ARGS, MULTI_DIR_ARGS, MODEL_ARGS, EFFORT_ARGS, NoteNotTyped, DELETED_TTL_MS, RESPAWN_HOOKS_MS, ALLOWED_ORIGINS;
18852
+ var SEAT_TOOLS, CONTINUE_ARGS, RESUME_ARGS, MULTI_DIR_ARGS, MODEL_ARGS, EFFORT_ARGS, NoteNotTyped, DELETED_TTL_MS, RESPAWN_HOOKS_MS, ALLOWED_ORIGINS;
18524
18853
  var init_server = __esm({
18525
18854
  "src/daemon/server.ts"() {
18526
18855
  "use strict";
@@ -18540,6 +18869,7 @@ var init_server = __esm({
18540
18869
  init_spawn();
18541
18870
  init_host();
18542
18871
  init_catalog();
18872
+ init_seats();
18543
18873
  init_repos2();
18544
18874
  init_scan2();
18545
18875
  init_folders();
@@ -18578,6 +18908,7 @@ var init_server = __esm({
18578
18908
  init_challenges();
18579
18909
  init_webauthn();
18580
18910
  init_frames();
18911
+ SEAT_TOOLS = { "claude-code": "claude", codex: "codex", omp: "omp" };
18581
18912
  CONTINUE_ARGS = { claude: ["--continue"], codex: ["resume", "--last"], cursor: ["--continue"] };
18582
18913
  RESUME_ARGS = { claude: (id) => ["--resume", id], codex: (id) => ["resume", id], cursor: (id) => ["--resume", id] };
18583
18914
  MULTI_DIR_ARGS = { claude: (dirs) => ["--add-dir", ...dirs] };
@@ -18606,7 +18937,7 @@ var init_server = __esm({
18606
18937
  // src/tmux.ts
18607
18938
  import { execFile as execFile7 } from "child_process";
18608
18939
  import { existsSync as existsSync3 } from "fs";
18609
- import { join as join29 } from "path";
18940
+ import { join as join30 } from "path";
18610
18941
  function runFile(file, args, env, opts) {
18611
18942
  return new Promise((resolve13) => {
18612
18943
  execFile7(file, args, { maxBuffer: 4 * 1024 * 1024, env, ...opts?.timeoutMs ? { timeout: opts.timeoutMs, killSignal: "SIGTERM" } : {} }, (err, stdout, stderr) => {
@@ -18616,7 +18947,7 @@ function runFile(file, args, env, opts) {
18616
18947
  });
18617
18948
  }
18618
18949
  function systemdAvailable(env = process.env, exists = existsSync3) {
18619
- return !!env.XDG_RUNTIME_DIR && exists(join29(env.XDG_RUNTIME_DIR, "systemd", "private"));
18950
+ return !!env.XDG_RUNTIME_DIR && exists(join30(env.XDG_RUNTIME_DIR, "systemd", "private"));
18620
18951
  }
18621
18952
  function newSessionCommand(tmuxArgs, scoped, unit) {
18622
18953
  if (!scoped) return { file: "tmux", args: tmuxArgs };
@@ -18749,10 +19080,10 @@ __export(setup_exports, {
18749
19080
  systemdUnit: () => systemdUnit,
18750
19081
  writeBin: () => writeBin
18751
19082
  });
18752
- import { readFile as readFile12, writeFile as writeFile14, mkdir as mkdir12, rename as rename5, realpath as realpath2, rm as rm6, stat as stat6, lstat, readlink } from "fs/promises";
19083
+ import { readFile as readFile13, writeFile as writeFile14, mkdir as mkdir12, rename as rename5, realpath as realpath2, rm as rm6, stat as stat7, lstat, readlink } from "fs/promises";
18753
19084
  import { execFile as execFile8 } from "child_process";
18754
- import { homedir as homedir14, userInfo } from "os";
18755
- import { join as join30, dirname as dirname10 } from "path";
19085
+ import { homedir as homedir15, userInfo } from "os";
19086
+ import { join as join31, dirname as dirname10 } from "path";
18756
19087
  function mergeHooks(settings, events, command, file) {
18757
19088
  if (settings === void 0) settings = {};
18758
19089
  if (!settings || typeof settings !== "object" || Array.isArray(settings)) throw new Error(`${file} root is not an object, not modified`);
@@ -18800,7 +19131,7 @@ function mergeCursorHooks(config, command = "ledge hook cursor") {
18800
19131
  async function mergeJsonFile(path3, merge, name) {
18801
19132
  let raw = null;
18802
19133
  try {
18803
- raw = await readFile12(path3, "utf8");
19134
+ raw = await readFile13(path3, "utf8");
18804
19135
  } catch (e) {
18805
19136
  if (e.code !== "ENOENT") throw e;
18806
19137
  }
@@ -18836,7 +19167,7 @@ WantedBy=default.target
18836
19167
  async function writeIfChanged(path3, content3) {
18837
19168
  let current2 = null;
18838
19169
  try {
18839
- current2 = await readFile12(path3, "utf8");
19170
+ current2 = await readFile13(path3, "utf8");
18840
19171
  } catch (e) {
18841
19172
  if (e.code !== "ENOENT") throw e;
18842
19173
  }
@@ -18850,7 +19181,7 @@ async function writeIfChanged(path3, content3) {
18850
19181
  async function stopDetachedDaemon(pidFile) {
18851
19182
  let pid;
18852
19183
  try {
18853
- pid = Number((await readFile12(pidFile, "utf8")).trim());
19184
+ pid = Number((await readFile13(pidFile, "utf8")).trim());
18854
19185
  } catch {
18855
19186
  return null;
18856
19187
  }
@@ -18880,10 +19211,10 @@ async function stopDetachedDaemon(pidFile) {
18880
19211
  async function installService(opts = {}) {
18881
19212
  const out = [];
18882
19213
  const run4 = opts.run ?? execRunner;
18883
- const unitDir = opts.unitDir ?? join30(homedir14(), ".config", "systemd", "user");
19214
+ const unitDir = opts.unitDir ?? join31(homedir15(), ".config", "systemd", "user");
18884
19215
  const node2 = opts.node ?? await realpath2(process.execPath);
18885
19216
  const script = opts.script ?? await realpath2(process.argv[1]);
18886
- const changed = await writeIfChanged(join30(unitDir, SERVICE), systemdUnit({ node: node2, script }));
19217
+ const changed = await writeIfChanged(join31(unitDir, SERVICE), systemdUnit({ node: node2, script }));
18887
19218
  const reload = await run4("systemctl", ["--user", "daemon-reload"]);
18888
19219
  if (reload.code !== 0) throw new Error(`systemd user session unavailable (systemctl --user daemon-reload failed: ${reload.stderr.trim() || reload.code}); the daemon will still be started on demand by ledge claude`);
18889
19220
  const active = (await run4("systemctl", ["--user", "is-active", SERVICE])).code === 0;
@@ -18912,7 +19243,7 @@ async function installService(opts = {}) {
18912
19243
  }
18913
19244
  async function isDir(path3) {
18914
19245
  try {
18915
- return (await stat6(path3)).isDirectory();
19246
+ return (await stat7(path3)).isDirectory();
18916
19247
  } catch {
18917
19248
  return false;
18918
19249
  }
@@ -18921,7 +19252,7 @@ async function writeBin(opts) {
18921
19252
  const st = await lstat(opts.link).catch(() => null);
18922
19253
  const was = st?.isSymbolicLink() ? await readlink(opts.link) : null;
18923
19254
  if (was !== null && !was.endsWith("/dist/cli/index.js")) return null;
18924
- const current2 = st && was === null && st.isFile() ? await readFile12(opts.link, "utf8").catch(() => "") : "";
19255
+ const current2 = st && was === null && st.isFile() ? await readFile13(opts.link, "utf8").catch(() => "") : "";
18925
19256
  if (st && was === null && !current2.startsWith(BIN_MARK)) return null;
18926
19257
  const script = `${BIN_MARK}: the hooks run \`ledge\` wherever node is not on the PATH.
18927
19258
  exec ${shQuote(opts.node)} ${shQuote(opts.target)} "$@"
@@ -18937,24 +19268,24 @@ async function runSetup(opts = {}) {
18937
19268
  const out = [];
18938
19269
  await ensureLedgeDir();
18939
19270
  out.push("~/.ledge ready (tmux.conf written if missing)");
18940
- const settingsPath2 = opts.settingsPath ?? join30(homedir14(), ".claude", "settings.json");
19271
+ const settingsPath2 = opts.settingsPath ?? join31(homedir15(), ".claude", "settings.json");
18941
19272
  if (await mergeJsonFile(settingsPath2, mergeClaudeHooks, "settings.json")) out.push(`Claude Code hooks added in ${settingsPath2}`);
18942
19273
  else out.push("Claude Code hooks already present");
18943
- const codexDir = opts.codexDir ?? process.env.CODEX_HOME ?? join30(homedir14(), ".codex");
19274
+ const codexDir = opts.codexDir ?? process.env.CODEX_HOME ?? join31(homedir15(), ".codex");
18944
19275
  if (!await isDir(codexDir)) out.push(`Codex not found (${codexDir} missing), Codex hooks skipped`);
18945
- else if (await mergeJsonFile(join30(codexDir, "hooks.json"), mergeCodexHooks, "hooks.json")) {
18946
- out.push(`Codex hooks added in ${join30(codexDir, "hooks.json")}; Codex asks to review them on its next start: trust them there or from Ledge`);
19276
+ else if (await mergeJsonFile(join31(codexDir, "hooks.json"), mergeCodexHooks, "hooks.json")) {
19277
+ out.push(`Codex hooks added in ${join31(codexDir, "hooks.json")}; Codex asks to review them on its next start: trust them there or from Ledge`);
18947
19278
  } else out.push("Codex hooks already present");
18948
- const cursorDir = opts.cursorDir ?? join30(homedir14(), ".cursor");
19279
+ const cursorDir = opts.cursorDir ?? join31(homedir15(), ".cursor");
18949
19280
  if (!await isDir(cursorDir)) out.push(`Cursor Agent not found (${cursorDir} missing), Cursor Agent hooks skipped`);
18950
- else if (await mergeJsonFile(join30(cursorDir, "hooks.json"), mergeCursorHooks, "hooks.json")) out.push(`Cursor Agent hooks added in ${join30(cursorDir, "hooks.json")}`);
19281
+ else if (await mergeJsonFile(join31(cursorDir, "hooks.json"), mergeCursorHooks, "hooks.json")) out.push(`Cursor Agent hooks added in ${join31(cursorDir, "hooks.json")}`);
18951
19282
  else out.push("Cursor Agent hooks already present");
18952
19283
  if (opts.service ?? true) {
18953
19284
  const cli = opts.cli ?? process.argv[1] ?? "";
18954
19285
  if (cli.endsWith(".ts")) out.push("systemd service skipped: run setup from the built CLI (pnpm --filter @paradow/ledge build, then ledge setup)");
18955
19286
  else {
18956
19287
  const script = await realpath2(cli);
18957
- const line2 = await writeBin({ link: opts.link ?? join30(homedir14(), ".local", "bin", "ledge"), node: process.execPath, target: script });
19288
+ const line2 = await writeBin({ link: opts.link ?? join31(homedir15(), ".local", "bin", "ledge"), node: process.execPath, target: script });
18958
19289
  if (line2) out.push(line2);
18959
19290
  out.push(...await installService({ script, unitDir: opts.unitDir, run: opts.run }));
18960
19291
  }
@@ -19016,10 +19347,10 @@ var init_setup = __esm({
19016
19347
  });
19017
19348
 
19018
19349
  // src/update/guard.ts
19019
- import { readFile as readFile13 } from "fs/promises";
19350
+ import { readFile as readFile14 } from "fs/promises";
19020
19351
  async function tmuxServerAtRisk(opts = {}) {
19021
19352
  const runTmux = opts.runTmux ?? run2;
19022
- const readCgroup = opts.readCgroup ?? ((pid2) => readFile13(`/proc/${pid2}/cgroup`, "utf8"));
19353
+ const readCgroup = opts.readCgroup ?? ((pid2) => readFile14(`/proc/${pid2}/cgroup`, "utf8"));
19023
19354
  const r = await runTmux(["-L", opts.socket ?? "ledge", "list-sessions", "-F", "#{pid}"]);
19024
19355
  if (r.code !== 0) return false;
19025
19356
  const pid = r.stdout.split("\n").map((s) => s.trim()).find(Boolean);
@@ -19042,12 +19373,12 @@ var init_guard = __esm({
19042
19373
 
19043
19374
  // src/update/self.ts
19044
19375
  import { existsSync as existsSync4 } from "fs";
19045
- import { readFile as readFile14, realpath as realpath3 } from "fs/promises";
19046
- import { homedir as homedir15 } from "os";
19047
- import { dirname as dirname11, join as join31 } from "path";
19376
+ import { readFile as readFile15, realpath as realpath3 } from "fs/promises";
19377
+ import { homedir as homedir16 } from "os";
19378
+ import { dirname as dirname11, join as join32 } from "path";
19048
19379
  function gitRoot(script, exists = existsSync4) {
19049
19380
  for (let dir = dirname11(script); ; dir = dirname11(dir)) {
19050
- if (exists(join31(dir, ".git"))) return dir;
19381
+ if (exists(join32(dir, ".git"))) return dir;
19051
19382
  if (dirname11(dir) === dir) return null;
19052
19383
  }
19053
19384
  }
@@ -19057,10 +19388,10 @@ function installKind(script, exists = existsSync4) {
19057
19388
  }
19058
19389
  async function daemonInfo() {
19059
19390
  const script = await realpath3(process.argv[1] ?? "").catch(() => process.argv[1] ?? "");
19060
- const cgroup = await readFile14("/proc/self/cgroup", "utf8").catch(() => "");
19391
+ const cgroup = await readFile15("/proc/self/cgroup", "utf8").catch(() => "");
19061
19392
  const install = installKind(script);
19062
19393
  const root2 = install === "git" ? gitRoot(script) : null;
19063
- return { version: LEDGE_VERSION, install, systemd: underSystemd(process.env, cgroup), home: homedir15(), ...root2 ? { manual: manualCommand(root2) } : {} };
19394
+ return { version: LEDGE_VERSION, install, systemd: underSystemd(process.env, cgroup), home: homedir16(), ...root2 ? { manual: manualCommand(root2) } : {} };
19064
19395
  }
19065
19396
  var underSystemd, manualCommand;
19066
19397
  var init_self = __esm({
@@ -19074,7 +19405,7 @@ var init_self = __esm({
19074
19405
  });
19075
19406
 
19076
19407
  // src/update/run-update.ts
19077
- import { dirname as dirname12, join as join32 } from "path";
19408
+ import { dirname as dirname12, join as join33 } from "path";
19078
19409
  function createDaemonUpdater(deps) {
19079
19410
  let busy = false;
19080
19411
  let restarting = false;
@@ -19163,7 +19494,7 @@ var init_run_update = __esm({
19163
19494
  PACKAGE = "@paradow/ledge";
19164
19495
  NPM_TIMEOUT_MS = 3 * 6e4;
19165
19496
  RESTART_COMMAND = "systemctl --user restart ledge-daemon";
19166
- npmCommand = (execPath, version) => ({ file: join32(dirname12(execPath), "npm"), args: ["i", "-g", `${PACKAGE}@${version}`] });
19497
+ npmCommand = (execPath, version) => ({ file: join33(dirname12(execPath), "npm"), args: ["i", "-g", `${PACKAGE}@${version}`] });
19167
19498
  lastLines = (output, n = 3) => output.split("\n").map((l) => l.trimEnd()).filter(Boolean).slice(-n).join("\n");
19168
19499
  npmEnv = (execPath, env) => ({ ...env, PATH: `${dirname12(execPath)}:${env.PATH ?? ""}` });
19169
19500
  }
@@ -19565,7 +19896,7 @@ function readRevocations() {
19565
19896
  function writeRevocations(list3) {
19566
19897
  writePrivate(paths().revocations, JSON.stringify(z31.array(RevocationSchema).parse(list3), null, 2) + "\n");
19567
19898
  }
19568
- var RelaySessionSchema, DeviceCredentialSchema, ApprovedDeviceSchema, DEFAULT_CONFIG, LEGACY_RELAY_URL, RevocationSchema;
19899
+ var RelaySessionSchema, DeviceCredentialSchema, MAX_DEVICE_PEERS, ApprovedDeviceSchema, DEFAULT_CONFIG, LEGACY_RELAY_URL, RevocationSchema;
19569
19900
  var init_files2 = __esm({
19570
19901
  "src/mobile/files.ts"() {
19571
19902
  "use strict";
@@ -19582,6 +19913,7 @@ var init_files2 = __esm({
19582
19913
  rpId: z31.string().min(1),
19583
19914
  transports: z31.array(z31.string()).optional()
19584
19915
  });
19916
+ MAX_DEVICE_PEERS = 8;
19585
19917
  ApprovedDeviceSchema = z31.object({
19586
19918
  id: z31.string().min(1),
19587
19919
  publicKey: z31.string().min(1),
@@ -19590,8 +19922,14 @@ var init_files2 = __esm({
19590
19922
  lastPeer: z31.string().optional(),
19591
19923
  lastSeenAt: z31.string().optional(),
19592
19924
  credential: DeviceCredentialSchema.optional(),
19593
- /** The phone's push subscription and the relay's VAPID key it was made with. */
19594
- push: z31.object({ endpoint: z31.string().min(1), p256dh: z31.string().min(1), auth: z31.string().min(1), key: z31.string().min(1) }).optional()
19925
+ /** The phone's push subscription and the relay's VAPID key it was made with; `resolved`: its service worker shows a `resolved` push. */
19926
+ push: z31.object({ endpoint: z31.string().min(1), p256dh: z31.string().min(1), auth: z31.string().min(1), key: z31.string().min(1), resolved: z31.literal(true).optional() }).optional(),
19927
+ /** Phone or tablet, from the pairing label; absent on a device approved before it was kept. */
19928
+ kind: DeviceKindSchema.optional(),
19929
+ /** The name the user gave it; absent means the default name the fronts derive. */
19930
+ name: DeviceNameSchema.optional(),
19931
+ /** Every relay peer that confirmed a channel for it, newest last (at most MAX_DEVICE_PEERS): all are revoked with it. */
19932
+ peers: z31.array(z31.string().min(1)).max(MAX_DEVICE_PEERS).optional()
19595
19933
  });
19596
19934
  DEFAULT_CONFIG = () => ({ enabled: false, relayUrl: DEFAULT_RELAY_URL });
19597
19935
  LEGACY_RELAY_URL = "https://server.ledge.paradow.dev";
@@ -20235,6 +20573,10 @@ var init_channels = __esm({
20235
20573
  });
20236
20574
 
20237
20575
  // src/mobile/service.ts
20576
+ function peersOf(d) {
20577
+ const list3 = d.peers ?? [];
20578
+ return d.lastPeer && !list3.includes(d.lastPeer) ? [...list3, d.lastPeer] : list3;
20579
+ }
20238
20580
  async function createMobileService(opts = {}) {
20239
20581
  const now2 = opts.now ?? Date.now;
20240
20582
  const fetchImpl = opts.fetchImpl ?? fetch;
@@ -20265,6 +20607,13 @@ async function createMobileService(opts = {}) {
20265
20607
  const sendFrame = (peer, frame) => {
20266
20608
  link3?.send({ type: "send", to: peer, body: JSON.stringify(frame) });
20267
20609
  };
20610
+ const withPeer = (d, peer) => [...peersOf(d).filter((p) => p !== peer), peer].slice(-MAX_DEVICE_PEERS);
20611
+ const withoutPeer = (d, peer) => {
20612
+ if (d.lastPeer !== peer && !d.peers?.includes(peer)) return d;
20613
+ const { lastPeer, peers, ...rest } = d;
20614
+ const left2 = (peers ?? []).filter((p) => p !== peer);
20615
+ return { ...rest, ...lastPeer && lastPeer !== peer ? { lastPeer } : {}, ...left2.length > 0 ? { peers: left2 } : {} };
20616
+ };
20268
20617
  const saveDevices = (next4) => {
20269
20618
  writeDevices(next4);
20270
20619
  devices = next4;
@@ -20274,7 +20623,8 @@ async function createMobileService(opts = {}) {
20274
20623
  send: sendFrame,
20275
20624
  approve(d) {
20276
20625
  const old = devices.find((x) => x.id === d.id);
20277
- saveDevices(old ? devices.map((x) => x.id === d.id ? { ...x, label: d.label } : x) : [...devices, d]);
20626
+ const kind = deviceKindOfLabel(d.label);
20627
+ saveDevices(old ? devices.map((x) => x.id === d.id ? { ...x, label: d.label, kind } : x) : [...devices, { ...d, kind }]);
20278
20628
  },
20279
20629
  onChange: changed,
20280
20630
  now: now2,
@@ -20288,7 +20638,7 @@ async function createMobileService(opts = {}) {
20288
20638
  if (devices.some((d) => d.id === deviceId)) {
20289
20639
  const at = new Date(now2()).toISOString();
20290
20640
  try {
20291
- saveDevices(devices.map((d) => d.id === deviceId ? { ...d, lastPeer: peer, lastSeenAt: at } : d));
20641
+ saveDevices(devices.map((d) => d.id === deviceId ? { ...d, lastPeer: peer, lastSeenAt: at, peers: withPeer(d, peer) } : withoutPeer(d, peer)));
20292
20642
  } catch (e) {
20293
20643
  log(`mobile: devices.json not written (${e.message})`);
20294
20644
  }
@@ -20491,9 +20841,17 @@ async function createMobileService(opts = {}) {
20491
20841
  saveDevices(devices.filter((x) => x.id !== id));
20492
20842
  log(`mobile: revoked device ${id}`);
20493
20843
  channels.dropDevice(id);
20494
- if (d.lastPeer) sendFrame(d.lastPeer, { k: "denied" });
20844
+ const others = new Set(devices.flatMap(peersOf));
20845
+ const peers = peersOf(d).filter((p) => !others.has(p));
20846
+ for (const p of peers) sendFrame(p, { k: "denied" });
20847
+ changed();
20848
+ await Promise.all(peers.map((p) => revokeOnRelay(p)));
20849
+ };
20850
+ const rename7 = (id, raw) => {
20851
+ const name = DeviceNameSchema.parse(raw);
20852
+ if (!devices.some((d) => d.id === id)) throw new Error("no such device");
20853
+ saveDevices(devices.map((d) => d.id === id ? { ...d, name } : d));
20495
20854
  changed();
20496
- if (d.lastPeer) await revokeOnRelay(d.lastPeer);
20497
20855
  };
20498
20856
  const pairingIn = (id, states) => {
20499
20857
  const p = pairing.views().find((v2) => v2.id === id);
@@ -20515,13 +20873,20 @@ async function createMobileService(opts = {}) {
20515
20873
  label: d.label,
20516
20874
  approvedAt: d.approvedAt,
20517
20875
  online: online.has(d.id),
20518
- ...d.lastSeenAt ? { lastSeenAt: d.lastSeenAt } : {}
20876
+ ...d.lastSeenAt ? { lastSeenAt: d.lastSeenAt } : {},
20877
+ kind: d.kind ?? deviceKindOfLabel(d.label),
20878
+ ...d.name ? { name: d.name } : {}
20519
20879
  })),
20520
20880
  pairings: pairing.views(),
20521
20881
  terminalInput: config.terminalInput === true
20522
20882
  };
20523
20883
  },
20524
20884
  revoke,
20885
+ rename: rename7,
20886
+ deviceInfo(deviceId) {
20887
+ const d = devices.find((x) => x.id === deviceId);
20888
+ return d ? { label: d.label, kind: d.kind ?? deviceKindOfLabel(d.label), ...d.name ? { name: d.name } : {} } : null;
20889
+ },
20525
20890
  credential(deviceId) {
20526
20891
  const c = devices.find((d) => d.id === deviceId)?.credential;
20527
20892
  return c && c.rpId === rpOf(config.relayUrl)?.rpId ? c : null;
@@ -20622,17 +20987,24 @@ async function createMobileService(opts = {}) {
20622
20987
  case "mobile.retry":
20623
20988
  link3?.retryNow();
20624
20989
  return;
20625
- case "mobile.pairing.approve":
20990
+ case "mobile.pairing.approve": {
20626
20991
  pairingIn(msg.id, ["confirm"]);
20627
20992
  if (linkState.state !== "online") throw new Error("the relay is offline");
20993
+ const before2 = new Set(devices.map((d) => d.id));
20628
20994
  pairing.approve(msg.id);
20995
+ const added = devices.some((d) => !before2.has(d.id));
20996
+ if (msg.replaces !== void 0 && added && before2.has(msg.replaces)) await revoke(msg.replaces);
20629
20997
  return;
20998
+ }
20630
20999
  case "mobile.pairing.deny":
20631
21000
  pairingIn(msg.id, ["confirm", "waiting"]);
20632
21001
  pairing.deny(msg.id);
20633
21002
  return;
20634
21003
  case "mobile.device.revoke":
20635
21004
  return revoke(msg.id);
21005
+ case "mobile.device.rename":
21006
+ rename7(msg.id, msg.name);
21007
+ return;
20636
21008
  }
20637
21009
  },
20638
21010
  send: (deviceId, text7) => channels.send(deviceId, text7),
@@ -20677,7 +21049,7 @@ __export(daemon_exports, {
20677
21049
  startDaemon: () => startDaemon
20678
21050
  });
20679
21051
  import { writeFile as writeFile15, rm as rm7 } from "fs/promises";
20680
- import { join as join33 } from "path";
21052
+ import { join as join34 } from "path";
20681
21053
  async function startDaemon(opts = {}) {
20682
21054
  await ensureLedgeDir();
20683
21055
  const store = new Store();
@@ -20694,7 +21066,7 @@ async function startDaemon(opts = {}) {
20694
21066
  }
20695
21067
  for (const s of store.all()) {
20696
21068
  if (backendOf(s) !== "omp-rpc") continue;
20697
- await reapOwner(join33(paths().dir, "omp", s.id)).catch(() => false);
21069
+ await reapOwner(join34(paths().dir, "omp", s.id)).catch(() => false);
20698
21070
  if (s.state === "exited") continue;
20699
21071
  const at = (/* @__PURE__ */ new Date()).toISOString();
20700
21072
  store.apply(s.id, { name: "StopFailure", at, source: "daemon", text: "Ledge restarted: the OMP process ended with it" });
@@ -20781,9 +21153,9 @@ __export(uninstall_exports, {
20781
21153
  runUninstall: () => runUninstall,
20782
21154
  uninstallArgs: () => uninstallArgs
20783
21155
  });
20784
- import { readFile as readFile15, writeFile as writeFile16, rename as rename6, rm as rm8, lstat as lstat2, realpath as realpath4 } from "fs/promises";
20785
- import { homedir as homedir16 } from "os";
20786
- import { join as join34 } from "path";
21156
+ import { readFile as readFile16, writeFile as writeFile16, rename as rename6, rm as rm8, lstat as lstat2, realpath as realpath4 } from "fs/promises";
21157
+ import { homedir as homedir17 } from "os";
21158
+ import { join as join35 } from "path";
20787
21159
  function prune(config, file, keep) {
20788
21160
  if (!isObject(config)) throw new Error(`${file} root is not an object, not modified`);
20789
21161
  if (config.hooks === void 0) return { settings: config, removed: 0 };
@@ -20820,7 +21192,7 @@ function removeCursorHooks(config) {
20820
21192
  async function pruneJsonFile(path3, remove, name) {
20821
21193
  let raw;
20822
21194
  try {
20823
- raw = await readFile15(path3, "utf8");
21195
+ raw = await readFile16(path3, "utf8");
20824
21196
  } catch (e) {
20825
21197
  if (e.code === "ENOENT") return null;
20826
21198
  throw e;
@@ -20841,7 +21213,7 @@ async function pruneJsonFile(path3, remove, name) {
20841
21213
  async function removeService(opts = {}) {
20842
21214
  const out = [];
20843
21215
  const run4 = opts.run ?? execRunner;
20844
- const unit = join34(opts.unitDir ?? join34(homedir16(), ".config", "systemd", "user"), SERVICE);
21216
+ const unit = join35(opts.unitDir ?? join35(homedir17(), ".config", "systemd", "user"), SERVICE);
20845
21217
  let present = true;
20846
21218
  try {
20847
21219
  await lstat2(unit);
@@ -20866,7 +21238,7 @@ async function runningSessions(run4, socket) {
20866
21238
  async function removeLink(link3, script) {
20867
21239
  try {
20868
21240
  const st = await lstat2(link3);
20869
- if (st.isFile() ? !(await readFile15(link3, "utf8")).startsWith(BIN_MARK) : !st.isSymbolicLink() || await realpath4(link3) !== await realpath4(script)) return false;
21241
+ if (st.isFile() ? !(await readFile16(link3, "utf8")).startsWith(BIN_MARK) : !st.isSymbolicLink() || await realpath4(link3) !== await realpath4(script)) return false;
20870
21242
  } catch {
20871
21243
  return false;
20872
21244
  }
@@ -20889,9 +21261,9 @@ async function runUninstall(opts = {}) {
20889
21261
  };
20890
21262
  const run4 = opts.run ?? execRunner;
20891
21263
  const files = [
20892
- ["Claude Code", opts.settingsPath ?? join34(homedir16(), ".claude", "settings.json"), (c) => removeClaudeHooks(c, "settings.json")],
20893
- ["Codex", join34(opts.codexDir ?? process.env.CODEX_HOME ?? join34(homedir16(), ".codex"), "hooks.json"), (c) => removeClaudeHooks(c, "hooks.json")],
20894
- ["Cursor Agent", join34(opts.cursorDir ?? join34(homedir16(), ".cursor"), "hooks.json"), removeCursorHooks]
21264
+ ["Claude Code", opts.settingsPath ?? join35(homedir17(), ".claude", "settings.json"), (c) => removeClaudeHooks(c, "settings.json")],
21265
+ ["Codex", join35(opts.codexDir ?? process.env.CODEX_HOME ?? join35(homedir17(), ".codex"), "hooks.json"), (c) => removeClaudeHooks(c, "hooks.json")],
21266
+ ["Cursor Agent", join35(opts.cursorDir ?? join35(homedir17(), ".cursor"), "hooks.json"), removeCursorHooks]
20895
21267
  ];
20896
21268
  for (const [agent, path3, remove] of files) {
20897
21269
  await step3(async () => {
@@ -20913,7 +21285,7 @@ async function runUninstall(opts = {}) {
20913
21285
  });
20914
21286
  else lines.push(`${dir} kept (session journal and log): \`ledge uninstall --purge\` removes it`);
20915
21287
  const script = opts.script ?? process.argv[1];
20916
- const link3 = opts.link ?? join34(homedir16(), ".local", "bin", "ledge");
21288
+ const link3 = opts.link ?? join35(homedir17(), ".local", "bin", "ledge");
20917
21289
  if (script) await step3(async () => {
20918
21290
  if (await removeLink(link3, script)) lines.push(`${link3} removed`);
20919
21291
  });
@@ -21171,10 +21543,10 @@ function createDaemonClient(opts) {
21171
21543
  };
21172
21544
  const schedule = () => {
21173
21545
  if (closed) return;
21174
- timer2 = setTimeout(open5, backoff);
21546
+ timer2 = setTimeout(open6, backoff);
21175
21547
  backoff = Math.min(max, backoff * 2);
21176
21548
  };
21177
- function open5() {
21549
+ function open6() {
21178
21550
  if (closed) return;
21179
21551
  setStatus(status === "open" ? "connecting" : status);
21180
21552
  if (!token) {
@@ -21238,7 +21610,7 @@ function createDaemonClient(opts) {
21238
21610
  ws = null;
21239
21611
  old?.close();
21240
21612
  setStatus("connecting");
21241
- open5();
21613
+ open6();
21242
21614
  },
21243
21615
  close: () => {
21244
21616
  closed = true;
@@ -23011,7 +23383,7 @@ __export(launch_exports, {
23011
23383
  parseLaunchArgs: () => parseLaunchArgs
23012
23384
  });
23013
23385
  import { spawn as spawn3 } from "child_process";
23014
- import { readFile as readFile16, rm as rm9 } from "fs/promises";
23386
+ import { readFile as readFile17, rm as rm9 } from "fs/promises";
23015
23387
  import { createConnection } from "net";
23016
23388
  function parseLaunchArgs(argv) {
23017
23389
  const args = [];
@@ -23124,7 +23496,7 @@ async function readExitCode(id) {
23124
23496
  const file = exitFile(id);
23125
23497
  let raw = null;
23126
23498
  try {
23127
- raw = await readFile16(file, "utf8");
23499
+ raw = await readFile17(file, "utf8");
23128
23500
  } catch {
23129
23501
  }
23130
23502
  await rm9(file, { force: true });
@@ -23201,6 +23573,7 @@ var init_launch2 = __esm({
23201
23573
  import { spawn as spawn4 } from "child_process";
23202
23574
  function attachCommand(fleet, s) {
23203
23575
  if (s.offline) return { why: "offline" };
23576
+ if (isStoppedOmpSeat(s)) return { why: "stopped" };
23204
23577
  if (backendOf(s) === "omp-rpc") return { why: "omp" };
23205
23578
  if (!s.tmuxSession) return { why: "none" };
23206
23579
  const argv = fleet.terminal(s.id);
@@ -23223,9 +23596,9 @@ var init_attach = __esm({
23223
23596
 
23224
23597
  // src/tui/clipboard.ts
23225
23598
  import { execFile as execFile9 } from "child_process";
23226
- import { mkdtemp as mkdtemp2, readFile as readFile17, rm as rm10 } from "fs/promises";
23599
+ import { mkdtemp as mkdtemp2, readFile as readFile18, rm as rm10 } from "fs/promises";
23227
23600
  import { tmpdir as tmpdir2 } from "os";
23228
- import { join as join35 } from "path";
23601
+ import { join as join36 } from "path";
23229
23602
  async function fromWindows(run4) {
23230
23603
  const { stdout } = await run4(POWERSHELL, ["-NoProfile", "-STA", "-Command", SCRIPT], { cwd: "/mnt/c", maxBuffer: MAX_BUFFER, encoding: "buffer" });
23231
23604
  const line2 = stdout.toString("utf8").trim();
@@ -23250,15 +23623,15 @@ async function fromLinux(run4) {
23250
23623
  return null;
23251
23624
  }
23252
23625
  async function fromMac(run4) {
23253
- const dir = await mkdtemp2(join35(tmpdir2(), "ledge-clip-"));
23254
- const file = join35(dir, "clip.png");
23626
+ const dir = await mkdtemp2(join36(tmpdir2(), "ledge-clip-"));
23627
+ const file = join36(dir, "clip.png");
23255
23628
  try {
23256
23629
  const script = `set f to open for access POSIX file "${file}" with write permission
23257
23630
  set eof f to 0
23258
23631
  write (the clipboard as \xABclass PNGf\xBB) to f
23259
23632
  close access f`;
23260
23633
  await run4("osascript", ["-e", script], { maxBuffer: MAX_BUFFER, encoding: "buffer" });
23261
- return fromBytes(await readFile17(file));
23634
+ return fromBytes(await readFile18(file));
23262
23635
  } catch {
23263
23636
  return null;
23264
23637
  } finally {
@@ -23689,7 +24062,7 @@ var init_fr = __esm({
23689
24062
  plural = (n, one2, many) => n > 1 ? many : one2;
23690
24063
  fr3 = {
23691
24064
  lang: { fr: "Fran\xE7ais", en: "English" },
23692
- state: { approval: "Votre accord", question: "Une question", starting: "D\xE9marrage", idle: "Disponible", working: "Au travail", done: "Termin\xE9e", error: "Erreur", readonly: "Lecture seule" },
24065
+ state: { approval: "Votre accord", question: "Une question", starting: "D\xE9marrage", idle: "Disponible", working: "Au travail", done: "Termin\xE9e", error: "Erreur", readonly: "Lecture seule", stopped: "Arr\xEAt\xE9" },
23693
24066
  time: {
23694
24067
  now: "\xE0 l\u2019instant",
23695
24068
  decimal: ",",
@@ -23712,6 +24085,7 @@ var init_fr = __esm({
23712
24085
  openImage: "ouvrir l'image",
23713
24086
  navigate: "naviguer",
23714
24087
  open: "ouvrir",
24088
+ attachSeat: "attacher son terminal",
23715
24089
  answer: "r\xE9pondre",
23716
24090
  attach: "attacher (tmux)",
23717
24091
  ask: "demander \xE0 Ledge",
@@ -23786,6 +24160,7 @@ var init_fr = __esm({
23786
24160
  noPreview: "Pas d\u2019aper\xE7u.",
23787
24161
  noExchange: "Aucun \xE9change.",
23788
24162
  sessions: (n) => `${n} ${plural(n, "session", "sessions")}`,
24163
+ sessionsOf: (live3, all2) => `${live3} ${plural(live3, "session", "sessions")} sur ${all2}`,
23789
24164
  outside: "Hors projet",
23790
24165
  offline: "hors ligne",
23791
24166
  readOnly: "lecture seule",
@@ -24009,6 +24384,7 @@ var init_fr = __esm({
24009
24384
  noteTooLong: (max) => `Note trop longue : ${max} caract\xE8res au plus. Elle reste dans le champ.`,
24010
24385
  noNote: (n) => `${n} ne prend pas de note ici.`,
24011
24386
  noTerminalOmp: (n) => `${n} est pilot\xE9 par Ledge via le protocole d\u2019OMP : pas de terminal \xE0 attacher.`,
24387
+ seatStopped: (n) => `${n} est arr\xEAt\xE9 : son terminal n\u2019existe plus. 6 ouvre OpenRig, o\xF9 est son rig.`,
24012
24388
  noTerminal: (n) => `Pas de terminal pour ${n}.`,
24013
24389
  attachFailed: (code3) => `tmux s\u2019est arr\xEAt\xE9 avec le code ${code3}.`,
24014
24390
  modelSwitch: "Ce daemon ne change pas le mod\xE8le d\u2019une session en cours.",
@@ -24257,7 +24633,7 @@ var init_en = __esm({
24257
24633
  plural2 = (n, one2, many) => n > 1 ? many : one2;
24258
24634
  en3 = {
24259
24635
  lang: { fr: "Fran\xE7ais", en: "English" },
24260
- state: { approval: "Needs you", question: "A question", starting: "Starting", idle: "Available", working: "Working", done: "Finished", error: "Error", readonly: "Read-only" },
24636
+ state: { approval: "Needs you", question: "A question", starting: "Starting", idle: "Available", working: "Working", done: "Finished", error: "Error", readonly: "Read-only", stopped: "Stopped" },
24261
24637
  time: { now: "just now", decimal: ".", s: (n) => `${n}s`, min: (n) => `${n} min`, h: (n) => `${n} h`, d: (n) => `${n} d` },
24262
24638
  tabs: { overview: "Overview", session: "Session", ask: "Ask Ledge", launch: "New session", settings: "Settings", openrig: "OpenRig" },
24263
24639
  bar: {
@@ -24273,6 +24649,7 @@ var init_en = __esm({
24273
24649
  openImage: "open image",
24274
24650
  navigate: "move",
24275
24651
  open: "open",
24652
+ attachSeat: "attach its terminal",
24276
24653
  answer: "answer",
24277
24654
  attach: "attach (tmux)",
24278
24655
  ask: "ask Ledge",
@@ -24346,6 +24723,7 @@ var init_en = __esm({
24346
24723
  noPreview: "No preview.",
24347
24724
  noExchange: "No message yet.",
24348
24725
  sessions: (n) => `${n} ${plural2(n, "session", "sessions")}`,
24726
+ sessionsOf: (live3, all2) => `${live3} ${live3 === 1 ? "session" : "sessions"} of ${all2}`,
24349
24727
  outside: "No project",
24350
24728
  offline: "offline",
24351
24729
  readOnly: "read-only",
@@ -24565,6 +24943,7 @@ var init_en = __esm({
24565
24943
  noteTooLong: (max) => `Note too long: ${max} characters at most. It stays in the field.`,
24566
24944
  noNote: (n) => `${n} takes no note here.`,
24567
24945
  noTerminalOmp: (n) => `${n} is driven by Ledge over OMP's protocol: there is no terminal to attach to.`,
24946
+ seatStopped: (n) => `${n} is stopped: its terminal is gone. 6 opens OpenRig, where its rig is.`,
24568
24947
  noTerminal: (n) => `No terminal for ${n}.`,
24569
24948
  attachFailed: (code3) => `tmux stopped with code ${code3}.`,
24570
24949
  modelSwitch: "This daemon cannot change a running session\u2019s model.",
@@ -24829,15 +25208,19 @@ function overview(sessions, projects) {
24829
25208
  const known2 = new Map((projects?.projects ?? []).map((p) => [p.id, p]));
24830
25209
  for (const p of projects?.projects ?? []) nodes.set(`p:${p.id}`, { key: `p:${p.id}`, label: p.name, color: p.color, branches: [], count: 0 });
24831
25210
  const rigNodes = /* @__PURE__ */ new Map();
25211
+ const rigNodeOf = (rig) => {
25212
+ const kind = rig === "kernel" ? "kernel" : "rig";
25213
+ const key = kind === "kernel" ? "g:kernel" : `g:rig:${rig}`;
25214
+ let node2 = rigNodes.get(key);
25215
+ if (!node2) {
25216
+ node2 = { key, label: kind === "kernel" ? "Kernel" : rig, kind, branches: [{ branch: "", sessions: [] }], count: 0 };
25217
+ rigNodes.set(key, node2);
25218
+ }
25219
+ return node2;
25220
+ };
24832
25221
  for (const s of live3) {
24833
25222
  if (s.rig) {
24834
- const kind = s.rig.name === "kernel" ? "kernel" : "rig";
24835
- const key2 = kind === "kernel" ? "g:kernel" : `g:rig:${s.rig.name}`;
24836
- let node3 = rigNodes.get(key2);
24837
- if (!node3) {
24838
- node3 = { key: key2, label: kind === "kernel" ? "Kernel" : s.rig.name, kind, branches: [{ branch: "", sessions: [] }], count: 0 };
24839
- rigNodes.set(key2, node3);
24840
- }
25223
+ const node3 = rigNodeOf(s.rig.name);
24841
25224
  node3.branches[0].sessions.push(s);
24842
25225
  node3.count += 1;
24843
25226
  continue;
@@ -24858,6 +25241,12 @@ function overview(sessions, projects) {
24858
25241
  row3.sessions.push(s);
24859
25242
  node2.count += 1;
24860
25243
  }
25244
+ for (const s of sessions) {
25245
+ if (!isStoppedOmpSeat(s) || !s.rig) continue;
25246
+ const node2 = rigNodeOf(s.rig.name);
25247
+ node2.branches[0].sessions.push(s);
25248
+ node2.shown = (node2.shown ?? node2.count) + 1;
25249
+ }
24861
25250
  const groups = [...rigNodes.values()].sort((a, b3) => a.kind === b3.kind ? a.label.localeCompare(b3.label) : a.kind === "kernel" ? 1 : -1);
24862
25251
  const tree2 = [...[...nodes.values()].filter((n) => n.count > 0), ...groups];
24863
25252
  const order2 = tree2.flatMap((n) => n.branches.flatMap((b3) => b3.sessions.map((s) => s.id)));
@@ -25118,7 +25507,7 @@ var init_theme = __esm({
25118
25507
 
25119
25508
  // src/tui/model/cells.ts
25120
25509
  import stringWidth from "string-width";
25121
- function join36(line2, a) {
25510
+ function join37(line2, a) {
25122
25511
  const out = [];
25123
25512
  for (const c of a) {
25124
25513
  const last = out[out.length - 1];
@@ -25133,7 +25522,7 @@ function cut2(line2, width, ellipsis = "\u2026") {
25133
25522
  const a = split2(line2), room = width - cells(ellipsis);
25134
25523
  let n = 0, i = 0;
25135
25524
  while (i < a.length && n + a[i].w <= room) n += a[i++].w;
25136
- const kept = join36(line2, a.slice(0, i));
25525
+ const kept = join37(line2, a.slice(0, i));
25137
25526
  const color2 = line2[a[Math.max(0, i - 1)]?.seg ?? 0]?.color;
25138
25527
  return [...kept, { text: ellipsis, ...color2 ? { color: color2 } : {} }];
25139
25528
  }
@@ -25152,7 +25541,7 @@ function wrapSegs(line2, width, first = width) {
25152
25541
  }
25153
25542
  w = Math.max(4, width);
25154
25543
  if (j >= a.length || a[j].g === "\n") {
25155
- out.push(join36(line2, a.slice(i, j)));
25544
+ out.push(join37(line2, a.slice(i, j)));
25156
25545
  i = j + 1;
25157
25546
  if (j >= a.length) break;
25158
25547
  if (i >= a.length) out.push([]);
@@ -25161,7 +25550,7 @@ function wrapSegs(line2, width, first = width) {
25161
25550
  const end2 = a[j].g === " " ? j : space > i ? space : Math.max(j, i + 1);
25162
25551
  let k = end2;
25163
25552
  while (k > i && a[k - 1].g === " ") k--;
25164
- out.push(join36(line2, a.slice(i, k)));
25553
+ out.push(join37(line2, a.slice(i, k)));
25165
25554
  i = end2;
25166
25555
  while (i < a.length && a[i].g === " ") i++;
25167
25556
  if (a[i]?.g === "\n") {
@@ -30374,16 +30763,16 @@ function resolveAllAttention(events, context) {
30374
30763
  let nextEvents;
30375
30764
  while (++index2 < events.length) {
30376
30765
  if (events[index2][0] === "enter" && events[index2][1].type === "attentionSequence" && events[index2][1]._close) {
30377
- let open5 = index2;
30378
- while (open5--) {
30379
- if (events[open5][0] === "exit" && events[open5][1].type === "attentionSequence" && events[open5][1]._open && // If the markers are the same:
30380
- context.sliceSerialize(events[open5][1]).charCodeAt(0) === context.sliceSerialize(events[index2][1]).charCodeAt(0)) {
30381
- if ((events[open5][1]._close || events[index2][1]._open) && (events[index2][1].end.offset - events[index2][1].start.offset) % 3 && !((events[open5][1].end.offset - events[open5][1].start.offset + events[index2][1].end.offset - events[index2][1].start.offset) % 3)) {
30766
+ let open6 = index2;
30767
+ while (open6--) {
30768
+ if (events[open6][0] === "exit" && events[open6][1].type === "attentionSequence" && events[open6][1]._open && // If the markers are the same:
30769
+ context.sliceSerialize(events[open6][1]).charCodeAt(0) === context.sliceSerialize(events[index2][1]).charCodeAt(0)) {
30770
+ if ((events[open6][1]._close || events[index2][1]._open) && (events[index2][1].end.offset - events[index2][1].start.offset) % 3 && !((events[open6][1].end.offset - events[open6][1].start.offset + events[index2][1].end.offset - events[index2][1].start.offset) % 3)) {
30382
30771
  continue;
30383
30772
  }
30384
- const use = events[open5][1].end.offset - events[open5][1].start.offset > 1 && events[index2][1].end.offset - events[index2][1].start.offset > 1 ? 2 : 1;
30773
+ const use = events[open6][1].end.offset - events[open6][1].start.offset > 1 && events[index2][1].end.offset - events[index2][1].start.offset > 1 ? 2 : 1;
30385
30774
  const start = {
30386
- ...events[open5][1].end
30775
+ ...events[open6][1].end
30387
30776
  };
30388
30777
  const end2 = {
30389
30778
  ...events[index2][1].start
@@ -30394,7 +30783,7 @@ function resolveAllAttention(events, context) {
30394
30783
  type: use > 1 ? "strongSequence" : "emphasisSequence",
30395
30784
  start,
30396
30785
  end: {
30397
- ...events[open5][1].end
30786
+ ...events[open6][1].end
30398
30787
  }
30399
30788
  };
30400
30789
  const closingSequence = {
@@ -30407,7 +30796,7 @@ function resolveAllAttention(events, context) {
30407
30796
  const text7 = {
30408
30797
  type: use > 1 ? "strongText" : "emphasisText",
30409
30798
  start: {
30410
- ...events[open5][1].end
30799
+ ...events[open6][1].end
30411
30800
  },
30412
30801
  end: {
30413
30802
  ...events[index2][1].start
@@ -30422,26 +30811,26 @@ function resolveAllAttention(events, context) {
30422
30811
  ...closingSequence.end
30423
30812
  }
30424
30813
  };
30425
- events[open5][1].end = {
30814
+ events[open6][1].end = {
30426
30815
  ...openingSequence.start
30427
30816
  };
30428
30817
  events[index2][1].start = {
30429
30818
  ...closingSequence.end
30430
30819
  };
30431
30820
  nextEvents = [];
30432
- if (events[open5][1].end.offset - events[open5][1].start.offset) {
30433
- nextEvents = push(nextEvents, [["enter", events[open5][1], context], ["exit", events[open5][1], context]]);
30821
+ if (events[open6][1].end.offset - events[open6][1].start.offset) {
30822
+ nextEvents = push(nextEvents, [["enter", events[open6][1], context], ["exit", events[open6][1], context]]);
30434
30823
  }
30435
30824
  nextEvents = push(nextEvents, [["enter", group, context], ["enter", openingSequence, context], ["exit", openingSequence, context], ["enter", text7, context]]);
30436
- nextEvents = push(nextEvents, resolveAll(context.parser.constructs.insideSpan.null, events.slice(open5 + 1, index2), context));
30825
+ nextEvents = push(nextEvents, resolveAll(context.parser.constructs.insideSpan.null, events.slice(open6 + 1, index2), context));
30437
30826
  nextEvents = push(nextEvents, [["exit", text7, context], ["enter", closingSequence, context], ["exit", closingSequence, context], ["exit", group, context]]);
30438
30827
  let offset = 0;
30439
30828
  if (events[index2][1].end.offset - events[index2][1].start.offset) {
30440
30829
  offset = 2;
30441
30830
  nextEvents = push(nextEvents, [["enter", events[index2][1], context], ["exit", events[index2][1], context]]);
30442
30831
  }
30443
- splice(events, open5 - 1, index2 - open5 + 3, nextEvents);
30444
- index2 = open5 + nextEvents.length - offset - 2;
30832
+ splice(events, open6 - 1, index2 - open6 + 3, nextEvents);
30833
+ index2 = open6 + nextEvents.length - offset - 2;
30445
30834
  break;
30446
30835
  }
30447
30836
  }
@@ -30473,10 +30862,10 @@ function tokenizeAttention(effects, ok4) {
30473
30862
  }
30474
30863
  const token = effects.exit("attentionSequence");
30475
30864
  const after2 = classifyCharacter(code3);
30476
- const open5 = !after2 || after2 === 2 && before2 || attentionMarkers2.includes(code3) && code3 !== 42 && code3 !== 95;
30865
+ const open6 = !after2 || after2 === 2 && before2 || attentionMarkers2.includes(code3) && code3 !== 42 && code3 !== 95;
30477
30866
  const close = !before2 || before2 === 2 && after2 || attentionMarkers2.includes(previous3) && previous3 !== 42 && previous3 !== 95;
30478
- token._open = Boolean(marker === 42 ? open5 : open5 && (before2 || !close));
30479
- token._close = Boolean(marker === 42 ? close : close && (after2 || !open5));
30867
+ token._open = Boolean(marker === 42 ? open6 : open6 && (before2 || !close));
30868
+ token._close = Boolean(marker === 42 ? close : close && (after2 || !open6));
30480
30869
  return ok4(code3);
30481
30870
  }
30482
30871
  }
@@ -30510,9 +30899,9 @@ function tokenizeAutolink(effects, ok4, nok) {
30510
30899
  effects.consume(code3);
30511
30900
  effects.exit("autolinkMarker");
30512
30901
  effects.enter("autolinkProtocol");
30513
- return open5;
30902
+ return open6;
30514
30903
  }
30515
- function open5(code3) {
30904
+ function open6(code3) {
30516
30905
  if (asciiAlpha(code3)) {
30517
30906
  effects.consume(code3);
30518
30907
  return schemeOrEmailAtext;
@@ -30793,9 +31182,9 @@ function tokenizeCharacterReference(effects, ok4, nok) {
30793
31182
  effects.enter("characterReferenceMarker");
30794
31183
  effects.consume(code3);
30795
31184
  effects.exit("characterReferenceMarker");
30796
- return open5;
31185
+ return open6;
30797
31186
  }
30798
- function open5(code3) {
31187
+ function open6(code3) {
30799
31188
  if (code3 === 35) {
30800
31189
  effects.enter("characterReferenceMarkerNumeric");
30801
31190
  effects.consume(code3);
@@ -32196,9 +32585,9 @@ function tokenizeHtmlFlow(effects, ok4, nok) {
32196
32585
  effects.enter("htmlFlow");
32197
32586
  effects.enter("htmlFlowData");
32198
32587
  effects.consume(code3);
32199
- return open5;
32588
+ return open6;
32200
32589
  }
32201
- function open5(code3) {
32590
+ function open6(code3) {
32202
32591
  if (code3 === 33) {
32203
32592
  effects.consume(code3);
32204
32593
  return declarationOpen;
@@ -32546,9 +32935,9 @@ function tokenizeHtmlText(effects, ok4, nok) {
32546
32935
  effects.enter("htmlText");
32547
32936
  effects.enter("htmlTextData");
32548
32937
  effects.consume(code3);
32549
- return open5;
32938
+ return open6;
32550
32939
  }
32551
- function open5(code3) {
32940
+ function open6(code3) {
32552
32941
  if (code3 === 33) {
32553
32942
  effects.consume(code3);
32554
32943
  return declarationOpen;
@@ -32870,12 +33259,12 @@ function resolveAllLabelEnd(events) {
32870
33259
  function resolveToLabelEnd(events, context) {
32871
33260
  let index2 = events.length;
32872
33261
  let offset = 0;
32873
- let open5;
33262
+ let open6;
32874
33263
  let close;
32875
33264
  let media;
32876
33265
  while (index2--) {
32877
33266
  const token = events[index2][1];
32878
- if (open5) {
33267
+ if (open6) {
32879
33268
  if (token.type === "link" || token.type === "labelLink" && token._inactive) {
32880
33269
  break;
32881
33270
  }
@@ -32884,7 +33273,7 @@ function resolveToLabelEnd(events, context) {
32884
33273
  }
32885
33274
  } else if (close) {
32886
33275
  if (events[index2][0] === "enter" && (token.type === "labelImage" || token.type === "labelLink") && !token._balanced) {
32887
- open5 = index2;
33276
+ open6 = index2;
32888
33277
  if (token.type !== "labelLink") {
32889
33278
  offset = 2;
32890
33279
  break;
@@ -32895,9 +33284,9 @@ function resolveToLabelEnd(events, context) {
32895
33284
  }
32896
33285
  }
32897
33286
  const group = {
32898
- type: events[open5][1].type === "labelLink" ? "link" : "image",
33287
+ type: events[open6][1].type === "labelLink" ? "link" : "image",
32899
33288
  start: {
32900
- ...events[open5][1].start
33289
+ ...events[open6][1].start
32901
33290
  },
32902
33291
  end: {
32903
33292
  ...events[events.length - 1][1].end
@@ -32906,7 +33295,7 @@ function resolveToLabelEnd(events, context) {
32906
33295
  const label2 = {
32907
33296
  type: "label",
32908
33297
  start: {
32909
- ...events[open5][1].start
33298
+ ...events[open6][1].start
32910
33299
  },
32911
33300
  end: {
32912
33301
  ...events[close][1].end
@@ -32915,20 +33304,20 @@ function resolveToLabelEnd(events, context) {
32915
33304
  const text7 = {
32916
33305
  type: "labelText",
32917
33306
  start: {
32918
- ...events[open5 + offset + 2][1].end
33307
+ ...events[open6 + offset + 2][1].end
32919
33308
  },
32920
33309
  end: {
32921
33310
  ...events[close - 2][1].start
32922
33311
  }
32923
33312
  };
32924
33313
  media = [["enter", group, context], ["enter", label2, context]];
32925
- media = push(media, events.slice(open5 + 1, open5 + offset + 3));
33314
+ media = push(media, events.slice(open6 + 1, open6 + offset + 3));
32926
33315
  media = push(media, [["enter", text7, context]]);
32927
- media = push(media, resolveAll(context.parser.constructs.insideSpan.null, events.slice(open5 + offset + 4, close - 3), context));
33316
+ media = push(media, resolveAll(context.parser.constructs.insideSpan.null, events.slice(open6 + offset + 4, close - 3), context));
32928
33317
  media = push(media, [["exit", text7, context], events[close - 2], events[close - 1], ["exit", label2, context]]);
32929
33318
  media = push(media, events.slice(close + 1));
32930
33319
  media = push(media, [["exit", group, context]]);
32931
- splice(events, open5, events.length, media);
33320
+ splice(events, open6, events.length, media);
32932
33321
  return events;
32933
33322
  }
32934
33323
  function tokenizeLabelEnd(effects, ok4, nok) {
@@ -33099,9 +33488,9 @@ function tokenizeLabelStartImage(effects, ok4, nok) {
33099
33488
  effects.enter("labelImageMarker");
33100
33489
  effects.consume(code3);
33101
33490
  effects.exit("labelImageMarker");
33102
- return open5;
33491
+ return open6;
33103
33492
  }
33104
- function open5(code3) {
33493
+ function open6(code3) {
33105
33494
  if (code3 === 91) {
33106
33495
  effects.enter("labelMarker");
33107
33496
  effects.consume(code3);
@@ -33933,30 +34322,30 @@ function gfmStrikethrough(options) {
33933
34322
  let index2 = -1;
33934
34323
  while (++index2 < events.length) {
33935
34324
  if (events[index2][0] === "enter" && events[index2][1].type === "strikethroughSequenceTemporary" && events[index2][1]._close) {
33936
- let open5 = index2;
33937
- while (open5--) {
33938
- if (events[open5][0] === "exit" && events[open5][1].type === "strikethroughSequenceTemporary" && events[open5][1]._open && // If the sizes are the same:
33939
- events[index2][1].end.offset - events[index2][1].start.offset === events[open5][1].end.offset - events[open5][1].start.offset) {
34325
+ let open6 = index2;
34326
+ while (open6--) {
34327
+ if (events[open6][0] === "exit" && events[open6][1].type === "strikethroughSequenceTemporary" && events[open6][1]._open && // If the sizes are the same:
34328
+ events[index2][1].end.offset - events[index2][1].start.offset === events[open6][1].end.offset - events[open6][1].start.offset) {
33940
34329
  events[index2][1].type = "strikethroughSequence";
33941
- events[open5][1].type = "strikethroughSequence";
34330
+ events[open6][1].type = "strikethroughSequence";
33942
34331
  const strikethrough = {
33943
34332
  type: "strikethrough",
33944
- start: Object.assign({}, events[open5][1].start),
34333
+ start: Object.assign({}, events[open6][1].start),
33945
34334
  end: Object.assign({}, events[index2][1].end)
33946
34335
  };
33947
34336
  const text7 = {
33948
34337
  type: "strikethroughText",
33949
- start: Object.assign({}, events[open5][1].end),
34338
+ start: Object.assign({}, events[open6][1].end),
33950
34339
  end: Object.assign({}, events[index2][1].start)
33951
34340
  };
33952
- const nextEvents = [["enter", strikethrough, context], ["enter", events[open5][1], context], ["exit", events[open5][1], context], ["enter", text7, context]];
34341
+ const nextEvents = [["enter", strikethrough, context], ["enter", events[open6][1], context], ["exit", events[open6][1], context], ["enter", text7, context]];
33953
34342
  const insideSpan2 = context.parser.constructs.insideSpan.null;
33954
34343
  if (insideSpan2) {
33955
- splice(nextEvents, nextEvents.length, 0, resolveAll(insideSpan2, events.slice(open5 + 1, index2), context));
34344
+ splice(nextEvents, nextEvents.length, 0, resolveAll(insideSpan2, events.slice(open6 + 1, index2), context));
33956
34345
  }
33957
34346
  splice(nextEvents, nextEvents.length, 0, [["exit", text7, context], ["enter", events[index2][1], context], ["exit", events[index2][1], context], ["exit", strikethrough, context]]);
33958
- splice(events, open5 - 1, index2 - open5 + 3, nextEvents);
33959
- index2 = open5 + nextEvents.length - 2;
34347
+ splice(events, open6 - 1, index2 - open6 + 3, nextEvents);
34348
+ index2 = open6 + nextEvents.length - 2;
33960
34349
  break;
33961
34350
  }
33962
34351
  }
@@ -34558,8 +34947,8 @@ function gfmTaskListItem() {
34558
34947
  }
34559
34948
  function tokenizeTasklistCheck(effects, ok4, nok) {
34560
34949
  const self = this;
34561
- return open5;
34562
- function open5(code3) {
34950
+ return open6;
34951
+ function open6(code3) {
34563
34952
  if (
34564
34953
  // Exit if there’s stuff before.
34565
34954
  self.previous !== null || // Exit if not in the first content that is the first child of a list
@@ -36048,8 +36437,8 @@ function compiler(options) {
36048
36437
  events[start][1]._spread = listSpread;
36049
36438
  }
36050
36439
  function opener(create, and) {
36051
- return open5;
36052
- function open5(token) {
36440
+ return open6;
36441
+ function open6(token) {
36053
36442
  enter.call(this, create(token), token);
36054
36443
  if (and) and.call(this, token);
36055
36444
  }
@@ -36081,19 +36470,19 @@ function compiler(options) {
36081
36470
  }
36082
36471
  function exit3(token, onExitError) {
36083
36472
  const node2 = this.stack.pop();
36084
- const open5 = this.tokenStack.pop();
36085
- if (!open5) {
36473
+ const open6 = this.tokenStack.pop();
36474
+ if (!open6) {
36086
36475
  throw new Error("Cannot close `" + token.type + "` (" + stringifyPosition({
36087
36476
  start: token.start,
36088
36477
  end: token.end
36089
36478
  }) + "): it\u2019s not open");
36090
36479
  }
36091
- if (open5[0].type !== token.type) {
36480
+ if (open6[0].type !== token.type) {
36092
36481
  if (onExitError) {
36093
- onExitError.call(this, token, open5[0]);
36482
+ onExitError.call(this, token, open6[0]);
36094
36483
  } else {
36095
- const handler = open5[1] || defaultOnError;
36096
- handler.call(this, token, open5[0]);
36484
+ const handler = open6[1] || defaultOnError;
36485
+ handler.call(this, token, open6[0]);
36097
36486
  }
36098
36487
  }
36099
36488
  node2.position.end = point2(token.end);
@@ -39513,7 +39902,8 @@ function Overview() {
39513
39902
  return true;
39514
39903
  }
39515
39904
  if (key.return) {
39516
- if (selected) go("session");
39905
+ if (selected && isOmpSeat(selected)) attach2(selected);
39906
+ else if (selected) go("session");
39517
39907
  return true;
39518
39908
  }
39519
39909
  if (!selected) return false;
@@ -39534,7 +39924,7 @@ function Overview() {
39534
39924
  return false;
39535
39925
  });
39536
39926
  const groupRows = (kind) => view.tree.filter((n) => n.kind === kind).flatMap((n) => [
39537
- ...kind === "rig" ? [{ kind: "project", key: n.key, label: n.label, color: C.blue, count: n.count, rig: true, folder: folderOf(n.branches[0]?.sessions[0]?.cwd) }] : [],
39927
+ ...kind === "rig" ? [{ kind: "project", key: n.key, label: n.label, color: C.blue, count: n.count, ...n.shown !== void 0 && { shown: n.shown }, rig: true, folder: folderOf(n.branches[0]?.sessions[0]?.cwd) }] : [],
39538
39928
  ...n.branches.flatMap((b3) => b3.sessions.map((s) => ({ kind: "session", key: s.id, s, indent: kind === "rig" ? 2 : 0, group: kind })))
39539
39929
  ]);
39540
39930
  const rigRows = groupRows("rig"), kernelRows = groupRows("kernel");
@@ -39556,6 +39946,7 @@ function Overview() {
39556
39946
  const stateW = Math.max(...Object.values(t.state).map((x) => x.length)) + 1;
39557
39947
  const topicW = Math.max(6, inner - 6 - nameW - stateW - 3);
39558
39948
  const askW = Math.max(6, inner - 2 - nameW - 9 - 7);
39949
+ const roleW = Math.max(11, ...rigRows.map((r) => r.kind === "session" ? [...r.s.rig?.role ?? ""].length + 2 : 0));
39559
39950
  const yoursBox = /* @__PURE__ */ jsx10(TitledBox, { title: t.overview.yours(yoursCount), color: yoursCount ? C.red : C.line, titleColor: yoursCount ? C.red : C.dim, height: yoursHeight + 2, children: yoursCount === 0 ? /* @__PURE__ */ jsx10(Text7, { color: C.faint, children: t.overview.nobodyWaits }) : [...view.yours.slice(0, yoursHeight).map((s) => {
39560
39951
  const st = botState(s);
39561
39952
  return /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", backgroundColor: s.id === sel ? C.sel : void 0, children: [
@@ -39621,16 +40012,18 @@ function Overview() {
39621
40012
  ] }) : null,
39622
40013
  /* @__PURE__ */ jsxs6(Text7, { color: C.faint, children: [
39623
40014
  " ",
39624
- t.overview.sessions(r.count)
40015
+ r.shown !== void 0 ? t.overview.sessionsOf(r.count, r.shown) : t.overview.sessions(r.count)
39625
40016
  ] })
39626
- ] }, r.key) : r.kind === "session" ? /* @__PURE__ */ jsx10(SessionRow, { s: r.s, on: r.s.id === sel, nameW, topicW: Math.max(6, topicW - r.indent), stateW, indent: r.indent, group: r.group }, r.key) : null) });
40017
+ ] }, r.key) : r.kind === "session" ? /* @__PURE__ */ jsx10(SessionRow, { s: r.s, on: r.s.id === sel, nameW, topicW: Math.max(6, topicW - r.indent), stateW, indent: r.indent, group: r.group, roleW }, r.key) : null) });
39627
40018
  const rigsBox = groupBox(t.overview.rigsGroup, rigRows, rigsHeight);
39628
40019
  const kernelBox = groupBox(t.overview.kernelGroup, kernelRows, kernelHeight);
40020
+ const stopped2 = !!selected && isStoppedOmpSeat(selected);
39629
40021
  const keys = writing ? [["\u21B5", t.keys.sendOnly], ["alt+\u21B5", t.keys.newLine], ["esc", t.keys.cancel]] : deciding?.confirm ? [["y", t.keys.sendAnswer], ["n esc", t.keys.edit]] : deciding ? [["\u21B5", t.keys.review], ["alt+\u21B5", t.keys.newLine], ["esc", t.keys.cancel]] : [
40022
+ // On an OMP seat ↵ attaches its terminal; on a stopped one, neither ↵ nor a has anything to attach.
39630
40023
  ["j k", t.keys.navigate],
39631
- ["\u21B5", t.keys.open],
40024
+ ...stopped2 ? [] : [["\u21B5", selected && isOmpSeat(selected) ? t.keys.attachSeat : t.keys.open]],
39632
40025
  ...view.yours.length ? [["tab", t.keys.next], ["y n", t.keys.answer]] : [],
39633
- ["a", t.keys.attach],
40026
+ ...stopped2 ? [] : [["a", t.keys.attach]],
39634
40027
  ...writable2 ? [["m", t.keys.writeProject]] : [],
39635
40028
  ...decisions.length ? [["D", t.keys.decide]] : [],
39636
40029
  ...near.length ? [["H", t.keys.handover]] : [],
@@ -39705,21 +40098,23 @@ function Overview() {
39705
40098
  ) : null
39706
40099
  ] });
39707
40100
  }
39708
- function SessionRow({ s, on, nameW, topicW, stateW, indent: indent2 = 0, group }) {
40101
+ function SessionRow({ s, on, nameW, topicW, stateW, indent: indent2 = 0, group, roleW = 11 }) {
39709
40102
  const { t, g } = useTui();
39710
40103
  const st = botState(s);
39711
40104
  const n = helperCount(s);
40105
+ const stopped2 = isStoppedOmpSeat(s);
40106
+ const roleTool = group === "kernel" ? `${(s.rig?.role ?? "").replace(/\.(agent|lead|worker)$/, "").padEnd(11)}${s.tool}` : `${(s.rig?.role ?? "").padEnd(roleW)}${s.tool}`;
39712
40107
  return /* @__PURE__ */ jsxs6(Text7, { wrap: "truncate", backgroundColor: on ? C.sel : void 0, children: [
39713
40108
  /* @__PURE__ */ jsx10(Text7, { color: on ? C.coral : C.faint, children: on ? "\u258C" : " " }),
39714
40109
  /* @__PURE__ */ jsx10(Text7, { children: " ".repeat(2 + indent2) }),
39715
- /* @__PURE__ */ jsxs6(Text7, { color: s.offline ? C.faint : STATE_COLOR[st], children: [
40110
+ /* @__PURE__ */ jsxs6(Text7, { color: s.offline || stopped2 ? C.faint : STATE_COLOR[st], children: [
39716
40111
  g[st],
39717
40112
  " "
39718
40113
  ] }),
39719
- /* @__PURE__ */ jsx10(Text7, { color: s.offline ? C.dim : C.fg, bold: true, children: fit3(s.name, nameW) }),
39720
- group ? /* @__PURE__ */ jsxs6(Text7, { color: C.dim, children: [
40114
+ /* @__PURE__ */ jsx10(Text7, { color: stopped2 ? C.faint : s.offline ? C.dim : C.fg, bold: !stopped2, children: fit3(s.name, nameW) }),
40115
+ group ? /* @__PURE__ */ jsxs6(Text7, { color: stopped2 ? C.faint : C.dim, children: [
39721
40116
  " ",
39722
- fit3(group === "kernel" ? `${(s.rig?.role ?? "").replace(/\.(agent|lead|worker)$/, "").padEnd(11)}${s.tool}` : s.rig?.role ?? "", topicW - 1),
40117
+ fit3(roleTool, topicW - 1),
39723
40118
  " "
39724
40119
  ] }) : /* @__PURE__ */ jsxs6(Fragment3, { children: [
39725
40120
  s.rig ? /* @__PURE__ */ jsxs6(Text7, { color: C.blue, children: [
@@ -39731,7 +40126,7 @@ function SessionRow({ s, on, nameW, topicW, stateW, indent: indent2 = 0, group }
39731
40126
  " "
39732
40127
  ] })
39733
40128
  ] }),
39734
- /* @__PURE__ */ jsx10(Text7, { color: s.offline ? C.faint : STATE_COLOR[st], children: fit3(s.offline ? t.overview.offline : t.state[st], stateW).trimEnd() }),
40129
+ /* @__PURE__ */ jsx10(Text7, { color: s.offline || stopped2 ? C.faint : STATE_COLOR[st], children: fit3(s.offline ? t.overview.offline : stopped2 ? t.state.stopped : t.state[st], stateW).trimEnd() }),
39735
40130
  n > 0 ? /* @__PURE__ */ jsxs6(Text7, { color: C.violet, children: [
39736
40131
  " +",
39737
40132
  n
@@ -39770,7 +40165,7 @@ var init_Overview = __esm({
39770
40165
  // src/tui/images.tsx
39771
40166
  import { mkdir as mkdir13, writeFile as writeFile17 } from "fs/promises";
39772
40167
  import { tmpdir as tmpdir3 } from "os";
39773
- import { join as join37 } from "path";
40168
+ import { join as join38 } from "path";
39774
40169
  import { useRef as useRef8, useState as useState4 } from "react";
39775
40170
  import { Text as Text8 } from "ink";
39776
40171
  import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
@@ -39873,9 +40268,9 @@ function useOpenImage() {
39873
40268
  const { ref, route } = item;
39874
40269
  void (async () => {
39875
40270
  const { mime, data } = await getImage(client, ref.id, route);
39876
- const dir = join37(tmpdir3(), "ledge-images");
40271
+ const dir = join38(tmpdir3(), "ledge-images");
39877
40272
  await mkdir13(dir, { recursive: true, mode: 448 });
39878
- const file = join37(dir, `${ref.id}.${EXT2[mime]}`);
40273
+ const file = join38(dir, `${ref.id}.${EXT2[mime]}`);
39879
40274
  await writeFile17(file, Buffer.from(data, "base64"), { mode: 384 });
39880
40275
  await openFile2(file);
39881
40276
  })().catch((e) => toast({ text: t.images.openFailed(e instanceof AttachmentFailure ? e.reason : e.message), tone: "error" }));
@@ -40660,11 +41055,11 @@ function cardPlan(actions, room) {
40660
41055
  const raw = rows.map((r) => r.state ?? "proposed");
40661
41056
  const stopAt = raw.findIndex((s) => s === "failed" || s === "refused" || s === "stale");
40662
41057
  const shown = states.map((s, i) => s === "proposed" && stopAt >= 0 && i > stopAt ? "held" : s);
40663
- const open5 = shown.flatMap((s, i) => ["done", "running", "denied", "stale", "cut"].includes(s) ? [] : [i]);
41058
+ const open6 = shown.flatMap((s, i) => ["done", "running", "denied", "stale", "cut"].includes(s) ? [] : [i]);
40664
41059
  const skip = stopAt >= 0 && raw[stopAt] !== "failed";
40665
- const rowsAll = skip ? open5.filter((i) => i !== stopAt) : stopAt >= 0 && states[stopAt] === "cut" ? [] : open5;
41060
+ const rowsAll = skip ? open6.filter((i) => i !== stopAt) : stopAt >= 0 && states[stopAt] === "cut" ? [] : open6;
40666
41061
  const all2 = { kind: stopAt < 0 ? "all" : skip ? "skip" : "resume", rows: rowsAll, k: stopAt + 1 };
40667
- return { rows, shown, open: open5, stopAt, all: all2, cutLines };
41062
+ return { rows, shown, open: open6, stopAt, all: all2, cutLines };
40668
41063
  }
40669
41064
  function firstRow(p) {
40670
41065
  const act = p.shown.findIndex((s) => s === "proposed" || s === "held" || s === "failed");
@@ -42054,8 +42449,8 @@ function listItems2(roles, query, builtinOpen) {
42054
42449
  const by = (a, b3) => a.name.localeCompare(b3.name) || a.id.localeCompare(b3.id);
42055
42450
  const kept = roles.filter((r) => matches(r, query)).sort(by);
42056
42451
  const mine = kept.filter((r) => r.source === "user"), builtin = kept.filter((r) => r.source === "builtin");
42057
- const open5 = builtinOpen || query.trim() !== "";
42058
- return { mine, builtin, items: [...mine.map((role) => ({ kind: "role", role })), ...builtin.length ? [{ kind: "builtin" }] : [], ...open5 ? builtin.map((role) => ({ kind: "role", role })) : []] };
42452
+ const open6 = builtinOpen || query.trim() !== "";
42453
+ return { mine, builtin, items: [...mine.map((role) => ({ kind: "role", role })), ...builtin.length ? [{ kind: "builtin" }] : [], ...open6 ? builtin.map((role) => ({ kind: "role", role })) : []] };
42059
42454
  }
42060
42455
  function listLines(roles, o, t, g) {
42061
42456
  const r = t.roles, w = o.width;
@@ -42087,11 +42482,11 @@ function listLines(roles, o, t, g) {
42087
42482
  if (mine.length) lines.push([]);
42088
42483
  const s = i === o.sel;
42089
42484
  if (s) selLine = lines.length;
42090
- const open5 = o.builtinOpen || o.query.trim() !== "";
42485
+ const open6 = o.builtinOpen || o.query.trim() !== "";
42091
42486
  const bg = s ? C.sel : void 0;
42092
- lines.push([{ text: s ? "\u258C" : " ", color: s && o.focused ? C.coral : C.faint, ...bg ? { bg } : {} }, { text: `${open5 ? g.unfolded : g.folded} ${r.builtin}`, color: C.dim, ...bg ? { bg } : {} }, { text: ` \xB7 ${builtin.length}`, color: C.faint, ...bg ? { bg } : {} }]);
42487
+ lines.push([{ text: s ? "\u258C" : " ", color: s && o.focused ? C.coral : C.faint, ...bg ? { bg } : {} }, { text: `${open6 ? g.unfolded : g.folded} ${r.builtin}`, color: C.dim, ...bg ? { bg } : {} }, { text: ` \xB7 ${builtin.length}`, color: C.faint, ...bg ? { bg } : {} }]);
42093
42488
  i++;
42094
- if (open5) builtin.forEach(roleLine);
42489
+ if (open6) builtin.forEach(roleLine);
42095
42490
  }
42096
42491
  return { lines, selLine };
42097
42492
  }
@@ -42187,7 +42582,7 @@ function param(label2, value, o, t, g, w, narrow, extra = {}) {
42187
42582
  const usedL = cells(lab.map((x) => x.text).join("")), usedV = cells(val.map((x) => x.text).join(""));
42188
42583
  return { line: [...lab, { text: " ".repeat(lw - usedL) }, ...val, { text: " ".repeat(Math.max(1, w - lw - usedV - cells(p))) }, { text: p, color: provColor(k) }], rail: k, ...extra };
42189
42584
  }
42190
- function paramRows(d, seat2, files, open5, o, t) {
42585
+ function paramRows(d, seat2, files, open6, o, t) {
42191
42586
  const r = t.roles, w = o.width, g = o.g, n = o.narrow;
42192
42587
  const P = (label2, value, origin, extra = {}) => param(label2, value, origin, t, g, w, n, extra);
42193
42588
  const out = [];
@@ -42228,8 +42623,8 @@ function paramRows(d, seat2, files, open5, o, t) {
42228
42623
  const u = prof.uses;
42229
42624
  const loadout = seat2?.live?.skills;
42230
42625
  if (loadout) {
42231
- out.push(P(r.p.skills, v(`${r.skillsProjected(loadout.length)} ${open5.has("loadout") ? g.unfolded : g.folded}`), { kind: "inherited", from: "launch" }, { fold: "loadout" }));
42232
- if (open5.has("loadout")) for (const s of loadout) out.push({ line: [{ text: " ".repeat(n ? 16 : 19) }, { text: s.id, color: C.dim }], rail: "inh" });
42626
+ out.push(P(r.p.skills, v(`${r.skillsProjected(loadout.length)} ${open6.has("loadout") ? g.unfolded : g.folded}`), { kind: "inherited", from: "launch" }, { fold: "loadout" }));
42627
+ if (open6.has("loadout")) for (const s of loadout) out.push({ line: [{ text: " ".repeat(n ? 16 : 19) }, { text: s.id, color: C.dim }], rail: "inh" });
42233
42628
  }
42234
42629
  const list3 = (label2, l, what, fold) => {
42235
42630
  if (!l.declared) {
@@ -42243,9 +42638,9 @@ function paramRows(d, seat2, files, open5, o, t) {
42243
42638
  l.items.forEach((x, i) => {
42244
42639
  const na = !!(seat2 && x.runtime && seat2.runtime.value && x.runtime !== seat2.runtime.value);
42245
42640
  const key = fold?.(x);
42246
- const row3 = P(i === 0 ? label2 : "", v(`${what(x)}${na ? ` \xB7 ${r.na(x.runtime)}` : ""}${key ? ` ${open5.has(key) ? g.unfolded : g.folded}` : ""}`, na ? C.faint : C.fg), x.origin, key ? { fold: key } : {});
42641
+ const row3 = P(i === 0 ? label2 : "", v(`${what(x)}${na ? ` \xB7 ${r.na(x.runtime)}` : ""}${key ? ` ${open6.has(key) ? g.unfolded : g.folded}` : ""}`, na ? C.faint : C.fg), x.origin, key ? { fold: key } : {});
42247
42642
  out.push(na ? { ...row3, line: row3.line.map((s) => ({ ...s, color: C.faint })) } : row3);
42248
- if (key && open5.has(key)) {
42643
+ if (key && open6.has(key)) {
42249
42644
  const body = x.plugin ? x.plugin.skills.join(", ") : (x.absolutePath && files[x.absolutePath]?.content) ?? t.roles.reading;
42250
42645
  for (const bl of body.split("\n")) out.push({ line: cut2([{ text: " ".repeat(n ? 16 : 19) }, { text: bl, color: C.dim }], w), rail: "inh" });
42251
42646
  }
@@ -42333,9 +42728,9 @@ function yamlRows(d, seat2, files, o, t) {
42333
42728
  }
42334
42729
  return out;
42335
42730
  }
42336
- function filesFor(d, seat2, tab, at, open5) {
42731
+ function filesFor(d, seat2, tab, at, open6) {
42337
42732
  if (tab === 0) return roleDocs(d, seat2).map((e) => e.absolutePath).filter((x) => !!x);
42338
- if (tab === 1) return (profileOf(d, seat2)?.uses.runtimeResources.items ?? []).filter((x) => x.absolutePath && open5.has(`rr:${x.id}`)).map((x) => x.absolutePath);
42733
+ if (tab === 1) return (profileOf(d, seat2)?.uses.runtimeResources.items ?? []).filter((x) => x.absolutePath && open6.has(`rr:${x.id}`)).map((x) => x.absolutePath);
42339
42734
  if (tab === 2) {
42340
42735
  const p = pieces(d, seat2)[at];
42341
42736
  return p?.kind === "entry" && p.entry.absolutePath && !isMissing(p.entry) ? [p.entry.absolutePath] : [];
@@ -42412,7 +42807,7 @@ function RolesScreen({ onRigs, machineId, onMachine }) {
42412
42807
  const [scroll, setScroll] = useState14(0);
42413
42808
  const [at, setAt] = useState14(0);
42414
42809
  const [reading, setReading] = useState14(false);
42415
- const [open5, setOpen] = useState14(/* @__PURE__ */ new Set());
42810
+ const [open6, setOpen] = useState14(/* @__PURE__ */ new Set());
42416
42811
  const [files, setFiles] = useState14({});
42417
42812
  const { items } = listItems2(roles.roles, query, builtinOpen);
42418
42813
  const firstRole = items.find((i) => i.kind === "role");
@@ -42438,7 +42833,7 @@ function RolesScreen({ onRigs, machineId, onMachine }) {
42438
42833
  asks.current.reset();
42439
42834
  setFiles({});
42440
42835
  }, [currentId, machine?.id]);
42441
- const wanted = (d ? filesFor(d, seat2, shownTab, at, open5) : []).join("\n");
42836
+ const wanted = (d ? filesFor(d, seat2, shownTab, at, open6) : []).join("\n");
42442
42837
  useEffect13(() => {
42443
42838
  if (!wanted || !machine || !currentId) return;
42444
42839
  const id = currentId, mid = machine.id;
@@ -42451,7 +42846,7 @@ function RolesScreen({ onRigs, machineId, onMachine }) {
42451
42846
  const bodyH = Math.max(3, rows - 4);
42452
42847
  const head2 = d && summary ? headRows(d, summary, { seat: seat2, tab: shownTab, width: w, narrow: !wide, seats }, t) : [];
42453
42848
  const ctx = d && shownTab === 2 ? contextRows(d, seat2, files, at, { width: w, raw, narrow: !wide, g, lang }, t) : null;
42454
- const body = !d ? [] : shownTab === 0 ? roleRows(d, seat2, files, { width: w, raw, narrow: !wide, g, lang }, t) : shownTab === 1 ? paramRows(d, seat2, files, open5, { width: w, narrow: !wide, g }, t) : shownTab === 2 ? ctx.rows : yamlRows(d, seat2, files, { width: w }, t);
42849
+ const body = !d ? [] : shownTab === 0 ? roleRows(d, seat2, files, { width: w, raw, narrow: !wide, g, lang }, t) : shownTab === 1 ? paramRows(d, seat2, files, open6, { width: w, narrow: !wide, g }, t) : shownTab === 2 ? ctx.rows : yamlRows(d, seat2, files, { width: w }, t);
42455
42850
  const room = Math.max(1, bodyH - head2.length);
42456
42851
  const maxScroll = Math.max(0, body.length - room);
42457
42852
  const params = shownTab === 1 ? body.map((x, i) => x.rail ? i : -1).filter((i) => i >= 0) : [];
@@ -42709,7 +43104,7 @@ function RigsView({ initialMachine, onRoles }) {
42709
43104
  const rigs = listedRigs(state2);
42710
43105
  const [at, setAt] = useState15(0);
42711
43106
  const selectedId = useRef13(null);
42712
- const [open5, setOpen] = useState15(/* @__PURE__ */ new Set());
43107
+ const [open6, setOpen] = useState15(/* @__PURE__ */ new Set());
42713
43108
  const [seatAt, setSeatAt] = useState15(null);
42714
43109
  const [confirm, setConfirm] = useState15(null);
42715
43110
  const [seats, setSeats] = useState15(null);
@@ -42719,7 +43114,7 @@ function RigsView({ initialMachine, onRoles }) {
42719
43114
  }, [ids]);
42720
43115
  const current2 = rigs[Math.min(at, rigs.length - 1)];
42721
43116
  selectedId.current = current2?.id ?? null;
42722
- const seatNodes = current2 && open5.has(current2.id) ? current2.nodes : [];
43117
+ const seatNodes = current2 && open6.has(current2.id) ? current2.nodes : [];
42723
43118
  const seatSel = seatAt !== null && seatAt < seatNodes.length ? seatAt : null;
42724
43119
  const down = () => {
42725
43120
  if (seatSel !== null ? seatSel + 1 < seatNodes.length : seatNodes.length > 0) {
@@ -42737,7 +43132,7 @@ function RigsView({ initialMachine, onRoles }) {
42737
43132
  }
42738
43133
  const n = step2(at, -1, rigs.length), prev = rigs[n];
42739
43134
  setAt(n);
42740
- setSeatAt(prev && open5.has(prev.id) && prev.nodes.length ? prev.nodes.length - 1 : null);
43135
+ setSeatAt(prev && open6.has(prev.id) && prev.nodes.length ? prev.nodes.length - 1 : null);
42741
43136
  };
42742
43137
  const run4 = (a) => {
42743
43138
  void rig.act(a).then((d) => {
@@ -42897,12 +43292,12 @@ function RigsView({ initialMachine, onRoles }) {
42897
43292
  const keys = seats ? [["\u2191 \u2193", t.rigSeats.role], ["- +", t.rigSeats.count], ["\u2190 \u2192", t.rigSeats.delegationKey], ["\u21B5", t.rigSeats.apply], ["esc", t.rigSeats.cancel]] : confirm ? [["y", o.yes], ["n", o.no]] : [["tab", o.machine], ["v", t.roles.k.view], ["j k", t.keys.navigate], ["\u21B5", o.unfold], ...seatSel !== null ? [["i", t.roles.k.role]] : [], ["s", o.toggle], ["R", o.restart], ["\xB1 =", t.rigSeats.key], ["+", t.rigLaunch.launchKey], ["D", o.daemonKey], ["K", o.kernelKey], ["O", o.operatorKey], ["esc", t.keys.back]];
42898
43293
  const crumb = [state2?.version ? o.version(state2.version) : o.title, machine?.label, t.roles.rigs].filter(Boolean).join(" \xB7 ");
42899
43294
  return /* @__PURE__ */ jsxs17(Frame, { crumb, crumbShort: o.title, keys, children: [
42900
- !machine ? null : rig.error ? /* @__PURE__ */ jsx22(TitledBox, { title: o.title, mark: { text: machine.label, color: C.dim }, flexGrow: 1, children: /* @__PURE__ */ jsx22(Text18, { color: C.yellow, children: rig.error === "offline" ? o.offline : o.outdated }) }) : !state2 ? /* @__PURE__ */ jsx22(TitledBox, { title: o.title, mark: { text: machine.label, color: C.dim }, flexGrow: 1, children: /* @__PURE__ */ jsx22(Text18, { color: C.faint, children: o.loading }) }) : !state2.installed ? /* @__PURE__ */ jsx22(TitledBox, { title: o.title, mark: { text: machine.label, color: C.dim }, flexGrow: 1, children: /* @__PURE__ */ jsx22(Text18, { color: C.dim, children: o.notInstalled }) }) : seats && seatRig ? /* @__PURE__ */ jsx22(RigSeats, { rig: seatRig, machine: machine.label, rows, counts: seats.counts, at: seats.at, delegation: seats.delegation, plan: planOf(seatRig, rows, seats.counts), applying: seats.applying }) : /* @__PURE__ */ jsx22(Body, { state: state2, rigs, at, seatAt: seatSel, open: open5, machineLabel: machine.label, roleOf: roleOf4 }),
43295
+ !machine ? null : rig.error ? /* @__PURE__ */ jsx22(TitledBox, { title: o.title, mark: { text: machine.label, color: C.dim }, flexGrow: 1, children: /* @__PURE__ */ jsx22(Text18, { color: C.yellow, children: rig.error === "offline" ? o.offline : o.outdated }) }) : !state2 ? /* @__PURE__ */ jsx22(TitledBox, { title: o.title, mark: { text: machine.label, color: C.dim }, flexGrow: 1, children: /* @__PURE__ */ jsx22(Text18, { color: C.faint, children: o.loading }) }) : !state2.installed ? /* @__PURE__ */ jsx22(TitledBox, { title: o.title, mark: { text: machine.label, color: C.dim }, flexGrow: 1, children: /* @__PURE__ */ jsx22(Text18, { color: C.dim, children: o.notInstalled }) }) : seats && seatRig ? /* @__PURE__ */ jsx22(RigSeats, { rig: seatRig, machine: machine.label, rows, counts: seats.counts, at: seats.at, delegation: seats.delegation, plan: planOf(seatRig, rows, seats.counts), applying: seats.applying }) : /* @__PURE__ */ jsx22(Body, { state: state2, rigs, at, seatAt: seatSel, open: open6, machineLabel: machine.label, roleOf: roleOf4 }),
42901
43296
  confirm ? /* @__PURE__ */ jsx22(ConfirmBox, { confirm }) : null,
42902
43297
  rig.pending ? /* @__PURE__ */ jsx22(Box17, { paddingX: 1, height: 1, children: /* @__PURE__ */ jsx22(Text18, { wrap: "truncate", color: C.blue, children: o.running(confirmOf(state2 ?? EMPTY2, rig.pending).commands.join(" && ")) }) }) : null
42903
43298
  ] });
42904
43299
  }
42905
- function Body({ state: state2, rigs, at, seatAt, open: open5, machineLabel: machineLabel2, roleOf: roleOf4 }) {
43300
+ function Body({ state: state2, rigs, at, seatAt, open: open6, machineLabel: machineLabel2, roleOf: roleOf4 }) {
42906
43301
  const { t } = useTui();
42907
43302
  const o = t.openrig;
42908
43303
  const up = daemonUp(state2);
@@ -42940,10 +43335,10 @@ function Body({ state: state2, rigs, at, seatAt, open: open5, machineLabel: mach
42940
43335
  ] }, n.session))
42941
43336
  ] })
42942
43337
  ] }),
42943
- /* @__PURE__ */ jsx22(TitledBox, { title: `${o.rigs} \xB7 ${rigs.length}`, flexGrow: 1, children: !up ? /* @__PURE__ */ jsx22(Text18, { color: C.faint, children: o.stoppedDaemon }) : !rigs.length ? /* @__PURE__ */ jsx22(Text18, { color: C.faint, children: o.noRigs }) : rigs.map((r, i) => /* @__PURE__ */ jsx22(RigRow, { rig: r, selected: i === at && seatAt === null, seatAt: i === at ? seatAt : null, open: open5.has(r.id), roleOf: roleOf4 }, r.id)) })
43338
+ /* @__PURE__ */ jsx22(TitledBox, { title: `${o.rigs} \xB7 ${rigs.length}`, flexGrow: 1, children: !up ? /* @__PURE__ */ jsx22(Text18, { color: C.faint, children: o.stoppedDaemon }) : !rigs.length ? /* @__PURE__ */ jsx22(Text18, { color: C.faint, children: o.noRigs }) : rigs.map((r, i) => /* @__PURE__ */ jsx22(RigRow, { rig: r, selected: i === at && seatAt === null, seatAt: i === at ? seatAt : null, open: open6.has(r.id), roleOf: roleOf4 }, r.id)) })
42944
43339
  ] });
42945
43340
  }
42946
- function RigRow({ rig, selected, seatAt, open: open5, roleOf: roleOf4 }) {
43341
+ function RigRow({ rig, selected, seatAt, open: open6, roleOf: roleOf4 }) {
42947
43342
  const { t } = useTui();
42948
43343
  const o = t.openrig;
42949
43344
  const look = rigLook(rig), l = LOOK[look];
@@ -42953,14 +43348,14 @@ function RigRow({ rig, selected, seatAt, open: open5, roleOf: roleOf4 }) {
42953
43348
  return /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", children: [
42954
43349
  /* @__PURE__ */ jsx22(Box17, { backgroundColor: selected ? C.sel : void 0, children: /* @__PURE__ */ jsxs17(Text18, { wrap: "truncate", children: [
42955
43350
  /* @__PURE__ */ jsx22(Text18, { color: selected ? C.coral : C.faint, children: selected ? "\u258C" : " " }),
42956
- /* @__PURE__ */ jsx22(Text18, { color: C.dim, children: open5 ? "\u25BE " : "\u25B8 " }),
43351
+ /* @__PURE__ */ jsx22(Text18, { color: C.dim, children: open6 ? "\u25BE " : "\u25B8 " }),
42957
43352
  /* @__PURE__ */ jsx22(Text18, { color: C.fg, bold: true, children: rig.name.padEnd(16) }),
42958
43353
  /* @__PURE__ */ jsx22(Text18, { color: l.color(), children: ` ${l.glyph} ${o.look[look]}`.padEnd(18) }),
42959
43354
  /* @__PURE__ */ jsx22(Text18, { color: C.fg, children: o.seats(rig.running, rig.seats).padEnd(7) }),
42960
43355
  note ? /* @__PURE__ */ jsx22(Text18, { color: note.color, children: note.text.padEnd(34) }) : /* @__PURE__ */ jsx22(Text18, { children: "".padEnd(34) }),
42961
43356
  /* @__PURE__ */ jsx22(Text18, { color: C.dim, children: when })
42962
43357
  ] }) }),
42963
- open5 ? rig.nodes.map((n, i) => {
43358
+ open6 ? rig.nodes.map((n, i) => {
42964
43359
  const s = i === seatAt, bg = s ? C.sel : void 0, role = roleOf4(rig.name, n.logicalId);
42965
43360
  return /* @__PURE__ */ jsx22(Box17, { backgroundColor: bg, children: /* @__PURE__ */ jsxs17(Text18, { wrap: "truncate", children: [
42966
43361
  /* @__PURE__ */ jsx22(Text18, { color: C.coral, children: s ? "\u258C" : " " }),
@@ -43472,7 +43867,7 @@ function Shell({ client, lang, setLang: setLangState, saveLang, theme, setThemeS
43472
43867
  const attach2 = useCallback3((s) => {
43473
43868
  const cmd = attachCommand(fleet, s);
43474
43869
  if ("why" in cmd) {
43475
- toast({ text: cmd.why === "offline" ? t.toast.offline(s.name) : cmd.why === "omp" ? t.toast.noTerminalOmp(s.name) : t.toast.noTerminal(s.name), tone: "info" });
43870
+ toast({ text: cmd.why === "offline" ? t.toast.offline(s.name) : cmd.why === "omp" ? t.toast.noTerminalOmp(s.name) : cmd.why === "stopped" ? t.toast.seatStopped(s.name) : t.toast.noTerminal(s.name), tone: "info" });
43476
43871
  return;
43477
43872
  }
43478
43873
  void suspendTerminal(async () => {
@@ -43772,7 +44167,7 @@ var init_glyphs = __esm({
43772
44167
 
43773
44168
  // src/tui/settings.ts
43774
44169
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync13, renameSync as renameSync5, writeFileSync as writeFileSync7 } from "fs";
43775
- import { join as join38 } from "path";
44170
+ import { join as join39 } from "path";
43776
44171
  import { z as z32 } from "zod";
43777
44172
  function defaultLang(env) {
43778
44173
  const locale = env.LC_ALL || env.LC_MESSAGES || env.LANG || "";
@@ -43798,14 +44193,14 @@ var init_settings = __esm({
43798
44193
  "use strict";
43799
44194
  init_paths();
43800
44195
  SettingsSchema = z32.object({ lang: z32.enum(["fr", "en"]).optional(), theme: z32.enum(["auto", "dark", "light"]).optional() }).passthrough();
43801
- settingsPath = () => join38(ledgeDir(), "tui.json");
44196
+ settingsPath = () => join39(ledgeDir(), "tui.json");
43802
44197
  }
43803
44198
  });
43804
44199
 
43805
44200
  // src/tui/terminalProfile.ts
43806
44201
  import { execFile as execFile10, spawn as spawn6 } from "child_process";
43807
- import { mkdir as mkdir14, readFile as readFile18, realpath as realpath5, writeFile as writeFile18 } from "fs/promises";
43808
- import { dirname as dirname13, join as join39 } from "path";
44202
+ import { mkdir as mkdir14, readFile as readFile19, realpath as realpath5, writeFile as writeFile18 } from "fs/promises";
44203
+ import { dirname as dirname13, join as join40 } from "path";
43809
44204
  import { fileURLToPath as fileURLToPath4 } from "url";
43810
44205
  import { promisify } from "util";
43811
44206
  function movesToLedgeTab(env, here, script) {
@@ -43833,13 +44228,13 @@ function fragment(commandline, iconWin) {
43833
44228
  }
43834
44229
  async function profileLoaded(localAppData) {
43835
44230
  for (const rel of SETTINGS) {
43836
- const text7 = await readFile18(join39(localAppData, rel), "utf8").catch(() => "");
44231
+ const text7 = await readFile19(join40(localAppData, rel), "utf8").catch(() => "");
43837
44232
  if (new RegExp(`"source"\\s*:\\s*"${PROFILE}"`).test(text7)) return true;
43838
44233
  }
43839
44234
  return false;
43840
44235
  }
43841
44236
  async function writeIfChanged2(path3, content3) {
43842
- const current2 = await readFile18(path3).catch(() => null);
44237
+ const current2 = await readFile19(path3).catch(() => null);
43843
44238
  if (current2 && current2.equals(Buffer.from(content3))) return false;
43844
44239
  await writeFile18(path3, content3);
43845
44240
  return true;
@@ -43851,9 +44246,9 @@ async function openInLedgeTab(o) {
43851
44246
  await writeIfChanged2(o.launcher, launcherScript(o.node, o.script, o.ledgeHome));
43852
44247
  const dir = fragmentDir(o.localAppData.linux);
43853
44248
  await mkdir14(dir, { recursive: true });
43854
- await writeIfChanged2(join39(dir, "ledge.ico"), await readFile18(o.icon ?? ICON));
44249
+ await writeIfChanged2(join40(dir, "ledge.ico"), await readFile19(o.icon ?? ICON));
43855
44250
  const iconWin = `${o.localAppData.win}\\Microsoft\\Windows Terminal\\Fragments\\${PROFILE}\\ledge.ico`;
43856
- await writeIfChanged2(join39(dir, "ledge.json"), fragment(commandline, iconWin));
44251
+ await writeIfChanged2(join40(dir, "ledge.json"), fragment(commandline, iconWin));
43857
44252
  const deadline = Date.now() + (o.waitMs ?? 15e3);
43858
44253
  for (let first = true; !await profileLoaded(o.localAppData.linux); first = false) {
43859
44254
  if (Date.now() > deadline) return false;
@@ -43874,11 +44269,11 @@ async function openTuiInLedgeTab(env, onWait) {
43874
44269
  distro: env.WSL_DISTRO_NAME,
43875
44270
  node: await realpath5(process.execPath),
43876
44271
  script: await realpath5(process.argv[1]),
43877
- launcher: join39(ledgeDir(), "tui.sh"),
44272
+ launcher: join40(ledgeDir(), "tui.sh"),
43878
44273
  ledgeHome: env.LEDGE_HOME,
43879
44274
  onWait,
43880
44275
  wt: (args) => new Promise((resolve13, reject) => {
43881
- const child = spawn6(join39(linux, "Microsoft", "WindowsApps", "wt.exe"), args, { cwd: "/mnt/c", stdio: "ignore", detached: true });
44276
+ const child = spawn6(join40(linux, "Microsoft", "WindowsApps", "wt.exe"), args, { cwd: "/mnt/c", stdio: "ignore", detached: true });
43882
44277
  child.once("error", reject);
43883
44278
  child.once("spawn", () => {
43884
44279
  child.unref();
@@ -43902,7 +44297,7 @@ var init_terminalProfile = __esm({
43902
44297
  "Packages/Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe/LocalState/settings.json",
43903
44298
  "Microsoft/Windows Terminal/settings.json"
43904
44299
  ];
43905
- fragmentDir = (localAppData) => join39(localAppData, "Microsoft", "Windows Terminal", "Fragments", PROFILE);
44300
+ fragmentDir = (localAppData) => join40(localAppData, "Microsoft", "Windows Terminal", "Fragments", PROFILE);
43906
44301
  PROFILE_ID = "{d10fee13-0978-595f-a851-b60ec80f11c9}";
43907
44302
  shQuote2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
43908
44303
  run3 = promisify(execFile10);