@sma1lboy/kobe 0.7.29 → 0.7.30

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
@@ -90,7 +90,7 @@ var init_package = __esm(() => {
90
90
  package_default = {
91
91
  $schema: "https://json.schemastore.org/package.json",
92
92
  name: "@sma1lboy/kobe",
93
- version: "0.7.29",
93
+ version: "0.7.30",
94
94
  description: "TUI orchestrator for Claude Code (codename)",
95
95
  type: "module",
96
96
  packageManager: "bun@1.3.13",
@@ -395,6 +395,9 @@ import { mkdir, readFile as readFileAsync, readdir as readdirAsync } from "fs/pr
395
395
  function shQuote(s) {
396
396
  return `'${s.replace(/'/g, "'\\''")}'`;
397
397
  }
398
+ function shToken(s) {
399
+ return /^[A-Za-z0-9_@%+=:,./-]+$/.test(s) ? s : shQuote(s);
400
+ }
398
401
  function shJoin(argv) {
399
402
  return argv.map(shQuote).join(" ");
400
403
  }
@@ -545,7 +548,8 @@ class RemoteExecHost {
545
548
  }
546
549
  wrapCommand(command, opts = {}) {
547
550
  const remote = opts.cwd ? `cd ${shQuote(opts.cwd)} && ${command}` : command;
548
- return `${sshConnectArgs(this.spec, { tty: opts.tty }).join(" ")} ${shQuote(remote)}`;
551
+ const connect = sshConnectArgs(this.spec, { tty: opts.tty }).map(shToken).join(" ");
552
+ return `${connect} ${shQuote(remote)}`;
549
553
  }
550
554
  }
551
555
  var defaultSpawner = (argv, env) => {
@@ -1025,7 +1029,20 @@ var init_add_remote = __esm(() => {
1025
1029
  });
1026
1030
 
1027
1031
  // src/types/task.ts
1028
- var toTaskId = (id) => id, DEFAULT_TASK_VENDOR = "claude";
1032
+ function isTaskStatus(value) {
1033
+ return typeof value === "string" && TASK_STATUSES.includes(value);
1034
+ }
1035
+ var toTaskId = (id) => id, DEFAULT_TASK_VENDOR = "claude", TASK_STATUSES;
1036
+ var init_task = __esm(() => {
1037
+ TASK_STATUSES = [
1038
+ "backlog",
1039
+ "in_progress",
1040
+ "in_review",
1041
+ "done",
1042
+ "canceled",
1043
+ "error"
1044
+ ];
1045
+ });
1029
1046
 
1030
1047
  // src/orchestrator/index/ulid.ts
1031
1048
  function encodeTime(now, len) {
@@ -1363,7 +1380,7 @@ function coerceTask(value) {
1363
1380
  if (typeof v.id !== "string" || typeof v.title !== "string" || typeof v.repo !== "string" || typeof v.branch !== "string" || typeof v.worktreePath !== "string" || typeof v.status !== "string" || typeof v.createdAt !== "string" || typeof v.updatedAt !== "string") {
1364
1381
  return null;
1365
1382
  }
1366
- if (!isTaskStatus(v.status))
1383
+ if (!isTaskStatus2(v.status))
1367
1384
  return null;
1368
1385
  const archived = typeof v.archived === "boolean" ? v.archived : false;
1369
1386
  const kind = v.kind === "main" ? "main" : "task";
@@ -1420,11 +1437,12 @@ function isPRCheckState(v) {
1420
1437
  function isVendorId(v) {
1421
1438
  return v === "claude" || v === "codex";
1422
1439
  }
1423
- function isTaskStatus(s) {
1440
+ function isTaskStatus2(s) {
1424
1441
  return s === "backlog" || s === "in_progress" || s === "in_review" || s === "done" || s === "canceled" || s === "error";
1425
1442
  }
1426
1443
  var CURRENT_VERSION2 = 3;
1427
1444
  var init_store2 = __esm(() => {
1445
+ init_task();
1428
1446
  init_ulid();
1429
1447
  });
1430
1448
 
@@ -3121,9 +3139,10 @@ function deriveTitleFromPrompt(prompt) {
3121
3139
  const collapsed = prompt.replace(/\s+/g, " ").trim();
3122
3140
  if (collapsed.length === 0)
3123
3141
  return "";
3124
- if (collapsed.length <= TITLE_CHAR_CAP)
3142
+ const points = [...collapsed];
3143
+ if (points.length <= TITLE_CHAR_CAP)
3125
3144
  return collapsed;
3126
- return `${collapsed.slice(0, TITLE_CHAR_CAP)}\u2026`;
3145
+ return `${points.slice(0, TITLE_CHAR_CAP).join("")}\u2026`;
3127
3146
  }
3128
3147
  function autoBranch(title, taskId) {
3129
3148
  const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
@@ -3689,11 +3708,6 @@ class Orchestrator {
3689
3708
  return this.tasksAcc;
3690
3709
  }
3691
3710
  subscribeTasks(listener) {
3692
- try {
3693
- listener(this.store.list());
3694
- } catch (err) {
3695
- console.error("[kobe Orchestrator] task listener threw on subscribe:", err);
3696
- }
3697
3711
  return this.store.subscribe(listener);
3698
3712
  }
3699
3713
  dispose() {
@@ -3982,6 +3996,7 @@ var PLACEHOLDER_TASK_TITLE = "(new task)";
3982
3996
  var init_core = __esm(() => {
3983
3997
  init_dev();
3984
3998
  init_repos();
3999
+ init_task();
3985
4000
  init_errors();
3986
4001
  init_slug_allocator();
3987
4002
  });
@@ -4154,6 +4169,7 @@ var init_client_log = __esm(() => {
4154
4169
 
4155
4170
  // ../kobe-daemon/src/client/index.ts
4156
4171
  import { connect } from "net";
4172
+ import { StringDecoder } from "string_decoder";
4157
4173
 
4158
4174
  class KobeDaemonClient {
4159
4175
  socketPath;
@@ -4274,7 +4290,9 @@ class KobeDaemonClient {
4274
4290
  };
4275
4291
  socket.once("connect", onConnect);
4276
4292
  socket.once("error", onError);
4277
- socket.on("data", (chunk) => this.onData(chunk.toString("utf8")));
4293
+ const decoder = new StringDecoder("utf8");
4294
+ this.buffer = "";
4295
+ socket.on("data", (chunk) => this.onData(decoder.write(chunk)));
4278
4296
  socket.on("close", () => this.onSocketClose(socket));
4279
4297
  });
4280
4298
  }
@@ -5067,6 +5085,24 @@ function normalizeClaudeContent(content) {
5067
5085
  return out;
5068
5086
  }
5069
5087
 
5088
+ // src/engine/claude-code-local/synthetic.ts
5089
+ function isSyntheticClaudeRecord(record) {
5090
+ return record.isMeta === true || record.isCompactSummary === true;
5091
+ }
5092
+ function isClaudeCommandBreadcrumb(blocks) {
5093
+ if (blocks.length === 0)
5094
+ return false;
5095
+ for (const b of blocks) {
5096
+ if (b.type !== "text")
5097
+ return false;
5098
+ const t = b.text.trim();
5099
+ if (!t.startsWith("<command-name>") && !t.startsWith("<command-message>") && !t.startsWith("<local-command")) {
5100
+ return false;
5101
+ }
5102
+ }
5103
+ return true;
5104
+ }
5105
+
5070
5106
  // src/engine/claude-code-local/history.ts
5071
5107
  import { appendFile as appendFile2, mkdir as mkdir4, readFile as readFile2, readdir, stat, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
5072
5108
  import { homedir as homedir8 } from "os";
@@ -5148,6 +5184,8 @@ function parseJsonl(raw, sessionId) {
5148
5184
  return out;
5149
5185
  }
5150
5186
  function extractMessage(record, fallbackSessionId) {
5187
+ if (isSyntheticClaudeRecord(record))
5188
+ return null;
5151
5189
  const inner = isObject(record.message) ? record.message : record;
5152
5190
  const role = inner.role;
5153
5191
  if (role !== "user" && role !== "assistant" && role !== "system")
@@ -5155,6 +5193,8 @@ function extractMessage(record, fallbackSessionId) {
5155
5193
  if (!("content" in inner))
5156
5194
  return null;
5157
5195
  const blocks = normalizeClaudeContent(inner.content);
5196
+ if (role === "user" && isClaudeCommandBreadcrumb(blocks))
5197
+ return null;
5158
5198
  const ts = typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString();
5159
5199
  const sid = typeof record.sessionId === "string" ? record.sessionId : fallbackSessionId;
5160
5200
  const usage = extractUsage(inner.usage);
@@ -5557,9 +5597,12 @@ async function listRolloutFiles(deps = defaultDeps7) {
5557
5597
  return out;
5558
5598
  }
5559
5599
  async function findRolloutFile(sessionId, deps = defaultDeps7) {
5600
+ if (!sessionId)
5601
+ return;
5602
+ const want = sessionId.toLowerCase();
5560
5603
  const all = await listRolloutFiles(deps);
5561
5604
  for (const p of all) {
5562
- if (path8.basename(p).endsWith(`-${sessionId}.jsonl`))
5605
+ if (path8.basename(p).match(UUID_AT_END)?.[1]?.toLowerCase() === want)
5563
5606
  return p;
5564
5607
  }
5565
5608
  return;
@@ -5855,7 +5898,7 @@ function deriveCodexUsageMetrics(raw) {
5855
5898
  if (timestampMs !== null && (latestUsageTimestampMs === null || timestampMs > latestUsageTimestampMs)) {
5856
5899
  latestUsageTimestampMs = timestampMs;
5857
5900
  latestUsage = snapshot;
5858
- } else if (latestUsage === undefined) {
5901
+ } else if (latestUsageTimestampMs === null) {
5859
5902
  latestUsage = snapshot;
5860
5903
  }
5861
5904
  }
@@ -6426,6 +6469,7 @@ async function deriveTitleFromSessionId(vendor, sessionId) {
6426
6469
  var MAX_SESSIONS_SCANNED = 8;
6427
6470
  var init_auto_title = __esm(() => {
6428
6471
  init_registry();
6472
+ init_task();
6429
6473
  });
6430
6474
 
6431
6475
  // src/tui/panes/terminal/launch.ts
@@ -6859,6 +6903,7 @@ var realRunner, realDeps;
6859
6903
  var init_chat_tab_naming = __esm(() => {
6860
6904
  init_auto_title();
6861
6905
  init_client2();
6906
+ init_task();
6862
6907
  realRunner = { capture: runTmuxCapturing, run: runTmux };
6863
6908
  realDeps = {
6864
6909
  runner: realRunner,
@@ -6953,6 +6998,7 @@ var init_auto_title_poller = __esm(() => {
6953
6998
  init_auto_title();
6954
6999
  init_core();
6955
7000
  init_chat_tab_naming();
7001
+ init_task();
6956
7002
  });
6957
7003
 
6958
7004
  // ../kobe-daemon/src/daemon/event-bus.ts
@@ -7261,24 +7307,16 @@ function createDaemonHandlerRegistry() {
7261
7307
  async handle(payload, ctx) {
7262
7308
  const taskId = requireString(payload, "taskId");
7263
7309
  const status = requireString(payload, "status");
7264
- if (status !== "backlog" && status !== "in_progress" && status !== "in_review" && status !== "done" && status !== "canceled" && status !== "error") {
7310
+ if (!isTaskStatus(status))
7265
7311
  throw new Error("status must be a TaskStatus");
7266
- }
7267
7312
  const linked = status === "done" ? ctx.orch.getTask(taskId) : undefined;
7268
7313
  const prevStatus = linked?.status;
7269
7314
  await ctx.orch.setStatus(taskId, status);
7270
7315
  if (status === "done" && prevStatus !== "done" && linked) {
7271
7316
  try {
7272
- const state = await ctx.issues.list(linked.repo);
7273
- const found = state.issues.find((i) => i.taskId === taskId);
7274
- if (found && found.status !== "done") {
7275
- const next = await ctx.issues.mutate(linked.repo, {
7276
- type: "setStatus",
7277
- id: found.id,
7278
- status: "done"
7279
- });
7317
+ const next = await ctx.issues.mirrorTaskDone(linked.repo, taskId);
7318
+ if (next)
7280
7319
  ctx.bus.publish("issue.snapshot", next);
7281
- }
7282
7320
  } catch (err) {
7283
7321
  logDaemonError("issue-done-mirror", err);
7284
7322
  }
@@ -7524,6 +7562,7 @@ function optionalActivityDetail(payload) {
7524
7562
  var init_handlers = __esm(() => {
7525
7563
  init_hook_events();
7526
7564
  init_status_rules();
7565
+ init_task();
7527
7566
  init_version();
7528
7567
  init_cwd_task();
7529
7568
  init_protocol();
@@ -7660,7 +7699,7 @@ class IssuesStore {
7660
7699
  }
7661
7700
  async list(repo) {
7662
7701
  const { repoRoot, repoKey } = await resolveRepo(repo);
7663
- return withLock(repoKey, async () => {
7702
+ return withLock(this.path, async () => {
7664
7703
  const store = await readStore(this.path);
7665
7704
  const record = store.repos[repoKey] ?? null;
7666
7705
  if (record && record.repoRoot !== repoRoot) {
@@ -7670,12 +7709,30 @@ class IssuesStore {
7670
7709
  return response(repoRoot, record);
7671
7710
  });
7672
7711
  }
7712
+ async mirrorTaskDone(repo, taskId) {
7713
+ const { repoRoot, repoKey } = await resolveRepo(repo);
7714
+ if (!taskId)
7715
+ return null;
7716
+ return withLock(this.path, async () => {
7717
+ const store = await readStore(this.path);
7718
+ const record = store.repos[repoKey];
7719
+ if (!record)
7720
+ return null;
7721
+ const issue = record.issues.find((i) => i.taskId === taskId);
7722
+ if (!issue || issue.status === "done")
7723
+ return null;
7724
+ issue.status = "done";
7725
+ record.repoRoot = repoRoot;
7726
+ await writeStore(this.path, store);
7727
+ return response(repoRoot, record);
7728
+ });
7729
+ }
7673
7730
  async mutate(repo, op) {
7674
7731
  const { repoRoot, repoKey } = await resolveRepo(repo);
7675
7732
  if (!op || typeof op !== "object" || Array.isArray(op) || typeof op.type !== "string") {
7676
7733
  throw new Error("missing op");
7677
7734
  }
7678
- return withLock(repoKey, async () => {
7735
+ return withLock(this.path, async () => {
7679
7736
  const store = await readStore(this.path);
7680
7737
  let record = store.repos[repoKey];
7681
7738
  if (!record) {
@@ -7764,6 +7821,76 @@ var init_issues_store = __esm(() => {
7764
7821
  locks = new Map;
7765
7822
  });
7766
7823
 
7824
+ // ../kobe-daemon/src/daemon/lifetime.ts
7825
+ class DaemonLifetime {
7826
+ clients;
7827
+ idleGraceMs;
7828
+ onIdleStop;
7829
+ schedule;
7830
+ log;
7831
+ cancelIdle = null;
7832
+ stopping = false;
7833
+ constructor(options) {
7834
+ this.clients = options.clients;
7835
+ this.idleGraceMs = options.idleGraceMs;
7836
+ this.onIdleStop = options.onIdleStop;
7837
+ this.schedule = options.schedule ?? defaultSchedule;
7838
+ this.log = options.log ?? logDaemonInfo;
7839
+ }
7840
+ guiCount() {
7841
+ let n = 0;
7842
+ for (const c of this.clients())
7843
+ if (c.holdsLifetime)
7844
+ n++;
7845
+ return n;
7846
+ }
7847
+ hasSubscribers() {
7848
+ for (const c of this.clients())
7849
+ if (c.subscribed)
7850
+ return true;
7851
+ return false;
7852
+ }
7853
+ isStopping() {
7854
+ return this.stopping;
7855
+ }
7856
+ markStopping() {
7857
+ this.stopping = true;
7858
+ this.clearIdle();
7859
+ }
7860
+ guiAttached() {
7861
+ this.clearIdle();
7862
+ }
7863
+ clientDisconnected(wasGui) {
7864
+ if (wasGui)
7865
+ this.maybeArm();
7866
+ }
7867
+ clearIdle() {
7868
+ if (this.cancelIdle) {
7869
+ this.cancelIdle();
7870
+ this.cancelIdle = null;
7871
+ }
7872
+ }
7873
+ maybeArm() {
7874
+ if (this.stopping || this.guiCount() > 0)
7875
+ return;
7876
+ this.clearIdle();
7877
+ this.log("idle", `last gui gone \u2014 arming ${this.idleGraceMs}ms idle-stop grace`);
7878
+ this.cancelIdle = this.schedule(() => {
7879
+ this.cancelIdle = null;
7880
+ if (this.stopping || this.guiCount() > 0)
7881
+ return;
7882
+ this.log("idle", "grace elapsed with no gui \u2014 self-stopping");
7883
+ this.onIdleStop();
7884
+ }, this.idleGraceMs);
7885
+ }
7886
+ }
7887
+ var defaultSchedule = (fn, ms) => {
7888
+ const t = setTimeout(fn, ms);
7889
+ t.unref?.();
7890
+ return () => clearTimeout(t);
7891
+ };
7892
+ var init_lifetime = () => {};
7893
+
7767
7894
  // ../kobe-daemon/src/daemon/keybindings-watcher.ts
7768
7895
  import { mkdirSync as mkdirSync2, watch } from "fs";
7769
7896
  import { homedir as homedir14 } from "os";
@@ -8125,6 +8252,7 @@ var init_worktree_changes_collector = __esm(() => {
8125
8252
  import { mkdir as mkdir7, readFile as readFile8, unlink as unlink4, writeFile as writeFile5 } from "fs/promises";
8126
8253
  import { createServer } from "net";
8127
8254
  import { dirname as dirname8 } from "path";
8255
+ import { StringDecoder as StringDecoder2 } from "string_decoder";
8128
8256
  function resolveIdleGraceMs() {
8129
8257
  const raw = process.env.KOBE_DAEMON_IDLE_GRACE_MS;
8130
8258
  if (raw === undefined)
@@ -8138,42 +8266,11 @@ async function startDaemonServer(orch, options = {}) {
8138
8266
  const startedAt = options.startedAt ?? new Date;
8139
8267
  const clients = new Set;
8140
8268
  let nextClientId = 1;
8141
- const idleGraceMs = resolveIdleGraceMs();
8142
- let idleTimer = null;
8143
- let stopping = false;
8144
- function guiCount() {
8145
- let n = 0;
8146
- for (const c of clients)
8147
- if (c.holdsLifetime)
8148
- n++;
8149
- return n;
8150
- }
8151
- function hasSubscribers() {
8152
- for (const c of clients)
8153
- if (c.subscribed)
8154
- return true;
8155
- return false;
8156
- }
8157
- function cancelIdleTimer() {
8158
- if (idleTimer) {
8159
- clearTimeout(idleTimer);
8160
- idleTimer = null;
8161
- }
8162
- }
8163
- function maybeArmIdleShutdown() {
8164
- if (stopping || guiCount() > 0)
8165
- return;
8166
- cancelIdleTimer();
8167
- logDaemonInfo("idle", `last gui gone \u2014 arming ${idleGraceMs}ms idle-stop grace`);
8168
- idleTimer = setTimeout(() => {
8169
- idleTimer = null;
8170
- if (stopping || guiCount() > 0)
8171
- return;
8172
- logDaemonInfo("idle", "grace elapsed with no gui \u2014 self-stopping");
8173
- stopSoon().catch((err) => logDaemonError("daemon-idle-shutdown", err));
8174
- }, idleGraceMs);
8175
- idleTimer.unref?.();
8176
- }
8269
+ const lifetime = new DaemonLifetime({
8270
+ clients: () => clients,
8271
+ idleGraceMs: resolveIdleGraceMs(),
8272
+ onIdleStop: () => void stopSoon().catch((err) => logDaemonError("daemon-idle-shutdown", err))
8273
+ });
8177
8274
  const bus = new DaemonEventBus;
8178
8275
  bus.onPublish((event) => {
8179
8276
  broadcast(clients, { type: "event", name: event.channel, payload: event.payload });
@@ -8194,18 +8291,18 @@ async function startDaemonServer(orch, options = {}) {
8194
8291
  channels: null
8195
8292
  };
8196
8293
  clients.add(client);
8294
+ const decoder = new StringDecoder2("utf8");
8197
8295
  socket.on("data", (chunk) => {
8198
- client.buffer += chunk.toString("utf8");
8296
+ client.buffer += decoder.write(chunk);
8199
8297
  drainClientBuffer(client);
8200
8298
  });
8201
8299
  socket.on("error", () => {});
8202
8300
  socket.on("close", () => {
8203
8301
  clients.delete(client);
8204
8302
  if (client.subscribed) {
8205
- logDaemonInfo("conn", `client #${client.id} (${client.holdsLifetime ? "gui" : "pane"}) disconnected \u2014 ${clients.size} client(s), ${guiCount()} gui left`);
8303
+ logDaemonInfo("conn", `client #${client.id} (${client.holdsLifetime ? "gui" : "pane"}) disconnected \u2014 ${clients.size} client(s), ${lifetime.guiCount()} gui left`);
8206
8304
  }
8207
- if (client.holdsLifetime)
8208
- maybeArmIdleShutdown();
8305
+ lifetime.clientDisconnected(client.holdsLifetime);
8209
8306
  });
8210
8307
  });
8211
8308
  const unsubscribeStore = orch.subscribeTasks((snapshot) => {
@@ -8223,7 +8320,7 @@ async function startDaemonServer(orch, options = {}) {
8223
8320
  updateTimer.unref?.();
8224
8321
  }
8225
8322
  const autoTitlePollMs = options.autoTitlePollMs ?? DEFAULT_AUTO_TITLE_POLL_MS;
8226
- const stopAutoTitlePoller = startAutoTitlePoller(orch, autoTitlePollMs, hasSubscribers);
8323
+ const stopAutoTitlePoller = startAutoTitlePoller(orch, autoTitlePollMs, () => lifetime.hasSubscribers());
8227
8324
  const stopUiPrefsWatcher = startUiPrefsWatcher(bus, {
8228
8325
  statePath: defaultUiPrefsStatePath(options.homeDir),
8229
8326
  debounceMs: options.uiPrefsDebounceMs ?? DEFAULT_UI_PREFS_DEBOUNCE_MS
@@ -8232,15 +8329,14 @@ async function startDaemonServer(orch, options = {}) {
8232
8329
  path: defaultKeybindingsPath(options.homeDir),
8233
8330
  debounceMs: options.keybindingsDebounceMs ?? DEFAULT_KEYBINDINGS_DEBOUNCE_MS
8234
8331
  });
8235
- const stopWorktreeChangesCollector = startWorktreeChangesCollector(orch, bus, options.worktreeChangesTickMs ?? DEFAULT_WORKTREE_CHANGES_TICK_MS, hasSubscribers);
8332
+ const stopWorktreeChangesCollector = startWorktreeChangesCollector(orch, bus, options.worktreeChangesTickMs ?? DEFAULT_WORKTREE_CHANGES_TICK_MS, () => lifetime.hasSubscribers());
8236
8333
  const serverApi = {
8237
8334
  socketPath,
8238
8335
  pidPath,
8239
8336
  startedAt,
8240
8337
  clients,
8241
8338
  async close() {
8242
- stopping = true;
8243
- cancelIdleTimer();
8339
+ lifetime.markStopping();
8244
8340
  unsubscribeStore();
8245
8341
  if (updateTimer)
8246
8342
  clearInterval(updateTimer);
@@ -8269,10 +8365,9 @@ async function startDaemonServer(orch, options = {}) {
8269
8365
  await writeFile5(pidPath, `${process.pid}
8270
8366
  `, "utf8");
8271
8367
  async function stopSoon() {
8272
- if (stopping)
8368
+ if (lifetime.isStopping())
8273
8369
  return;
8274
- stopping = true;
8275
- cancelIdleTimer();
8370
+ lifetime.markStopping();
8276
8371
  await options.onStop?.();
8277
8372
  setTimeout(() => {
8278
8373
  serverApi.close().catch((err) => logDaemonError("daemon-shutdown", err));
@@ -8289,8 +8384,8 @@ async function startDaemonServer(orch, options = {}) {
8289
8384
  client.channels = normalizeChannelFilter(payload.channels);
8290
8385
  const firstSubscriber = !wasSubscribed;
8291
8386
  if (client.holdsLifetime)
8292
- cancelIdleTimer();
8293
- logDaemonInfo("conn", `client #${client.id} subscribed as ${role}${client.channels ? ` [${[...client.channels].join(",")}]` : ""} \u2014 ${clients.size} client(s), ${guiCount()} gui${firstSubscriber ? " (collectors resume)" : ""}`);
8387
+ lifetime.guiAttached();
8388
+ logDaemonInfo("conn", `client #${client.id} subscribed as ${role}${client.channels ? ` [${[...client.channels].join(",")}]` : ""} \u2014 ${clients.size} client(s), ${lifetime.guiCount()} gui${firstSubscriber ? " (collectors resume)" : ""}`);
8294
8389
  for (const event of bus.snapshot()) {
8295
8390
  if (client.channels && !client.channels.has(event.channel))
8296
8391
  continue;
@@ -8312,7 +8407,7 @@ async function startDaemonServer(orch, options = {}) {
8312
8407
  bus,
8313
8408
  activity,
8314
8409
  issues,
8315
- daemon: { startedAt, socketPath, pid: process.pid, guiCount, stopSoon },
8410
+ daemon: { startedAt, socketPath, pid: process.pid, guiCount: () => lifetime.guiCount(), stopSoon },
8316
8411
  clientId: client.id
8317
8412
  });
8318
8413
  }
@@ -8381,6 +8476,7 @@ var init_server = __esm(() => {
8381
8476
  init_auto_title_poller();
8382
8477
  init_handlers();
8383
8478
  init_issues_store();
8479
+ init_lifetime();
8384
8480
  init_keybindings_watcher();
8385
8481
  init_paths2();
8386
8482
  init_protocol();
@@ -11091,6 +11187,68 @@ var init_tmux_border_theme = __esm(() => {
11091
11187
  FOCUS_ACCENT_SLOT_NAMES2 = ["primary", "success", "info"];
11092
11188
  });
11093
11189
 
11190
+ // src/tui/panes/terminal/layout-coord.ts
11191
+ var exports_layout_coord = {};
11192
+ __export(exports_layout_coord, {
11193
+ recordGen: () => recordGen,
11194
+ isLatestGen: () => isLatestGen,
11195
+ genAgeMs: () => genAgeMs,
11196
+ coalesceLayoutWork: () => coalesceLayoutWork,
11197
+ RESIZE_GUARD_MS: () => RESIZE_GUARD_MS,
11198
+ LAYOUT_COALESCE_MS: () => LAYOUT_COALESCE_MS
11199
+ });
11200
+ import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
11201
+ import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
11202
+ import { join as join8 } from "path";
11203
+ function coordDir() {
11204
+ return join8(kobeStateDir(), "layout-coord");
11205
+ }
11206
+ function genPath(session, kind) {
11207
+ const hash = createHash5("sha1").update(session).digest("hex").slice(0, 16);
11208
+ return join8(coordDir(), `${hash}.${kind}`);
11209
+ }
11210
+ function recordGen(session, kind) {
11211
+ const nonce = randomUUID2();
11212
+ try {
11213
+ mkdirSync5(coordDir(), { recursive: true });
11214
+ const path11 = genPath(session, kind);
11215
+ const tmp = `${path11}.${nonce}.tmp`;
11216
+ writeFileSync2(tmp, `${Date.now()}
11217
+ ${nonce}`);
11218
+ renameSync2(tmp, path11);
11219
+ } catch {}
11220
+ return nonce;
11221
+ }
11222
+ function isLatestGen(session, kind, nonce) {
11223
+ try {
11224
+ return readFileSync8(genPath(session, kind), "utf8").split(`
11225
+ `)[1]?.trim() === nonce;
11226
+ } catch {
11227
+ return true;
11228
+ }
11229
+ }
11230
+ function genAgeMs(session, kind, now = Date.now()) {
11231
+ try {
11232
+ const ts = Number.parseInt(readFileSync8(genPath(session, kind), "utf8").split(`
11233
+ `)[0] ?? "", 10);
11234
+ return Number.isFinite(ts) ? now - ts : Number.POSITIVE_INFINITY;
11235
+ } catch {
11236
+ return Number.POSITIVE_INFINITY;
11237
+ }
11238
+ }
11239
+ async function coalesceLayoutWork(session, kind, work, debounceMs = LAYOUT_COALESCE_MS) {
11240
+ const nonce = recordGen(session, kind);
11241
+ if (debounceMs > 0)
11242
+ await new Promise((resolve5) => setTimeout(resolve5, debounceMs));
11243
+ if (!isLatestGen(session, kind, nonce))
11244
+ return;
11245
+ await work();
11246
+ }
11247
+ var LAYOUT_COALESCE_MS = 120, RESIZE_GUARD_MS = 400;
11248
+ var init_layout_coord = __esm(() => {
11249
+ init_env();
11250
+ });
11251
+
11094
11252
  // src/tui/panes/terminal/pane-heal.ts
11095
11253
  function parseKobePaneRows(stdout) {
11096
11254
  const rows = [];
@@ -11204,6 +11362,7 @@ async function globalRightColumnResizeArgs() {
11204
11362
  return (await globalLayoutPrefs()).rcArgs;
11205
11363
  }
11206
11364
  async function healWorkspaceLayout(session, versions) {
11365
+ recordGen(session, "resize");
11207
11366
  const { tasksWidth, rcArgs } = await globalLayoutPrefs();
11208
11367
  const rows = await listKobePanes(session);
11209
11368
  if (!rows)
@@ -11241,7 +11400,7 @@ async function captureGlobalLayout(session) {
11241
11400
  "-t",
11242
11401
  `=${session}`,
11243
11402
  "-F",
11244
- "#{@kobe_role}\t#{pane_width}\t#{pane_height}\t#{window_width}\t#{window_height}"
11403
+ "#{@kobe_role}\t#{pane_width}\t#{pane_height}\t#{window_width}\t#{window_height}\t#{window_zoomed_flag}"
11245
11404
  ]);
11246
11405
  if (code !== 0)
11247
11406
  return;
@@ -11249,6 +11408,8 @@ async function captureGlobalLayout(session) {
11249
11408
  `).map((line) => line.split("\t")).filter((cols) => (cols[0]?.trim() ?? "") !== "");
11250
11409
  if (rows.length === 0)
11251
11410
  return;
11411
+ if (rows.some((cols) => cols[5]?.trim() === "1"))
11412
+ return;
11252
11413
  const winW = Number.parseInt(rows[0][3]?.trim() ?? "", 10);
11253
11414
  const winH = Number.parseInt(rows[0][4]?.trim() ?? "", 10);
11254
11415
  const sets = [];
@@ -11272,6 +11433,28 @@ async function captureGlobalLayout(session) {
11272
11433
  if (sets.length > 0)
11273
11434
  await runTmuxSequence(sets);
11274
11435
  }
11436
+ function shouldCaptureDrag(stdout) {
11437
+ const rows = stdout.split(`
11438
+ `).map((line) => line.split("\t")).filter((cols) => (cols[0]?.trim() ?? "") !== "");
11439
+ if (rows.length === 0)
11440
+ return false;
11441
+ if (rows.some((cols) => cols[1]?.trim() === "1"))
11442
+ return false;
11443
+ const roles = new Set(rows.map((cols) => cols[0]?.trim()));
11444
+ return roles.has("tasks") && roles.has("ops");
11445
+ }
11446
+ async function captureGlobalLayoutOnDrag(session) {
11447
+ const { code, stdout } = await runTmuxCapturing([
11448
+ "list-panes",
11449
+ "-t",
11450
+ `=${session}`,
11451
+ "-F",
11452
+ "#{@kobe_role}\t#{window_zoomed_flag}"
11453
+ ]);
11454
+ if (code !== 0 || !shouldCaptureDrag(stdout))
11455
+ return;
11456
+ await captureGlobalLayout(session);
11457
+ }
11275
11458
  async function refreshKobeWorkspacePanes(session) {
11276
11459
  const sessionOptions = await getSessionOptions(session, ["@kobe_worktree", "@kobe_task", "@kobe_vendor"]);
11277
11460
  const cwd = sessionOptions["@kobe_worktree"] || process.cwd();
@@ -11298,6 +11481,7 @@ var init_pane_heal = __esm(() => {
11298
11481
  init_tmux_border_theme();
11299
11482
  init_version();
11300
11483
  init_launch();
11484
+ init_layout_coord();
11301
11485
  KOBE_PANE_LIST_FORMAT = `#{window_id} #{pane_id} #{@kobe_role} #{${PANE_VERSION_OPTION}} #{pane_width}`;
11302
11486
  });
11303
11487
 
@@ -11527,6 +11711,7 @@ __export(exports_tmux, {
11527
11711
  selectTasksPane: () => selectTasksPane,
11528
11712
  refreshKobeWorkspacePanes: () => refreshKobeWorkspacePanes,
11529
11713
  quickCreate: () => quickCreate,
11714
+ prepareWindowForSwitch: () => prepareWindowForSwitch,
11530
11715
  prepareWindowForAttach: () => prepareWindowForAttach,
11531
11716
  parseObservedSession: () => parseObservedSession,
11532
11717
  openUpdateTab: () => openUpdateTab,
@@ -11544,6 +11729,7 @@ __export(exports_tmux, {
11544
11729
  chatTabRenameBinding: () => chatTabRenameBinding,
11545
11730
  chatTabCloseBinding: () => chatTabCloseBinding,
11546
11731
  chatTabChooseEngineBindings: () => chatTabChooseEngineBindings,
11732
+ captureGlobalLayoutOnDrag: () => captureGlobalLayoutOnDrag,
11547
11733
  captureGlobalLayout: () => captureGlobalLayout,
11548
11734
  attachArgv: () => attachArgv,
11549
11735
  PANE_VERSION_OPTION: () => PANE_VERSION_OPTION,
@@ -11562,11 +11748,26 @@ function positiveInt(value) {
11562
11748
  return Number.isInteger(n) && n > 0 ? n : undefined;
11563
11749
  }
11564
11750
  async function prepareWindowForAttach(session) {
11751
+ recordGen(session, "resize");
11565
11752
  const sizeArgs = tmuxInitialSizeArgs();
11566
11753
  if (sizeArgs.length > 0)
11567
11754
  await runTmux(["resize-window", "-t", `=${session}`, ...sizeArgs]);
11568
11755
  await healWorkspaceLayout(session);
11569
11756
  }
11757
+ async function attachedWindowSizeArgs() {
11758
+ const { code, stdout } = await runTmuxCapturing(["display-message", "-p", "#{window_width}\t#{window_height}"]);
11759
+ if (code !== 0)
11760
+ return [];
11761
+ const [w, h] = stdout.trim().split("\t").map((s) => Number.parseInt(s, 10));
11762
+ return Number.isInteger(w) && w > 0 && Number.isInteger(h) && h > 0 ? ["-x", `${w}`, "-y", `${h}`] : [];
11763
+ }
11764
+ async function prepareWindowForSwitch(session) {
11765
+ recordGen(session, "resize");
11766
+ const sizeArgs = await attachedWindowSizeArgs();
11767
+ if (sizeArgs.length > 0)
11768
+ await runTmux(["resize-window", "-t", `=${session}`, ...sizeArgs]);
11769
+ await healWorkspaceLayout(session);
11770
+ }
11570
11771
  function focusBindCommand(key, dir) {
11571
11772
  return [
11572
11773
  "bind-key",
@@ -11698,6 +11899,8 @@ async function ensureSessionImpl(opts) {
11698
11899
  const focusTasksTmuxCommand = `run-shell ${shellQuote(focusTasksCommand)}`;
11699
11900
  const healLayoutCommand = `${envStr}${invStr} heal-layout --session '#{session_name}'`;
11700
11901
  const healLayoutTmuxCommand = `run-shell -b ${shellQuote(healLayoutCommand)}`;
11902
+ const captureLayoutCommand = `${envStr}${invStr} capture-layout --session '#{session_name}'`;
11903
+ const captureLayoutTmuxCommand = `run-shell -b ${shellQuote(captureLayoutCommand)}`;
11701
11904
  const userKeys = resolveUserTmuxKeys();
11702
11905
  const unbinds = [];
11703
11906
  if (userKeys.overridden.has(TMUX_FOCUS_ID)) {
@@ -11740,6 +11943,7 @@ async function ensureSessionImpl(opts) {
11740
11943
  ],
11741
11944
  ["set-option", "-g", "mouse", "on"],
11742
11945
  ["set-hook", "-g", "window-resized", healLayoutTmuxCommand],
11946
+ ["set-hook", "-g", "window-layout-changed", captureLayoutTmuxCommand],
11743
11947
  ...unbinds,
11744
11948
  ...b["tmux.detach"] ? [
11745
11949
  [
@@ -11790,6 +11994,7 @@ var init_tmux = __esm(() => {
11790
11994
  init_tmux_border_theme();
11791
11995
  init_chattab();
11792
11996
  init_launch();
11997
+ init_layout_coord();
11793
11998
  init_pane_heal();
11794
11999
  init_client2();
11795
12000
  init_chattab();
@@ -11808,17 +12013,17 @@ var exports_repo_init = {};
11808
12013
  __export(exports_repo_init, {
11809
12014
  resolveRepoInit: () => resolveRepoInit
11810
12015
  });
11811
- import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
11812
- import { join as join8 } from "path";
12016
+ import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
12017
+ import { join as join9 } from "path";
11813
12018
  function repoFileScript(worktreePath) {
11814
- return existsSync7(join8(worktreePath, INIT_SCRIPT_REL)) ? `sh ${INIT_SCRIPT_REL}` : undefined;
12019
+ return existsSync7(join9(worktreePath, INIT_SCRIPT_REL)) ? `sh ${INIT_SCRIPT_REL}` : undefined;
11815
12020
  }
11816
12021
  function repoFilePrompt(worktreePath) {
11817
- const p = join8(worktreePath, INIT_PROMPT_REL);
12022
+ const p = join9(worktreePath, INIT_PROMPT_REL);
11818
12023
  if (!existsSync7(p))
11819
12024
  return;
11820
12025
  try {
11821
- const text = readFileSync8(p, "utf8");
12026
+ const text = readFileSync9(p, "utf8");
11822
12027
  return text.trim().length > 0 ? text : undefined;
11823
12028
  } catch {
11824
12029
  return;
@@ -11836,8 +12041,8 @@ function resolveRepoInit(repoRoot, worktreePath) {
11836
12041
  var INIT_SCRIPT_REL, INIT_PROMPT_REL;
11837
12042
  var init_repo_init = __esm(() => {
11838
12043
  init_repos();
11839
- INIT_SCRIPT_REL = join8(".kobe", "init.sh");
11840
- INIT_PROMPT_REL = join8(".kobe", "init-prompt.md");
12044
+ INIT_SCRIPT_REL = join9(".kobe", "init.sh");
12045
+ INIT_PROMPT_REL = join9(".kobe", "init-prompt.md");
11841
12046
  });
11842
12047
 
11843
12048
  // src/cli/api-cmd.ts
@@ -12052,6 +12257,9 @@ function parseAgentsSpec(spec) {
12052
12257
  if (!Number.isInteger(count) || count <= 0) {
12053
12258
  throw new ApiError(`--agents count for "${vendor}" must be a positive integer`, "BAD_FLAG");
12054
12259
  }
12260
+ if (out.length + count > FANOUT_CAP) {
12261
+ throw new ApiError(`--agents requests ${out.length + count} agents, exceeds the cap of ${FANOUT_CAP}`, "BAD_FLAG");
12262
+ }
12055
12263
  for (let i = 0;i < count; i++)
12056
12264
  out.push(vendor);
12057
12265
  }
@@ -12501,7 +12709,7 @@ ${apiUsage()}`, "BAD_VERB", 2);
12501
12709
  session?.close();
12502
12710
  }
12503
12711
  }
12504
- var API_SCHEMA_VERSION = 2, FANOUT_CAP = 10, TASK_STATUSES, ISSUE_STATUSES2, ApiError, F, VERB_ALIASES, VERB_GROUPS, VERBS, API_VERBS, GLOBAL_FLAGS, realPromptDeliveryOps, defaultApiRuntime;
12712
+ var API_SCHEMA_VERSION = 2, FANOUT_CAP = 10, TASK_STATUSES2, ISSUE_STATUSES2, ApiError, F, VERB_ALIASES, VERB_GROUPS, VERBS, API_VERBS, GLOBAL_FLAGS, realPromptDeliveryOps, defaultApiRuntime;
12505
12713
  var init_api_cmd = __esm(() => {
12506
12714
  init_interactive_command();
12507
12715
  init_feedback();
@@ -12510,7 +12718,7 @@ var init_api_cmd = __esm(() => {
12510
12718
  init_vendor();
12511
12719
  init_version();
12512
12720
  init_daemon_session();
12513
- TASK_STATUSES = ["backlog", "in_progress", "in_review", "done", "canceled", "error"];
12721
+ TASK_STATUSES2 = ["backlog", "in_progress", "in_review", "done", "canceled", "error"];
12514
12722
  ISSUE_STATUSES2 = ["open", "doing", "hold", "done"];
12515
12723
  ApiError = class ApiError extends Error {
12516
12724
  code;
@@ -12602,7 +12810,7 @@ var init_api_cmd = __esm(() => {
12602
12810
  {
12603
12811
  name: "status",
12604
12812
  type: "enum",
12605
- values: TASK_STATUSES,
12813
+ values: TASK_STATUSES2,
12606
12814
  default: "backlog",
12607
12815
  description: "Initial lifecycle status."
12608
12816
  },
@@ -12755,7 +12963,7 @@ var init_api_cmd = __esm(() => {
12755
12963
  summary: "Set a task's lifecycle status.",
12756
12964
  flags: [
12757
12965
  F.taskId(),
12758
- { name: "status", type: "enum", required: true, values: TASK_STATUSES, description: "New status." }
12966
+ { name: "status", type: "enum", required: true, values: TASK_STATUSES2, description: "New status." }
12759
12967
  ],
12760
12968
  handler: (ctx) => simpleRpc(ctx, "task.status", {
12761
12969
  taskId: ctx.args.require("task-id"),
@@ -12948,8 +13156,8 @@ var exports_theme = {};
12948
13156
  __export(exports_theme, {
12949
13157
  runThemeSubcommand: () => runThemeSubcommand
12950
13158
  });
12951
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync9, readdirSync as readdirSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
12952
- import { basename as basename5, join as join9, resolve as resolve6 } from "path";
13159
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync10, readdirSync as readdirSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
13160
+ import { basename as basename5, join as join10, resolve as resolve6 } from "path";
12953
13161
  function fail3(message) {
12954
13162
  process.stderr.write(`kobe theme: ${message}
12955
13163
  `);
@@ -12980,7 +13188,7 @@ function listThemes() {
12980
13188
  } else {
12981
13189
  for (const f of userFiles) {
12982
13190
  const name = f.slice(0, -".json".length);
12983
- const path11 = join9(dir, f);
13191
+ const path11 = join10(dir, f);
12984
13192
  const overridesBundled = BUNDLED_NAMES.includes(name) ? " (overrides built-in)" : "";
12985
13193
  lines.push(` ${name}${overridesBundled} ${path11}`);
12986
13194
  }
@@ -13009,7 +13217,7 @@ async function readSource(source) {
13009
13217
  const abs = resolve6(process.cwd(), source);
13010
13218
  let text;
13011
13219
  try {
13012
- text = readFileSync9(abs, "utf8");
13220
+ text = readFileSync10(abs, "utf8");
13013
13221
  } catch (err) {
13014
13222
  fail3(`failed to read ${abs}: ${err instanceof Error ? err.message : String(err)}`);
13015
13223
  }
@@ -13069,12 +13277,12 @@ async function addTheme(args) {
13069
13277
  fail3(`invalid theme name "${name}" (use letters, digits, '.', '_', '-')`);
13070
13278
  }
13071
13279
  const dir = userThemesDir();
13072
- mkdirSync5(dir, { recursive: true });
13073
- const dest = join9(dir, `${name}.json`);
13280
+ mkdirSync6(dir, { recursive: true });
13281
+ const dest = join10(dir, `${name}.json`);
13074
13282
  if (existsSync8(dest) && !opts.force) {
13075
13283
  fail3(`${dest} already exists (pass --force to overwrite)`);
13076
13284
  }
13077
- writeFileSync2(dest, `${JSON.stringify(result.theme, null, 2)}
13285
+ writeFileSync3(dest, `${JSON.stringify(result.theme, null, 2)}
13078
13286
  `, "utf8");
13079
13287
  process.stdout.write(`installed theme "${name}" -> ${dest}
13080
13288
  `);
@@ -13088,7 +13296,7 @@ function removeTheme(args) {
13088
13296
  if (BUNDLED_NAMES.includes(name)) {
13089
13297
  fail3(`"${name}" is a built-in theme and cannot be removed`);
13090
13298
  }
13091
- const dest = join9(userThemesDir(), `${name}.json`);
13299
+ const dest = join10(userThemesDir(), `${name}.json`);
13092
13300
  if (!existsSync8(dest)) {
13093
13301
  fail3(`no user theme named "${name}" (looked for ${dest})`);
13094
13302
  }
@@ -13158,7 +13366,7 @@ __export(exports_feedback_cmd, {
13158
13366
  runFeedbackSubcommand: () => runFeedbackSubcommand,
13159
13367
  parseFeedbackArgs: () => parseFeedbackArgs
13160
13368
  });
13161
- import { readFileSync as readFileSync10 } from "fs";
13369
+ import { readFileSync as readFileSync11 } from "fs";
13162
13370
  function usageError2(message) {
13163
13371
  process.stderr.write(`kobe feedback: ${message}
13164
13372
 
@@ -13168,8 +13376,8 @@ ${FEEDBACK_USAGE}
13168
13376
  }
13169
13377
  function readBodyFile(path11) {
13170
13378
  if (path11 === "-")
13171
- return readFileSync10(0, "utf8");
13172
- return readFileSync10(path11, "utf8");
13379
+ return readFileSync11(0, "utf8");
13380
+ return readFileSync11(path11, "utf8");
13173
13381
  }
13174
13382
  function parseFeedbackArgs(args) {
13175
13383
  const parsed = { help: false };
@@ -13320,6 +13528,8 @@ async function runDaemonSubcommand(argv) {
13320
13528
  try {
13321
13529
  await client.request("daemon.stop");
13322
13530
  console.log("kobe daemon: stop requested");
13531
+ } catch {
13532
+ console.log(`kobe daemon: no daemon running at ${socketPath}`);
13323
13533
  } finally {
13324
13534
  client.close();
13325
13535
  }
@@ -13368,9 +13578,9 @@ var init_daemon_cmd = __esm(() => {
13368
13578
  });
13369
13579
 
13370
13580
  // src/lib/skill-install.ts
13371
- import { existsSync as existsSync9, readFileSync as readFileSync11 } from "fs";
13581
+ import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
13372
13582
  import { homedir as homedir17 } from "os";
13373
- import { join as join10 } from "path";
13583
+ import { join as join11 } from "path";
13374
13584
  function npxSkillsArgv(opts = {}) {
13375
13585
  return ["skills", "add", SKILL_SOURCE_SLUG, "--skill", "kobe", "--agent", opts.agent ?? DEFAULT_SKILL_AGENT];
13376
13586
  }
@@ -13380,7 +13590,7 @@ function npxSkillsCommand(opts = {}) {
13380
13590
  function kobeSkillPaths(opts = {}) {
13381
13591
  const home = opts.home ?? homedir17();
13382
13592
  const cwd = opts.cwd ?? process.cwd();
13383
- return [join10(home, SKILL_REL_PATH), join10(cwd, SKILL_REL_PATH)];
13593
+ return [join11(home, SKILL_REL_PATH), join11(cwd, SKILL_REL_PATH)];
13384
13594
  }
13385
13595
  function parseSkillVersion(content) {
13386
13596
  const m = content.match(/kobe-skill-version:\s*(\d+)/);
@@ -13393,7 +13603,7 @@ function kobeSkillState(opts) {
13393
13603
  }
13394
13604
  let installedVersion = null;
13395
13605
  try {
13396
- installedVersion = parseSkillVersion(readFileSync11(path11, "utf8"));
13606
+ installedVersion = parseSkillVersion(readFileSync12(path11, "utf8"));
13397
13607
  } catch {
13398
13608
  installedVersion = null;
13399
13609
  }
@@ -13439,9 +13649,9 @@ __export(exports_maintenance, {
13439
13649
  runReloadSubcommand: () => runReloadSubcommand,
13440
13650
  runDoctorSubcommand: () => runDoctorSubcommand
13441
13651
  });
13442
- import { existsSync as existsSync10, readFileSync as readFileSync12, statSync as statSync5 } from "fs";
13652
+ import { existsSync as existsSync10, readFileSync as readFileSync13, statSync as statSync5 } from "fs";
13443
13653
  import { unlink as unlink6 } from "fs/promises";
13444
- import { join as join11 } from "path";
13654
+ import { join as join12 } from "path";
13445
13655
  import { createInterface as createInterface2 } from "readline";
13446
13656
  function isProcessAlive2(pid) {
13447
13657
  try {
@@ -13488,7 +13698,7 @@ function describeFile(path11) {
13488
13698
  }
13489
13699
  function taskCount(tasksPath) {
13490
13700
  try {
13491
- const parsed = JSON.parse(readFileSync12(tasksPath, "utf8"));
13701
+ const parsed = JSON.parse(readFileSync13(tasksPath, "utf8"));
13492
13702
  return Array.isArray(parsed.tasks) ? parsed.tasks.length : null;
13493
13703
  } catch {
13494
13704
  return null;
@@ -13496,7 +13706,7 @@ function taskCount(tasksPath) {
13496
13706
  }
13497
13707
  function tailFile(path11, n) {
13498
13708
  try {
13499
- const lines = readFileSync12(path11, "utf8").split(`
13709
+ const lines = readFileSync13(path11, "utf8").split(`
13500
13710
  `).filter((l) => l.trim().length > 0);
13501
13711
  return lines.slice(-n).join(`
13502
13712
  `);
@@ -13538,7 +13748,7 @@ async function runDoctorSubcommand(argv = []) {
13538
13748
  const socketPath = defaultDaemonSocketPath();
13539
13749
  const pidPath = defaultDaemonPidPath();
13540
13750
  const logPath = defaultDaemonLogPath();
13541
- const tasksPath = join11(kobeStateDir(), "tasks.json");
13751
+ const tasksPath = join12(kobeStateDir(), "tasks.json");
13542
13752
  const statePath2 = kvStatePath();
13543
13753
  const out = ["kobe doctor", ` home: ${homeDir()}`, ` socket: ${socketPath}`, ""];
13544
13754
  const status = await probeDaemonStatus(socketPath);
@@ -13657,7 +13867,7 @@ async function runResetSubcommand(argv) {
13657
13867
  const yes = argv.includes("--yes") || argv.includes("-y");
13658
13868
  const socketPath = defaultDaemonSocketPath();
13659
13869
  const pidPath = defaultDaemonPidPath();
13660
- const tasksPath = join11(kobeStateDir(), "tasks.json");
13870
+ const tasksPath = join12(kobeStateDir(), "tasks.json");
13661
13871
  const statePath2 = kvStatePath();
13662
13872
  console.log("kobe reset will:");
13663
13873
  console.log(" \u2022 stop the kobe daemon (graceful \u2192 SIGTERM \u2192 SIGKILL)");
@@ -14019,15 +14229,15 @@ var init_history4 = __esm(() => {
14019
14229
 
14020
14230
  // src/web/notes.ts
14021
14231
  import { mkdir as mkdir8, readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
14022
- import { join as join12 } from "path";
14232
+ import { join as join13 } from "path";
14023
14233
  function notesDir() {
14024
- return join12(kobeStateDir(), "notes");
14234
+ return join13(kobeStateDir(), "notes");
14025
14235
  }
14026
14236
  function isSafeTaskId(taskId) {
14027
14237
  return typeof taskId === "string" && taskId.length > 0 && /^[A-Za-z0-9_-]+$/.test(taskId);
14028
14238
  }
14029
14239
  function noteFilePath(taskId) {
14030
- return join12(notesDir(), `${taskId}.md`);
14240
+ return join13(notesDir(), `${taskId}.md`);
14031
14241
  }
14032
14242
  async function handleGet(url) {
14033
14243
  const taskId = url.searchParams.get("taskId");
@@ -14165,6 +14375,26 @@ var init_themes = __esm(() => {
14165
14375
  WEB_THEMES = Object.fromEntries(Object.entries(THEME_JSONS).map(([name, json]) => [name, toWebPalette(json)]).filter((entry) => entry[1] !== null));
14166
14376
  });
14167
14377
 
14378
+ // ../kobe-web/src/lib/repo-key.ts
14379
+ function normalizeRepoPath(path11) {
14380
+ return path11.length > 1 ? path11.replace(/\/+$/, "") : path11;
14381
+ }
14382
+ function repoSnapshotAliases(tasks, repoRoot) {
14383
+ const root = normalizeRepoPath(repoRoot);
14384
+ const aliases = new Set([repoRoot]);
14385
+ for (const task of tasks) {
14386
+ const taskRepo = normalizeRepoPath(task.repo);
14387
+ const taskWorktree = normalizeRepoPath(task.worktreePath);
14388
+ if (taskRepo === root || taskWorktree === root) {
14389
+ if (task.repo)
14390
+ aliases.add(task.repo);
14391
+ if (task.worktreePath)
14392
+ aliases.add(task.worktreePath);
14393
+ }
14394
+ }
14395
+ return [...aliases];
14396
+ }
14397
+
14168
14398
  // ../kobe-web/server/spa-channels.ts
14169
14399
  var SPA_CHANNELS, SPA_CHANNEL_SET;
14170
14400
  var init_spa_channels = __esm(() => {
@@ -14186,24 +14416,6 @@ var init_spa_channels = __esm(() => {
14186
14416
  function sleep2(ms) {
14187
14417
  return new Promise((resolve7) => setTimeout(resolve7, ms));
14188
14418
  }
14189
- function normalizedPath(path11) {
14190
- return path11.length > 1 ? path11.replace(/\/+$/, "") : path11;
14191
- }
14192
- function issueSnapshotAliases(tasks, repoRoot) {
14193
- const root = normalizedPath(repoRoot);
14194
- const aliases = new Set([repoRoot]);
14195
- for (const task of tasks) {
14196
- const taskRepo = normalizedPath(task.repo);
14197
- const taskWorktree = normalizedPath(task.worktreePath);
14198
- if (taskRepo === root || taskWorktree === root) {
14199
- if (task.repo)
14200
- aliases.add(task.repo);
14201
- if (task.worktreePath)
14202
- aliases.add(task.worktreePath);
14203
- }
14204
- }
14205
- return [...aliases];
14206
- }
14207
14419
 
14208
14420
  class DaemonLink {
14209
14421
  client = null;
@@ -14347,7 +14559,7 @@ class DaemonLink {
14347
14559
  case "issue.snapshot": {
14348
14560
  const state = payload;
14349
14561
  const next = { ...this.issueSnapshots };
14350
- for (const alias of issueSnapshotAliases(this.tasks, state.repoRoot)) {
14562
+ for (const alias of repoSnapshotAliases(this.tasks, state.repoRoot)) {
14351
14563
  next[alias] = { ...state, repoRoot: alias };
14352
14564
  }
14353
14565
  this.issueSnapshots = next;
@@ -14388,11 +14600,11 @@ var init_daemon_link = __esm(() => {
14388
14600
  });
14389
14601
 
14390
14602
  // ../kobe-web/server/issue-assets-route.ts
14391
- import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
14603
+ import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
14392
14604
  import { mkdir as mkdir9 } from "fs/promises";
14393
- import { join as join13, resolve as resolve7 } from "path";
14605
+ import { join as join14, resolve as resolve7 } from "path";
14394
14606
  function repoHashOf(repoRoot) {
14395
- return createHash5("sha1").update(repoRoot).digest("hex").slice(0, 16);
14607
+ return createHash6("sha1").update(repoRoot).digest("hex").slice(0, 16);
14396
14608
  }
14397
14609
  async function handlePost(req) {
14398
14610
  const declared = Number.parseInt(req.headers.get("content-length") ?? "", 10);
@@ -14422,11 +14634,11 @@ async function handlePost(req) {
14422
14634
  }
14423
14635
  try {
14424
14636
  const repoHash = repoHashOf(repoRoot);
14425
- const dir = join13(issueAssetsDir(), repoHash);
14637
+ const dir = join14(issueAssetsDir(), repoHash);
14426
14638
  await mkdir9(dir, { recursive: true });
14427
- const assetId = randomUUID2();
14639
+ const assetId = randomUUID3();
14428
14640
  const name = `${assetId}.${ext}`;
14429
- await Bun.write(join13(dir, name), file);
14641
+ await Bun.write(join14(dir, name), file);
14430
14642
  return Response.json({ url: `${ASSETS_ROUTE}/${repoHash}/${name}` });
14431
14643
  } catch (err) {
14432
14644
  return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 });
@@ -14444,7 +14656,7 @@ async function handleGet2(pathname) {
14444
14656
  }
14445
14657
  const root = issueAssetsDir();
14446
14658
  const resolved = resolve7(root, repoHash, fileSeg);
14447
- if (resolved !== join13(root, repoHash, fileSeg) || !resolved.startsWith(`${root}/`)) {
14659
+ if (resolved !== join14(root, repoHash, fileSeg) || !resolved.startsWith(`${root}/`)) {
14448
14660
  return Response.json({ error: "invalid asset path" }, { status: 400 });
14449
14661
  }
14450
14662
  const ext = fileSeg.slice(fileSeg.lastIndexOf(".") + 1).toLowerCase();
@@ -14644,7 +14856,21 @@ var init_session = __esm(() => {
14644
14856
 
14645
14857
  // ../kobe-web/server/bridge.ts
14646
14858
  import { existsSync as existsSync11 } from "fs";
14647
- import { join as join14, normalize as normalize2 } from "path";
14859
+ import { join as join15, normalize as normalize2 } from "path";
14860
+ function originAllowed(req, allowedHost) {
14861
+ const origin = req.headers.get("origin");
14862
+ if (!origin)
14863
+ return true;
14864
+ if (LOCAL_ORIGIN.test(origin))
14865
+ return true;
14866
+ if (allowedHost) {
14867
+ try {
14868
+ if (new URL(origin).hostname === allowedHost)
14869
+ return true;
14870
+ } catch {}
14871
+ }
14872
+ return false;
14873
+ }
14648
14874
  function sseResponse(register) {
14649
14875
  let unregister = null;
14650
14876
  let heartbeat = null;
@@ -14867,6 +15093,9 @@ function createRequestHandler(deps) {
14867
15093
  const url = new URL(req.url);
14868
15094
  if (url.pathname === WEB_HEALTH_PATH)
14869
15095
  return new Response(WEB_HEALTH_MARKER);
15096
+ if (!originAllowed(req, deps.allowedHost)) {
15097
+ return new Response("forbidden: cross-origin request rejected", { status: 403 });
15098
+ }
14870
15099
  if (url.pathname === "/events") {
14871
15100
  return sseResponse((send2) => {
14872
15101
  send2("snapshot", link.snapshot());
@@ -14939,10 +15168,10 @@ async function quickPromptsPut(req) {
14939
15168
  }
14940
15169
  async function staticResponse(pathname, staticDir) {
14941
15170
  const rel = pathname === "/" ? "/index.html" : pathname;
14942
- const resolved = normalize2(join14(staticDir, rel));
15171
+ const resolved = normalize2(join15(staticDir, rel));
14943
15172
  if (!resolved.startsWith(staticDir))
14944
15173
  return new Response("forbidden", { status: 403 });
14945
- const file = Bun.file(existsSync11(resolved) ? resolved : join14(staticDir, "index.html"));
15174
+ const file = Bun.file(existsSync11(resolved) ? resolved : join15(staticDir, "index.html"));
14946
15175
  if (!await file.exists()) {
14947
15176
  return new Response("kobe web assets not built \u2014 run `bun --filter kobe-web build`", { status: 503 });
14948
15177
  }
@@ -15003,8 +15232,9 @@ async function createBridgeServer(opts = {}) {
15003
15232
  for (const send2 of sseSends)
15004
15233
  send2("snapshot", link.snapshot());
15005
15234
  });
15006
- const handle = createRequestHandler({ link, sseSends, staticDir });
15007
15235
  const hostname = process.env.KOBE_WEB_HOST?.trim() || "127.0.0.1";
15236
+ const allowedHost = LOCAL_ORIGIN.test(`http://${hostname}`) ? undefined : hostname;
15237
+ const handle = createRequestHandler({ link, sseSends, staticDir, allowedHost });
15008
15238
  const server = Bun.serve({ port, hostname, idleTimeout: 0, fetch: handle });
15009
15239
  return {
15010
15240
  port: server.port ?? port,
@@ -15014,7 +15244,7 @@ async function createBridgeServer(opts = {}) {
15014
15244
  }
15015
15245
  };
15016
15246
  }
15017
- var WEB_HEALTH_MARKER = "kobe-web", WEB_HEALTH_PATH = "/__kobe_web", FOCUS_ACCENTS, ENGINE_ID_RE, QUICK_PROMPT_KEYS;
15247
+ var WEB_HEALTH_MARKER = "kobe-web", WEB_HEALTH_PATH = "/__kobe_web", LOCAL_ORIGIN, FOCUS_ACCENTS, ENGINE_ID_RE, QUICK_PROMPT_KEYS;
15018
15248
  var init_bridge = __esm(() => {
15019
15249
  init_account_detect();
15020
15250
  init_interactive_command();
@@ -15032,6 +15262,7 @@ var init_bridge = __esm(() => {
15032
15262
  init_issue_assets_route();
15033
15263
  init_rpc_allowlist();
15034
15264
  init_session();
15265
+ LOCAL_ORIGIN = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/;
15035
15266
  FOCUS_ACCENTS = ["primary", "success", "info"];
15036
15267
  ENGINE_ID_RE = /^[a-z][a-z0-9_-]{0,47}$/;
15037
15268
  QUICK_PROMPT_KEYS = {
@@ -15337,7 +15568,7 @@ __export(exports_hook_cmd, {
15337
15568
  ensureGlobalKobeHooks: () => ensureGlobalKobeHooks
15338
15569
  });
15339
15570
  import { homedir as homedir19 } from "os";
15340
- import { join as join15, resolve as resolve9 } from "path";
15571
+ import { join as join16, resolve as resolve9 } from "path";
15341
15572
  async function readTextWithTimeout(read, timeoutMs = STDIN_READ_TIMEOUT_MS) {
15342
15573
  let raceTimer;
15343
15574
  try {
@@ -15468,7 +15699,7 @@ function activityHookAdapters() {
15468
15699
  return ALL_VENDORS.map((v) => createEngineHookAdapter(v)).filter((a) => a.supportsHooks());
15469
15700
  }
15470
15701
  function globalSettingsPath() {
15471
- return join15(homedir19(), ".claude", "settings.json");
15702
+ return join16(homedir19(), ".claude", "settings.json");
15472
15703
  }
15473
15704
  function persistedSyncPath(stored) {
15474
15705
  if (!stored || stored === "off")
@@ -15476,7 +15707,7 @@ function persistedSyncPath(stored) {
15476
15707
  if (stored === "global")
15477
15708
  return globalSettingsPath();
15478
15709
  if (stored.startsWith("repo:"))
15479
- return join15(resolve9(stored.slice(5)), ".claude", "settings.json");
15710
+ return join16(resolve9(stored.slice(5)), ".claude", "settings.json");
15480
15711
  return stored;
15481
15712
  }
15482
15713
  async function ensureGlobalKobeHooks() {
@@ -17213,6 +17444,7 @@ var init_remote_orchestrator = __esm(() => {
17213
17444
  init_protocol();
17214
17445
  init_dev();
17215
17446
  init_worktree_changes();
17447
+ init_task();
17216
17448
  init_version();
17217
17449
  });
17218
17450
 
@@ -20406,9 +20638,9 @@ var pulse_default = "../pulse-n3cq1btw.wav";
20406
20638
  var init_pulse = () => {};
20407
20639
 
20408
20640
  // src/tui/lib/sound.ts
20409
- import { existsSync as existsSync14, mkdirSync as mkdirSync6 } from "fs";
20641
+ import { existsSync as existsSync14, mkdirSync as mkdirSync7 } from "fs";
20410
20642
  import { tmpdir as tmpdir2 } from "os";
20411
- import { basename as basename6, isAbsolute as isAbsolute3, join as join17, resolve as resolve10 } from "path";
20643
+ import { basename as basename6, isAbsolute as isAbsolute3, join as join18, resolve as resolve10 } from "path";
20412
20644
  function args(player, file, volume) {
20413
20645
  if (player === "ffplay")
20414
20646
  return [player, "-autoexit", "-nodisp", "-af", `volume=${volume}`, file];
@@ -20431,13 +20663,13 @@ function pickPlayer() {
20431
20663
  return cachedPlayer;
20432
20664
  const path12 = process.env.PATH ?? "";
20433
20665
  const segments = path12.split(":").filter(Boolean);
20434
- cachedPlayer = PLAYERS.find((p) => segments.some((dir) => existsSync14(join17(dir, p)))) ?? null;
20666
+ cachedPlayer = PLAYERS.find((p) => segments.some((dir) => existsSync14(join18(dir, p)))) ?? null;
20435
20667
  return cachedPlayer;
20436
20668
  }
20437
20669
  async function ensureAsset() {
20438
20670
  cachedPath ??= (async () => {
20439
- mkdirSync6(DIR, { recursive: true });
20440
- const dest = join17(DIR, basename6(pulseAsset));
20671
+ mkdirSync7(DIR, { recursive: true });
20672
+ const dest = join18(DIR, basename6(pulseAsset));
20441
20673
  const out = Bun.file(dest);
20442
20674
  if (await out.exists())
20443
20675
  return dest;
@@ -20467,7 +20699,7 @@ var pulseAsset, DIR, PLAYERS, cachedPlayer, cachedPath;
20467
20699
  var init_sound = __esm(() => {
20468
20700
  init_pulse();
20469
20701
  pulseAsset = isAbsolute3(pulse_default) ? pulse_default : resolve10(import.meta.dir, pulse_default);
20470
- DIR = join17(tmpdir2(), "kobe-sfx");
20702
+ DIR = join18(tmpdir2(), "kobe-sfx");
20471
20703
  PLAYERS = [
20472
20704
  "ffplay",
20473
20705
  "mpv",
@@ -20592,10 +20824,10 @@ var init_apply_ui_prefs = __esm(() => {
20592
20824
  });
20593
20825
 
20594
20826
  // src/tui/lib/persisted-ui-prefs.ts
20595
- import { readFileSync as readFileSync13 } from "fs";
20827
+ import { readFileSync as readFileSync14 } from "fs";
20596
20828
  function readPersistedUiPrefs(fallbackTheme) {
20597
20829
  try {
20598
- const parsed = JSON.parse(readFileSync13(kvStatePath(), "utf8"));
20830
+ const parsed = JSON.parse(readFileSync14(kvStatePath(), "utf8"));
20599
20831
  const theme = typeof parsed.activeTheme === "string" && hasTheme(parsed.activeTheme) ? parsed.activeTheme : fallbackTheme;
20600
20832
  const transparent = parsed.transparentBackground === true;
20601
20833
  const focusAccent = typeof parsed.focusAccent === "string" && FOCUS_ACCENT_SLOTS.includes(parsed.focusAccent) ? parsed.focusAccent : null;
@@ -20871,6 +21103,7 @@ var init_host = __esm(() => {
20871
21103
  init_remote_orchestrator();
20872
21104
  init_account_detect();
20873
21105
  init_repos();
21106
+ init_task();
20874
21107
  init_new_task_dialog();
20875
21108
  init_theme2();
20876
21109
  init_host_boot();
@@ -21052,6 +21285,10 @@ async function deliverFirstPromptToTask(orch, task, repo, vendor, prompt) {
21052
21285
  async function jumpToTask(orch, task, repo, vendor) {
21053
21286
  await ensureTaskSession2(orch, task, repo, vendor);
21054
21287
  await orch.setActiveTask(task.id).catch(() => {});
21288
+ const {
21289
+ prepareWindowForSwitch: prepareWindowForSwitch2
21290
+ } = await Promise.resolve().then(() => (init_tmux(), exports_tmux));
21291
+ await prepareWindowForSwitch2(tmuxSessionName(task.id));
21055
21292
  await runTmux(["switch-client", "-t", `=${tmuxSessionName(task.id)}`]);
21056
21293
  }
21057
21294
  function QuickTaskPage(props) {
@@ -21146,6 +21383,7 @@ var init_host2 = __esm(() => {
21146
21383
  init_repos();
21147
21384
  init_client2();
21148
21385
  init_prompt_delivery();
21386
+ init_task();
21149
21387
  init_quick_task_composer();
21150
21388
  init_theme2();
21151
21389
  init_git_snapshot();
@@ -21646,7 +21884,7 @@ var init_dialog_confirm = __esm(() => {
21646
21884
 
21647
21885
  // src/tui/component/settings-dialog/actions.ts
21648
21886
  import { unlinkSync as unlinkSync2 } from "fs";
21649
- import { join as join18 } from "path";
21887
+ import { join as join19 } from "path";
21650
21888
  function hasRestartableDaemon(orchestrator) {
21651
21889
  return orchestrator instanceof RemoteOrchestrator;
21652
21890
  }
@@ -21663,7 +21901,7 @@ async function confirmResetState(dialog, kv, renderer) {
21663
21901
  return;
21664
21902
  kv.clear();
21665
21903
  try {
21666
- unlinkSync2(join18(homeDir(), ".kobe", "tasks.json"));
21904
+ unlinkSync2(join19(homeDir(), ".kobe", "tasks.json"));
21667
21905
  } catch (err) {
21668
21906
  if (err.code !== "ENOENT") {
21669
21907
  console.error("kobe: failed to delete tasks.json during reset:", err);
@@ -23549,6 +23787,7 @@ var init_settings_dialog = __esm(() => {
23549
23787
  init_auto_status();
23550
23788
  init_dispatcher();
23551
23789
  init_repos();
23790
+ init_task();
23552
23791
  init_vendor();
23553
23792
  init_theme2();
23554
23793
  init_editor_prefs();
@@ -23965,6 +24204,7 @@ var init_task_actions = __esm(() => {
23965
24204
  init_interactive_command();
23966
24205
  init_errors();
23967
24206
  init_repos();
24207
+ init_task();
23968
24208
  init_vendor();
23969
24209
  init_tmux();
23970
24210
  });
@@ -23972,7 +24212,7 @@ var init_task_actions = __esm(() => {
23972
24212
  // src/tui/lib/worktree-opener.ts
23973
24213
  import { spawn as spawn5 } from "child_process";
23974
24214
  import { existsSync as existsSync15 } from "fs";
23975
- import { basename as basename7, delimiter, isAbsolute as isAbsolute4, join as join19 } from "path";
24215
+ import { basename as basename7, delimiter, isAbsolute as isAbsolute4, join as join20 } from "path";
23976
24216
  function executableOnPath(command, env, exists) {
23977
24217
  if (isAbsolute4(command))
23978
24218
  return exists(command);
@@ -23980,7 +24220,7 @@ function executableOnPath(command, env, exists) {
23980
24220
  for (const dir of pathEnv.split(delimiter)) {
23981
24221
  if (!dir)
23982
24222
  continue;
23983
- if (exists(join19(dir, command)))
24223
+ if (exists(join20(dir, command)))
23984
24224
  return true;
23985
24225
  }
23986
24226
  return false;
@@ -24114,10 +24354,10 @@ var init_background_poll = __esm(() => {
24114
24354
 
24115
24355
  // src/tui/panes/sidebar/git-head.ts
24116
24356
  import { stat as stat5 } from "fs/promises";
24117
- import { join as join20 } from "path";
24357
+ import { join as join21 } from "path";
24118
24358
  async function headFingerprint(repo) {
24119
24359
  try {
24120
- const st = await stat5(join20(repo, ".git", "HEAD"));
24360
+ const st = await stat5(join21(repo, ".git", "HEAD"));
24121
24361
  return `${st.mtimeMs}:${st.size}`;
24122
24362
  } catch {
24123
24363
  return null;
@@ -24634,8 +24874,14 @@ function Sidebar(props) {
24634
24874
  return;
24635
24875
  }
24636
24876
  const idx = ids.indexOf(id);
24637
- if (idx >= 0 && idx !== cur)
24638
- setCursorIndex(idx);
24877
+ if (idx >= 0) {
24878
+ if (idx !== cur)
24879
+ setCursorIndex(idx);
24880
+ } else if (ids.length === 0) {
24881
+ setCursorIndex(-1);
24882
+ } else if (cur < 0 || cur >= ids.length) {
24883
+ setCursorIndex(ids.length - 1);
24884
+ }
24639
24885
  }));
24640
24886
  createEffect(on(view, () => {
24641
24887
  const ids = flatIds();
@@ -25707,6 +25953,7 @@ function TasksShell(props) {
25707
25953
  initPrompt: init3.initPrompt
25708
25954
  });
25709
25955
  }
25956
+ await prepareWindowForSwitch(name);
25710
25957
  await runTmux(["switch-client", "-t", `=${name}`]);
25711
25958
  props.orch?.setActiveTask(id).catch(() => {});
25712
25959
  return;
@@ -25745,6 +25992,7 @@ function TasksShell(props) {
25745
25992
  notifyError("Couldn't start this task's session");
25746
25993
  return;
25747
25994
  }
25995
+ await prepareWindowForSwitch(name);
25748
25996
  await runTmux(["switch-client", "-t", `=${name}`]);
25749
25997
  props.orch?.setActiveTask(id).catch(() => {});
25750
25998
  }
@@ -28061,7 +28309,7 @@ __export(exports_host7, {
28061
28309
  startOpsHost: () => startOpsHost,
28062
28310
  nextActivityPollDelay: () => nextActivityPollDelay
28063
28311
  });
28064
- import { createHash as createHash6 } from "crypto";
28312
+ import { createHash as createHash7 } from "crypto";
28065
28313
  import { SyntaxStyle } from "@opentui/core";
28066
28314
  function nextActivityPollDelay(currentMs, idleStreak) {
28067
28315
  if (idleStreak < ACTIVITY_IDLE_RAMP_POLLS)
@@ -28234,7 +28482,7 @@ function basename8(p) {
28234
28482
  return i >= 0 ? p.slice(i + 1) : p;
28235
28483
  }
28236
28484
  function fingerprint(text) {
28237
- return createHash6("sha1").update(text).digest("hex");
28485
+ return createHash7("sha1").update(text).digest("hex");
28238
28486
  }
28239
28487
  async function startOpsHost(args2) {
28240
28488
  await bootPaneHost({
@@ -29035,7 +29283,24 @@ async function main() {
29035
29283
  process.exit(2);
29036
29284
  }
29037
29285
  const { healSessionLayout: healSessionLayout2 } = await Promise.resolve().then(() => (init_tmux(), exports_tmux));
29038
- await healSessionLayout2(session);
29286
+ const { coalesceLayoutWork: coalesceLayoutWork2 } = await Promise.resolve().then(() => (init_layout_coord(), exports_layout_coord));
29287
+ await coalesceLayoutWork2(session, "heal", () => healSessionLayout2(session));
29288
+ return;
29289
+ }
29290
+ if (subcommand === "capture-layout") {
29291
+ const flags = parseOpsFlags(rest);
29292
+ const session = flags.session;
29293
+ if (!session) {
29294
+ console.error("kobe capture-layout: --session <name> is required");
29295
+ process.exit(2);
29296
+ }
29297
+ const { captureGlobalLayoutOnDrag: captureGlobalLayoutOnDrag2 } = await Promise.resolve().then(() => (init_tmux(), exports_tmux));
29298
+ const { coalesceLayoutWork: coalesceLayoutWork2, genAgeMs: genAgeMs2, RESIZE_GUARD_MS: RESIZE_GUARD_MS2 } = await Promise.resolve().then(() => (init_layout_coord(), exports_layout_coord));
29299
+ await coalesceLayoutWork2(session, "capture", async () => {
29300
+ if (genAgeMs2(session, "resize") < RESIZE_GUARD_MS2)
29301
+ return;
29302
+ await captureGlobalLayoutOnDrag2(session);
29303
+ });
29039
29304
  return;
29040
29305
  }
29041
29306
  if (subcommand === "quick-task") {