@sma1lboy/kobe 0.7.28 → 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.
Files changed (28) hide show
  1. package/dist/cli/index.js +1587 -845
  2. package/dist/web-ui/assets/AppShell-DB2eNRP2.js +8 -0
  3. package/dist/web-ui/assets/{ChatTerminal-CcdiyDLi.js → ChatTerminal-dv25rCIz.js} +3 -3
  4. package/dist/web-ui/assets/IssuePeek-B4sEOPyN.css +1 -0
  5. package/dist/web-ui/assets/IssuePeek-Dh3qej2l.js +184 -0
  6. package/dist/web-ui/assets/ViewToggle-GsDQZY-j.js +1 -0
  7. package/dist/web-ui/assets/board-CbM3YF06.js +1 -0
  8. package/dist/web-ui/assets/index-LIEeaROP.css +2 -0
  9. package/dist/web-ui/assets/index-j-izWmuG.js +10 -0
  10. package/dist/web-ui/assets/issues-BXc_TiwK.js +1 -0
  11. package/dist/web-ui/assets/routes-CS4Lj7ol.js +1 -0
  12. package/dist/web-ui/assets/{tabs-7WK4MwGF.js → tabs-CmaDBSUQ.js} +1 -1
  13. package/dist/web-ui/assets/task._taskId-BQ5h5DZ2.js +1 -0
  14. package/dist/web-ui/assets/useNavigate-CXvSccc8.js +1 -0
  15. package/dist/web-ui/assets/vendor-X3nYbtqQ.js +1 -0
  16. package/dist/web-ui/index.html +4 -3
  17. package/dist/web-ui/pty-server.mjs +74 -18
  18. package/package.json +1 -1
  19. package/dist/web-ui/assets/AppShell-CseeU4ld.js +0 -7
  20. package/dist/web-ui/assets/ChatTranscript-DBk96m82.js +0 -11
  21. package/dist/web-ui/assets/arrow-left-DbEl9vGA.js +0 -1
  22. package/dist/web-ui/assets/board-4GZVbT7l.js +0 -6
  23. package/dist/web-ui/assets/chips-4L0VTRjY.js +0 -2
  24. package/dist/web-ui/assets/index-DwX3YPSK.css +0 -2
  25. package/dist/web-ui/assets/index-Dy89TZk1.js +0 -10
  26. package/dist/web-ui/assets/overview-DlOA2rw0.js +0 -1
  27. package/dist/web-ui/assets/routes-BU7iBSg2.js +0 -1
  28. package/dist/web-ui/assets/task._taskId-9jugSRjg.js +0 -1
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.28",
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",
@@ -175,6 +175,9 @@ function kobeSettingsDir() {
175
175
  function keybindingsConfigPath() {
176
176
  return join(kobeSettingsDir(), "keybindings.yaml");
177
177
  }
178
+ function issueAssetsDir() {
179
+ return join(kobeStateDir(), "issue-assets");
180
+ }
178
181
  function remoteControlSocketPath(host, user, port) {
179
182
  const hash = createHash("sha1").update(`${user}@${host}:${port ?? 22}`).digest("hex").slice(0, 16);
180
183
  return join(kobeStateDir(), "ssh", `${hash}.sock`);
@@ -193,7 +196,9 @@ __export(exports_version, {
193
196
  recommendedGlobalInstallCommand: () => recommendedGlobalInstallCommand,
194
197
  isNewerSemver: () => isNewerSemver,
195
198
  fetchReleaseSummaries: () => fetchReleaseSummaries,
199
+ fetchReleaseNotesRange: () => fetchReleaseNotesRange,
196
200
  fetchReleaseNotes: () => fetchReleaseNotes,
201
+ compareSemver: () => compareSemver,
197
202
  checkLatestVersion: () => checkLatestVersion,
198
203
  UPDATE_SCRIPT_URL: () => UPDATE_SCRIPT_URL,
199
204
  UPDATE_COMMAND: () => UPDATE_COMMAND,
@@ -234,20 +239,23 @@ async function fetchLatestFromRegistry(packageName) {
234
239
  }
235
240
  }
236
241
  function isNewerSemver(latest, current) {
242
+ return compareSemver(latest, current) > 0;
243
+ }
244
+ function compareSemver(aVersion, bVersion) {
237
245
  const norm = (v) => v.split("-")[0] ?? v;
238
- const a = norm(latest).split(".").map((s) => Number.parseInt(s, 10));
239
- const b = norm(current).split(".").map((s) => Number.parseInt(s, 10));
246
+ const a = norm(aVersion).split(".").map((s) => Number.parseInt(s, 10));
247
+ const b = norm(bVersion).split(".").map((s) => Number.parseInt(s, 10));
240
248
  for (let i = 0;i < 3; i++) {
241
249
  const av = a[i] ?? 0;
242
250
  const bv = b[i] ?? 0;
243
251
  if (Number.isNaN(av) || Number.isNaN(bv))
244
- return false;
252
+ return 0;
245
253
  if (av > bv)
246
- return true;
254
+ return 1;
247
255
  if (av < bv)
248
- return false;
256
+ return -1;
249
257
  }
250
- return false;
258
+ return 0;
251
259
  }
252
260
  async function checkLatestVersion(opts = {}) {
253
261
  const fake = process.env.KOBE_FAKE_UPDATE;
@@ -298,6 +306,42 @@ async function fetchReleaseNotes(version) {
298
306
  clearTimeout(timer);
299
307
  }
300
308
  }
309
+ async function fetchReleaseNotesRange(args) {
310
+ const slug = repoSlug();
311
+ if (!slug)
312
+ return [];
313
+ const limit = args.limit ?? 100;
314
+ const ctrl = new AbortController;
315
+ const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
316
+ try {
317
+ const res = await fetch(`https://api.github.com/repos/${slug}/releases?per_page=${limit}`, {
318
+ signal: ctrl.signal,
319
+ headers: {
320
+ accept: "application/vnd.github+json",
321
+ "x-github-api-version": "2022-11-28"
322
+ }
323
+ });
324
+ if (!res.ok)
325
+ return [];
326
+ const body = await res.json();
327
+ if (!Array.isArray(body))
328
+ return [];
329
+ return body.map((release) => {
330
+ const version = versionFromTagName(release.tag_name);
331
+ if (!version || typeof release.html_url !== "string" || typeof release.body !== "string")
332
+ return null;
333
+ if (compareSemver(version, args.current) <= 0)
334
+ return null;
335
+ if (compareSemver(version, args.latest) > 0)
336
+ return null;
337
+ return { version, url: release.html_url, body: release.body };
338
+ }).filter((release) => release !== null);
339
+ } catch {
340
+ return [];
341
+ } finally {
342
+ clearTimeout(timer);
343
+ }
344
+ }
301
345
  async function fetchReleaseSummaries(limit = 12) {
302
346
  const slug = repoSlug();
303
347
  if (!slug)
@@ -351,6 +395,9 @@ import { mkdir, readFile as readFileAsync, readdir as readdirAsync } from "fs/pr
351
395
  function shQuote(s) {
352
396
  return `'${s.replace(/'/g, "'\\''")}'`;
353
397
  }
398
+ function shToken(s) {
399
+ return /^[A-Za-z0-9_@%+=:,./-]+$/.test(s) ? s : shQuote(s);
400
+ }
354
401
  function shJoin(argv) {
355
402
  return argv.map(shQuote).join(" ");
356
403
  }
@@ -501,7 +548,8 @@ class RemoteExecHost {
501
548
  }
502
549
  wrapCommand(command, opts = {}) {
503
550
  const remote = opts.cwd ? `cd ${shQuote(opts.cwd)} && ${command}` : command;
504
- 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)}`;
505
553
  }
506
554
  }
507
555
  var defaultSpawner = (argv, env) => {
@@ -622,6 +670,7 @@ __export(exports_repos, {
622
670
  setRepoInitOverride: () => setRepoInitOverride,
623
671
  setPersistedString: () => setPersistedString,
624
672
  resolveRepoRoot: () => resolveRepoRoot,
673
+ resolveMainRepoRoot: () => resolveMainRepoRoot,
625
674
  removeSavedRepo: () => removeSavedRepo,
626
675
  remoteRepoKey: () => remoteRepoKey,
627
676
  normalizeSavedRepos: () => normalizeSavedRepos,
@@ -657,6 +706,19 @@ function resolveRepoRoot(absPath) {
657
706
  } catch {}
658
707
  return top;
659
708
  }
709
+ function resolveMainRepoRoot(absPath) {
710
+ if (isRemoteRepoKey(absPath))
711
+ return absPath;
712
+ const r = spawnSync3("git", ["worktree", "list", "--porcelain"], {
713
+ cwd: absPath,
714
+ encoding: "utf8",
715
+ shell: false
716
+ });
717
+ if (r.status !== 0)
718
+ return resolveRepoRoot(absPath);
719
+ const first = (r.stdout ?? "").split(/\r?\n/).find((line) => line.startsWith("worktree "))?.slice("worktree ".length).trim();
720
+ return first || resolveRepoRoot(absPath);
721
+ }
660
722
  function statePath() {
661
723
  return kvStatePath();
662
724
  }
@@ -967,7 +1029,20 @@ var init_add_remote = __esm(() => {
967
1029
  });
968
1030
 
969
1031
  // src/types/task.ts
970
- 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
+ });
971
1046
 
972
1047
  // src/orchestrator/index/ulid.ts
973
1048
  function encodeTime(now, len) {
@@ -1305,7 +1380,7 @@ function coerceTask(value) {
1305
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") {
1306
1381
  return null;
1307
1382
  }
1308
- if (!isTaskStatus(v.status))
1383
+ if (!isTaskStatus2(v.status))
1309
1384
  return null;
1310
1385
  const archived = typeof v.archived === "boolean" ? v.archived : false;
1311
1386
  const kind = v.kind === "main" ? "main" : "task";
@@ -1323,6 +1398,7 @@ function coerceTask(value) {
1323
1398
  vendor: isVendorId(v.vendor) ? v.vendor : DEFAULT_TASK_VENDOR,
1324
1399
  prStatus: coercePRStatus(v.prStatus),
1325
1400
  ...typeof v.position === "number" && Number.isFinite(v.position) ? { position: v.position } : {},
1401
+ ...typeof v.modelEffort === "string" && v.modelEffort.length > 0 ? { modelEffort: v.modelEffort } : {},
1326
1402
  createdAt: v.createdAt,
1327
1403
  updatedAt: v.updatedAt
1328
1404
  };
@@ -1361,11 +1437,12 @@ function isPRCheckState(v) {
1361
1437
  function isVendorId(v) {
1362
1438
  return v === "claude" || v === "codex";
1363
1439
  }
1364
- function isTaskStatus(s) {
1440
+ function isTaskStatus2(s) {
1365
1441
  return s === "backlog" || s === "in_progress" || s === "in_review" || s === "done" || s === "canceled" || s === "error";
1366
1442
  }
1367
1443
  var CURRENT_VERSION2 = 3;
1368
1444
  var init_store2 = __esm(() => {
1445
+ init_task();
1369
1446
  init_ulid();
1370
1447
  });
1371
1448
 
@@ -3062,9 +3139,10 @@ function deriveTitleFromPrompt(prompt) {
3062
3139
  const collapsed = prompt.replace(/\s+/g, " ").trim();
3063
3140
  if (collapsed.length === 0)
3064
3141
  return "";
3065
- if (collapsed.length <= TITLE_CHAR_CAP)
3142
+ const points = [...collapsed];
3143
+ if (points.length <= TITLE_CHAR_CAP)
3066
3144
  return collapsed;
3067
- return `${collapsed.slice(0, TITLE_CHAR_CAP)}\u2026`;
3145
+ return `${points.slice(0, TITLE_CHAR_CAP).join("")}\u2026`;
3068
3146
  }
3069
3147
  function autoBranch(title, taskId) {
3070
3148
  const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
@@ -3630,11 +3708,6 @@ class Orchestrator {
3630
3708
  return this.tasksAcc;
3631
3709
  }
3632
3710
  subscribeTasks(listener) {
3633
- try {
3634
- listener(this.store.list());
3635
- } catch (err) {
3636
- console.error("[kobe Orchestrator] task listener threw on subscribe:", err);
3637
- }
3638
3711
  return this.store.subscribe(listener);
3639
3712
  }
3640
3713
  dispose() {
@@ -3657,7 +3730,8 @@ class Orchestrator {
3657
3730
  worktreePath: "",
3658
3731
  status: "backlog",
3659
3732
  kind: "task",
3660
- vendor: input.vendor ?? DEFAULT_TASK_VENDOR
3733
+ vendor: input.vendor ?? DEFAULT_TASK_VENDOR,
3734
+ ...input.modelEffort ? { modelEffort: input.modelEffort } : {}
3661
3735
  });
3662
3736
  if (input.baseRef)
3663
3737
  this.pendingBaseRefs.set(task.id, input.baseRef);
@@ -3922,6 +3996,7 @@ var PLACEHOLDER_TASK_TITLE = "(new task)";
3922
3996
  var init_core = __esm(() => {
3923
3997
  init_dev();
3924
3998
  init_repos();
3999
+ init_task();
3925
4000
  init_errors();
3926
4001
  init_slug_allocator();
3927
4002
  });
@@ -3961,6 +4036,7 @@ function serializeTask(task) {
3961
4036
  vendor: task.vendor,
3962
4037
  prStatus: task.prStatus,
3963
4038
  position: task.position,
4039
+ modelEffort: task.modelEffort,
3964
4040
  createdAt: task.createdAt,
3965
4041
  updatedAt: task.updatedAt
3966
4042
  };
@@ -3973,6 +4049,7 @@ var DAEMON_PROTOCOL_VERSION = 3, MIN_COMPATIBLE_PROTOCOL_VERSION = 2, CHANNEL_NA
3973
4049
  var init_protocol = __esm(() => {
3974
4050
  CHANNEL_NAMES = [
3975
4051
  "task.snapshot",
4052
+ "issue.snapshot",
3976
4053
  "active-task",
3977
4054
  "update",
3978
4055
  "engine-state",
@@ -3980,7 +4057,6 @@ var init_protocol = __esm(() => {
3980
4057
  "keybindings",
3981
4058
  "task.jobs",
3982
4059
  "worktree.changes",
3983
- "task.conflicts",
3984
4060
  "session.deliver"
3985
4061
  ];
3986
4062
  CHANNEL_NAME_SET = new Set(CHANNEL_NAMES);
@@ -4093,6 +4169,7 @@ var init_client_log = __esm(() => {
4093
4169
 
4094
4170
  // ../kobe-daemon/src/client/index.ts
4095
4171
  import { connect } from "net";
4172
+ import { StringDecoder } from "string_decoder";
4096
4173
 
4097
4174
  class KobeDaemonClient {
4098
4175
  socketPath;
@@ -4213,7 +4290,9 @@ class KobeDaemonClient {
4213
4290
  };
4214
4291
  socket.once("connect", onConnect);
4215
4292
  socket.once("error", onError);
4216
- 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)));
4217
4296
  socket.on("close", () => this.onSocketClose(socket));
4218
4297
  });
4219
4298
  }
@@ -5006,6 +5085,24 @@ function normalizeClaudeContent(content) {
5006
5085
  return out;
5007
5086
  }
5008
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
+
5009
5106
  // src/engine/claude-code-local/history.ts
5010
5107
  import { appendFile as appendFile2, mkdir as mkdir4, readFile as readFile2, readdir, stat, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
5011
5108
  import { homedir as homedir8 } from "os";
@@ -5087,6 +5184,8 @@ function parseJsonl(raw, sessionId) {
5087
5184
  return out;
5088
5185
  }
5089
5186
  function extractMessage(record, fallbackSessionId) {
5187
+ if (isSyntheticClaudeRecord(record))
5188
+ return null;
5090
5189
  const inner = isObject(record.message) ? record.message : record;
5091
5190
  const role = inner.role;
5092
5191
  if (role !== "user" && role !== "assistant" && role !== "system")
@@ -5094,6 +5193,8 @@ function extractMessage(record, fallbackSessionId) {
5094
5193
  if (!("content" in inner))
5095
5194
  return null;
5096
5195
  const blocks = normalizeClaudeContent(inner.content);
5196
+ if (role === "user" && isClaudeCommandBreadcrumb(blocks))
5197
+ return null;
5097
5198
  const ts = typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString();
5098
5199
  const sid = typeof record.sessionId === "string" ? record.sessionId : fallbackSessionId;
5099
5200
  const usage = extractUsage(inner.usage);
@@ -5496,9 +5597,12 @@ async function listRolloutFiles(deps = defaultDeps7) {
5496
5597
  return out;
5497
5598
  }
5498
5599
  async function findRolloutFile(sessionId, deps = defaultDeps7) {
5600
+ if (!sessionId)
5601
+ return;
5602
+ const want = sessionId.toLowerCase();
5499
5603
  const all = await listRolloutFiles(deps);
5500
5604
  for (const p of all) {
5501
- if (path8.basename(p).endsWith(`-${sessionId}.jsonl`))
5605
+ if (path8.basename(p).match(UUID_AT_END)?.[1]?.toLowerCase() === want)
5502
5606
  return p;
5503
5607
  }
5504
5608
  return;
@@ -5794,7 +5898,7 @@ function deriveCodexUsageMetrics(raw) {
5794
5898
  if (timestampMs !== null && (latestUsageTimestampMs === null || timestampMs > latestUsageTimestampMs)) {
5795
5899
  latestUsageTimestampMs = timestampMs;
5796
5900
  latestUsage = snapshot;
5797
- } else if (latestUsage === undefined) {
5901
+ } else if (latestUsageTimestampMs === null) {
5798
5902
  latestUsage = snapshot;
5799
5903
  }
5800
5904
  }
@@ -6313,6 +6417,7 @@ var init_registry = __esm(() => {
6313
6417
  builtin: true,
6314
6418
  displayName: "Codex",
6315
6419
  defaultCommand: ["codex"],
6420
+ effortLevels: ["none", "low", "medium", "high", "xhigh"],
6316
6421
  history: codexHistoryReader,
6317
6422
  detectAccount: (deps) => detectCodexAccount(deps),
6318
6423
  createHookAdapter: () => new NoopHookAdapter("codex"),
@@ -6364,6 +6469,7 @@ async function deriveTitleFromSessionId(vendor, sessionId) {
6364
6469
  var MAX_SESSIONS_SCANNED = 8;
6365
6470
  var init_auto_title = __esm(() => {
6366
6471
  init_registry();
6472
+ init_task();
6367
6473
  });
6368
6474
 
6369
6475
  // src/tui/panes/terminal/launch.ts
@@ -6797,6 +6903,7 @@ var realRunner, realDeps;
6797
6903
  var init_chat_tab_naming = __esm(() => {
6798
6904
  init_auto_title();
6799
6905
  init_client2();
6906
+ init_task();
6800
6907
  realRunner = { capture: runTmuxCapturing, run: runTmux };
6801
6908
  realDeps = {
6802
6909
  runner: realRunner,
@@ -6891,347 +6998,7 @@ var init_auto_title_poller = __esm(() => {
6891
6998
  init_auto_title();
6892
6999
  init_core();
6893
7000
  init_chat_tab_naming();
6894
- });
6895
-
6896
- // src/lib/poll-scheduling.ts
6897
- import { spawn as spawn2 } from "child_process";
6898
- function computeNextAllowedAt(startedAt, finishedAt, timedOut, cfg) {
6899
- if (timedOut)
6900
- return startedAt + cfg.slowRetryMs;
6901
- return finishedAt + Math.max(cfg.minIntervalMs, (finishedAt - startedAt) * 5);
6902
- }
6903
- function shouldPoll(state, now) {
6904
- return !state.inFlight && now >= state.nextAllowedAt;
6905
- }
6906
- function maybeStartScheduledRun(state, cfg, run, onValue) {
6907
- const startedAt = Date.now();
6908
- if (!shouldPoll(state, startedAt))
6909
- return false;
6910
- state.inFlight = true;
6911
- const controller = new AbortController;
6912
- const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
6913
- (async () => {
6914
- let value;
6915
- let ok = false;
6916
- try {
6917
- value = await run(controller.signal);
6918
- ok = true;
6919
- } catch {}
6920
- clearTimeout(timer);
6921
- const timedOut = controller.signal.aborted;
6922
- state.nextAllowedAt = computeNextAllowedAt(startedAt, Date.now(), timedOut, cfg);
6923
- state.inFlight = false;
6924
- if (ok && !timedOut)
6925
- onValue(value);
6926
- })();
6927
- return true;
6928
- }
6929
- function spawnCapture(cmd, args, opts) {
6930
- return new Promise((resolve2) => {
6931
- let out = "";
6932
- let settled = false;
6933
- const finish = (status) => {
6934
- if (settled)
6935
- return;
6936
- settled = true;
6937
- resolve2({ status, stdout: out });
6938
- };
6939
- const child = spawn2(cmd, args.slice(), {
6940
- cwd: opts.cwd,
6941
- stdio: ["ignore", "pipe", "ignore"],
6942
- env: opts.env,
6943
- signal: opts.signal,
6944
- killSignal: "SIGKILL"
6945
- });
6946
- child.stdout?.on("data", (chunk) => {
6947
- out += String(chunk);
6948
- });
6949
- child.on("error", () => finish(null));
6950
- child.on("close", (code) => finish(code));
6951
- });
6952
- }
6953
- var init_poll_scheduling = () => {};
6954
-
6955
- // ../kobe-daemon/src/daemon/conflict-collector.ts
6956
- class GitGate {
6957
- limit;
6958
- active = 0;
6959
- waiters = [];
6960
- constructor(limit = MAX_CONCURRENT_GIT) {
6961
- this.limit = limit;
6962
- }
6963
- async run(fn) {
6964
- if (this.active >= this.limit) {
6965
- await new Promise((resolve2) => this.waiters.push(resolve2));
6966
- }
6967
- this.active += 1;
6968
- try {
6969
- return await fn();
6970
- } finally {
6971
- this.active -= 1;
6972
- this.waiters.shift()?.();
6973
- }
6974
- }
6975
- }
6976
- async function git(cwd, args, signal, gate) {
6977
- return gate.run(() => spawnCapture("git", args, { cwd, env: { ...process.env, ...LOCK_FREE_ENV }, signal }));
6978
- }
6979
- function parsePorcelainPaths(stdout) {
6980
- const paths = [];
6981
- for (const line of stdout.split(`
6982
- `)) {
6983
- if (line.length < 4)
6984
- continue;
6985
- const rest = line.slice(3);
6986
- const arrow = rest.indexOf(" -> ");
6987
- if (arrow >= 0) {
6988
- paths.push(rest.slice(0, arrow), rest.slice(arrow + 4));
6989
- } else {
6990
- paths.push(rest);
6991
- }
6992
- }
6993
- return paths;
6994
- }
6995
- async function resolveBaseRef(worktree, signal, gate) {
6996
- for (const ref of BASE_REF_CANDIDATES) {
6997
- const res = await git(worktree, ["rev-parse", "--verify", "--quiet", ref], signal, gate);
6998
- if (res.status === 0)
6999
- return ref;
7000
- }
7001
- return null;
7002
- }
7003
- async function collectFootprint(worktree, repo, baseRef, signal, gate) {
7004
- const head = await git(worktree, ["rev-parse", "HEAD"], signal, gate);
7005
- if (head.status !== 0)
7006
- throw new Error("rev-parse HEAD failed");
7007
- const files = new Set;
7008
- const status = await git(worktree, ["status", "--porcelain=v1"], signal, gate);
7009
- if (status.status !== 0)
7010
- throw new Error("git status failed");
7011
- for (const p of parsePorcelainPaths(status.stdout))
7012
- files.add(p);
7013
- if (baseRef) {
7014
- const diff = await git(worktree, ["diff", "--name-only", `${baseRef}...HEAD`], signal, gate);
7015
- if (diff.status === 0) {
7016
- for (const p of diff.stdout.split(`
7017
- `))
7018
- if (p)
7019
- files.add(p);
7020
- }
7021
- }
7022
- return { repo, head: head.stdout.trim(), files };
7023
- }
7024
- function sameFootprint(a, b) {
7025
- if (a.head !== b.head || a.files.size !== b.files.size)
7026
- return false;
7027
- for (const f of a.files)
7028
- if (!b.files.has(f))
7029
- return false;
7030
- return true;
7031
- }
7032
- function trackedConflictTasks(tasks) {
7033
- return tasks.filter((t) => !t.archived && (t.kind ?? "task") !== "main" && !!t.worktreePath && !isRemoteRepoKey(t.repo) && !isRemoteRepoKey(t.worktreePath));
7034
- }
7035
- function overlapPairs(cards) {
7036
- const ids = [...cards.keys()].sort();
7037
- const pairs = [];
7038
- for (let i = 0;i < ids.length; i++) {
7039
- for (let j = i + 1;j < ids.length; j++) {
7040
- const a = cards.get(ids[i]);
7041
- const b = cards.get(ids[j]);
7042
- if (a.repo !== b.repo)
7043
- continue;
7044
- const files = [...a.files].filter((f) => b.files.has(f)).sort();
7045
- if (files.length === 0)
7046
- continue;
7047
- pairs.push({ a: ids[i], b: ids[j], files, level: "overlap" });
7048
- }
7049
- }
7050
- return pairs;
7051
- }
7052
- function parseMergeTreeNames(stdout) {
7053
- const lines = stdout.split(`
7054
- `);
7055
- const names = [];
7056
- for (const line of lines.slice(1)) {
7057
- if (!line)
7058
- break;
7059
- names.push(line);
7060
- }
7061
- return names;
7062
- }
7063
- function samePairs(a, b) {
7064
- return JSON.stringify(a) === JSON.stringify(b);
7065
- }
7066
-
7067
- class ConflictCollector {
7068
- orch;
7069
- bus;
7070
- options;
7071
- entries = new Map;
7072
- baseRefs = new Map;
7073
- mergeProbes = new Map;
7074
- gate = new GitGate;
7075
- mergeTreeUnsupported = false;
7076
- lastPublished = [];
7077
- stopped = false;
7078
- constructor(orch, bus, options = {}) {
7079
- this.orch = orch;
7080
- this.bus = bus;
7081
- this.options = options;
7082
- }
7083
- tick() {
7084
- if (this.stopped)
7085
- return;
7086
- if (this.options.hasSubscribers && !this.options.hasSubscribers())
7087
- return;
7088
- try {
7089
- const tracked = trackedConflictTasks(this.orch.listTasks());
7090
- const trackedIds = new Set(tracked.map((t) => t.id));
7091
- let pruned = false;
7092
- for (const id of this.entries.keys()) {
7093
- if (trackedIds.has(id))
7094
- continue;
7095
- if (this.entries.get(id)?.value)
7096
- pruned = true;
7097
- this.entries.delete(id);
7098
- }
7099
- if (pruned)
7100
- this.recompute();
7101
- for (const task of tracked)
7102
- this.maybeCollect(task);
7103
- } catch (err) {
7104
- logDaemonError("conflict-radar", err);
7105
- }
7106
- }
7107
- stop() {
7108
- this.stopped = true;
7109
- }
7110
- maybeCollect(task) {
7111
- const id = task.id;
7112
- let entry = this.entries.get(id);
7113
- if (!entry) {
7114
- entry = { inFlight: false, nextAllowedAt: 0 };
7115
- this.entries.set(id, entry);
7116
- }
7117
- const cadence = this.options.cadence ?? {
7118
- timeoutMs: CONFLICTS_TIMEOUT_MS,
7119
- slowRetryMs: CONFLICTS_SLOW_RETRY_MS,
7120
- minIntervalMs: CONFLICTS_MIN_INTERVAL_MS
7121
- };
7122
- const run = this.options.footprint ?? (async (t, signal) => {
7123
- const baseRef = await this.baseRefFor(t.worktreePath, signal);
7124
- return collectFootprint(t.worktreePath, t.repo, baseRef, signal, this.gate);
7125
- });
7126
- maybeStartScheduledRun(entry, cadence, (signal) => run(task, signal), (value) => {
7127
- if (this.stopped)
7128
- return;
7129
- if (this.entries.get(id) !== entry)
7130
- return;
7131
- if (entry.value && sameFootprint(entry.value, value))
7132
- return;
7133
- entry.value = value;
7134
- this.recompute();
7135
- });
7136
- }
7137
- baseRefFor(worktree, signal) {
7138
- const cached = this.baseRefs.get(worktree);
7139
- if (cached)
7140
- return cached;
7141
- const promise = resolveBaseRef(worktree, signal, this.gate).catch(() => null);
7142
- this.baseRefs.set(worktree, promise);
7143
- return promise;
7144
- }
7145
- recompute() {
7146
- const cards = new Map;
7147
- for (const [id, entry] of this.entries) {
7148
- if (entry.value)
7149
- cards.set(id, entry.value);
7150
- }
7151
- const pairs = overlapPairs(cards);
7152
- const resolved = [];
7153
- for (const pair of pairs) {
7154
- const a = cards.get(pair.a);
7155
- const b = cards.get(pair.b);
7156
- const key = [a.repo, ...[a.head, b.head].sort()].join("\x00");
7157
- const probe2 = this.mergeProbes.get(key);
7158
- if (probe2?.state === "conflict") {
7159
- resolved.push({
7160
- ...pair,
7161
- level: "conflict",
7162
- files: probe2.files.length > 0 ? probe2.files : pair.files
7163
- });
7164
- continue;
7165
- }
7166
- resolved.push(pair);
7167
- if (!probe2 && !this.mergeTreeUnsupported && a.head !== b.head) {
7168
- this.scheduleMergeProbe(key, cards, pair);
7169
- }
7170
- }
7171
- if (samePairs(this.lastPublished, resolved))
7172
- return;
7173
- this.lastPublished = resolved;
7174
- const payload = { pairs: resolved };
7175
- this.bus.publish("task.conflicts", payload);
7176
- }
7177
- scheduleMergeProbe(key, cards, pair) {
7178
- const a = cards.get(pair.a);
7179
- const b = cards.get(pair.b);
7180
- this.mergeProbes.set(key, { state: "pending" });
7181
- const worktree = this.worktreeOf(pair.a);
7182
- const probe2 = this.options.probeMerge ?? (async (wt, headA, headB) => {
7183
- const res = await git(wt, ["merge-tree", "--write-tree", "--name-only", headA, headB], AbortSignal.timeout(MERGE_TREE_TIMEOUT_MS), this.gate);
7184
- if (res.status === 0)
7185
- return { conflict: false, files: [] };
7186
- if (res.status === 1)
7187
- return { conflict: true, files: parseMergeTreeNames(res.stdout) };
7188
- return null;
7189
- });
7190
- if (!worktree) {
7191
- this.mergeProbes.delete(key);
7192
- return;
7193
- }
7194
- probe2(worktree, a.head, b.head).then((result) => {
7195
- if (this.stopped)
7196
- return;
7197
- if (result === null) {
7198
- if (!this.mergeTreeUnsupported) {
7199
- this.mergeTreeUnsupported = true;
7200
- console.log("[conflict-radar] merge-tree dry-run unavailable (git < 2.38 or no merge base) \u2014 radar degrades to file-overlap only");
7201
- }
7202
- this.mergeProbes.delete(key);
7203
- return;
7204
- }
7205
- this.mergeProbes.set(key, result.conflict ? { state: "conflict", files: result.files } : { state: "clean" });
7206
- this.recompute();
7207
- }).catch((err) => {
7208
- this.mergeProbes.delete(key);
7209
- logDaemonError("conflict-radar", err);
7210
- });
7211
- }
7212
- worktreeOf(taskId) {
7213
- const task = this.orch.listTasks().find((t) => t.id === taskId);
7214
- return task?.worktreePath || undefined;
7215
- }
7216
- }
7217
- function startConflictCollector(orch, bus, tickMs = DEFAULT_CONFLICTS_TICK_MS, hasSubscribers) {
7218
- if (tickMs <= 0)
7219
- return () => {};
7220
- const collector = new ConflictCollector(orch, bus, { hasSubscribers });
7221
- collector.tick();
7222
- const timer = setInterval(() => collector.tick(), tickMs);
7223
- timer.unref?.();
7224
- return () => {
7225
- clearInterval(timer);
7226
- collector.stop();
7227
- };
7228
- }
7229
- var DEFAULT_CONFLICTS_TICK_MS = 5000, CONFLICTS_TIMEOUT_MS = 5000, CONFLICTS_SLOW_RETRY_MS = 60000, CONFLICTS_MIN_INTERVAL_MS = 1e4, MAX_CONCURRENT_GIT = 3, MERGE_TREE_TIMEOUT_MS = 8000, LOCK_FREE_ENV, BASE_REF_CANDIDATES;
7230
- var init_conflict_collector = __esm(() => {
7231
- init_poll_scheduling();
7232
- init_repos();
7233
- LOCK_FREE_ENV = { GIT_OPTIONAL_LOCKS: "0" };
7234
- BASE_REF_CANDIDATES = ["origin/HEAD", "origin/main", "origin/master", "main", "master"];
7001
+ init_task();
7235
7002
  });
7236
7003
 
7237
7004
  // ../kobe-daemon/src/daemon/event-bus.ts
@@ -7466,7 +7233,8 @@ function createDaemonHandlerRegistry() {
7466
7233
  title: optionalString(payload, "title"),
7467
7234
  branch: optionalString(payload, "branch"),
7468
7235
  baseRef: optionalString(payload, "baseRef"),
7469
- vendor: optionalVendor(payload, "vendor")
7236
+ vendor: optionalVendor(payload, "vendor"),
7237
+ modelEffort: optionalString(payload, "effort")
7470
7238
  });
7471
7239
  return { taskId: task.id, task: serializeTask(task) };
7472
7240
  }
@@ -7539,10 +7307,20 @@ function createDaemonHandlerRegistry() {
7539
7307
  async handle(payload, ctx) {
7540
7308
  const taskId = requireString(payload, "taskId");
7541
7309
  const status = requireString(payload, "status");
7542
- if (status !== "backlog" && status !== "in_progress" && status !== "in_review" && status !== "done" && status !== "canceled" && status !== "error") {
7310
+ if (!isTaskStatus(status))
7543
7311
  throw new Error("status must be a TaskStatus");
7544
- }
7312
+ const linked = status === "done" ? ctx.orch.getTask(taskId) : undefined;
7313
+ const prevStatus = linked?.status;
7545
7314
  await ctx.orch.setStatus(taskId, status);
7315
+ if (status === "done" && prevStatus !== "done" && linked) {
7316
+ try {
7317
+ const next = await ctx.issues.mirrorTaskDone(linked.repo, taskId);
7318
+ if (next)
7319
+ ctx.bus.publish("issue.snapshot", next);
7320
+ } catch (err) {
7321
+ logDaemonError("issue-done-mirror", err);
7322
+ }
7323
+ }
7546
7324
  return {};
7547
7325
  }
7548
7326
  },
@@ -7645,6 +7423,20 @@ function createDaemonHandlerRegistry() {
7645
7423
  return {};
7646
7424
  }
7647
7425
  },
7426
+ {
7427
+ name: "issue.list",
7428
+ async handle(payload, ctx) {
7429
+ return ctx.issues.list(requireString(payload, "repoRoot"));
7430
+ }
7431
+ },
7432
+ {
7433
+ name: "issue.mutate",
7434
+ async handle(payload, ctx) {
7435
+ const state = await ctx.issues.mutate(requireString(payload, "repoRoot"), payload.op);
7436
+ ctx.bus.publish("issue.snapshot", state);
7437
+ return state;
7438
+ }
7439
+ },
7648
7440
  {
7649
7441
  name: "session.deliver",
7650
7442
  async handle(payload, ctx) {
@@ -7770,24 +7562,348 @@ function optionalActivityDetail(payload) {
7770
7562
  var init_handlers = __esm(() => {
7771
7563
  init_hook_events();
7772
7564
  init_status_rules();
7565
+ init_task();
7773
7566
  init_version();
7774
7567
  init_cwd_task();
7775
7568
  init_protocol();
7776
7569
  });
7777
7570
 
7571
+ // ../kobe-daemon/src/daemon/issues-store.ts
7572
+ import { execFile } from "child_process";
7573
+ import { mkdir as mkdir6, readFile as readFile7, realpath, rename as rename2, stat as stat4, writeFile as writeFile4 } from "fs/promises";
7574
+ import { homedir as homedir13 } from "os";
7575
+ import { dirname as dirname5, isAbsolute, join as join4, resolve as resolve2 } from "path";
7576
+ import { promisify } from "util";
7577
+ function isGitNotRepositoryError(err) {
7578
+ const message = err instanceof Error ? err.message : String(err);
7579
+ return message.includes("not a git repository");
7580
+ }
7581
+ function defaultIssuesStorePath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir13()) {
7582
+ return join4(homeDir2, ".kobe", "issues.json");
7583
+ }
7584
+ function isValidStatus(value) {
7585
+ return typeof value === "string" && ISSUE_STATUSES.includes(value);
7586
+ }
7587
+ function normalizeIssue(entry) {
7588
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry))
7589
+ return null;
7590
+ const raw = entry;
7591
+ if (typeof raw.id !== "number")
7592
+ return null;
7593
+ return {
7594
+ id: raw.id,
7595
+ title: typeof raw.title === "string" ? raw.title : "(untitled)",
7596
+ status: isValidStatus(raw.status) ? raw.status : "open",
7597
+ created: typeof raw.created === "string" ? raw.created : "",
7598
+ body: typeof raw.body === "string" ? raw.body : "",
7599
+ taskId: typeof raw.taskId === "string" ? raw.taskId : undefined
7600
+ };
7601
+ }
7602
+ function emptyStore() {
7603
+ return { version: 1, repos: {} };
7604
+ }
7605
+ function todayStamp() {
7606
+ const d = new Date;
7607
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
7608
+ const dd = String(d.getDate()).padStart(2, "0");
7609
+ return `${d.getFullYear()}-${mm}-${dd}`;
7610
+ }
7611
+ async function gitCommonDir(path11) {
7612
+ const { stdout } = await execFileAsync("git", ["-C", path11, "rev-parse", "--git-common-dir"]);
7613
+ const dir = stdout.trim();
7614
+ return realpath(isAbsolute(dir) ? dir : resolve2(path11, dir));
7615
+ }
7616
+ async function gitTopLevel(path11) {
7617
+ const { stdout } = await execFileAsync("git", ["-C", path11, "rev-parse", "--show-toplevel"]);
7618
+ return stdout.trim();
7619
+ }
7620
+ async function gitMainWorktree(path11) {
7621
+ const { stdout } = await execFileAsync("git", ["-C", path11, "worktree", "list", "--porcelain"]);
7622
+ const first = stdout.split(/\r?\n/).find((line) => line.startsWith("worktree "))?.slice("worktree ".length).trim();
7623
+ return first ? realpath(first) : gitTopLevel(path11);
7624
+ }
7625
+ async function resolveRepo(raw) {
7626
+ if (typeof raw !== "string" || raw.length === 0)
7627
+ throw new Error("repoRoot is required");
7628
+ const absolute = resolve2(raw);
7629
+ const s = await stat4(absolute).catch(() => null);
7630
+ if (!s?.isDirectory())
7631
+ throw new Error("repoRoot does not exist");
7632
+ try {
7633
+ const [repoRoot, repoKey] = await Promise.all([gitMainWorktree(absolute), gitCommonDir(absolute)]);
7634
+ return { repoRoot, repoKey };
7635
+ } catch (err) {
7636
+ if (isGitNotRepositoryError(err))
7637
+ throw new Error("repoRoot is not a git repository");
7638
+ throw err;
7639
+ }
7640
+ }
7641
+ async function readStore(path11) {
7642
+ try {
7643
+ const raw = JSON.parse(await readFile7(path11, "utf8"));
7644
+ const repos = {};
7645
+ if (raw.repos && typeof raw.repos === "object") {
7646
+ for (const [key, value] of Object.entries(raw.repos)) {
7647
+ if (!value || typeof value !== "object")
7648
+ continue;
7649
+ const record = value;
7650
+ repos[key] = {
7651
+ repoRoot: typeof record.repoRoot === "string" ? record.repoRoot : "",
7652
+ nextId: typeof record.nextId === "number" ? record.nextId : 1,
7653
+ issues: Array.isArray(record.issues) ? record.issues.map(normalizeIssue).filter((issue) => issue !== null) : []
7654
+ };
7655
+ }
7656
+ }
7657
+ return { version: 1, repos };
7658
+ } catch (err) {
7659
+ if (err.code === "ENOENT")
7660
+ return emptyStore();
7661
+ throw err;
7662
+ }
7663
+ }
7664
+ async function writeStore(path11, store) {
7665
+ await mkdir6(dirname5(path11), { recursive: true });
7666
+ const tmp = `${path11}.tmp`;
7667
+ await writeFile4(tmp, `${JSON.stringify(store, null, 2)}
7668
+ `, "utf8");
7669
+ await rename2(tmp, path11);
7670
+ }
7671
+ function response(repoRoot, record) {
7672
+ return {
7673
+ repoRoot,
7674
+ exists: record !== null,
7675
+ nextId: record?.nextId ?? 1,
7676
+ issues: record?.issues ?? []
7677
+ };
7678
+ }
7679
+ async function withLock(key, fn) {
7680
+ const tail = locks.get(key) ?? Promise.resolve();
7681
+ const run = tail.then(fn);
7682
+ const settled = run.then(() => {
7683
+ return;
7684
+ }, () => {
7685
+ return;
7686
+ });
7687
+ locks.set(key, settled);
7688
+ settled.then(() => {
7689
+ if (locks.get(key) === settled)
7690
+ locks.delete(key);
7691
+ });
7692
+ return run;
7693
+ }
7694
+
7695
+ class IssuesStore {
7696
+ path;
7697
+ constructor(path11 = defaultIssuesStorePath()) {
7698
+ this.path = path11;
7699
+ }
7700
+ async list(repo) {
7701
+ const { repoRoot, repoKey } = await resolveRepo(repo);
7702
+ return withLock(this.path, async () => {
7703
+ const store = await readStore(this.path);
7704
+ const record = store.repos[repoKey] ?? null;
7705
+ if (record && record.repoRoot !== repoRoot) {
7706
+ record.repoRoot = repoRoot;
7707
+ await writeStore(this.path, store);
7708
+ }
7709
+ return response(repoRoot, record);
7710
+ });
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
+ }
7730
+ async mutate(repo, op) {
7731
+ const { repoRoot, repoKey } = await resolveRepo(repo);
7732
+ if (!op || typeof op !== "object" || Array.isArray(op) || typeof op.type !== "string") {
7733
+ throw new Error("missing op");
7734
+ }
7735
+ return withLock(this.path, async () => {
7736
+ const store = await readStore(this.path);
7737
+ let record = store.repos[repoKey];
7738
+ if (!record) {
7739
+ record = { repoRoot, nextId: 1, issues: [] };
7740
+ store.repos[repoKey] = record;
7741
+ }
7742
+ record.repoRoot = repoRoot;
7743
+ const typed = op;
7744
+ if (typed.type === "create") {
7745
+ if (typeof typed.title !== "string" || typed.title.trim().length === 0) {
7746
+ throw new Error("create requires a non-empty title");
7747
+ }
7748
+ if (typed.body !== undefined && typeof typed.body !== "string")
7749
+ throw new Error("body must be a string");
7750
+ record.issues = [
7751
+ {
7752
+ id: record.nextId,
7753
+ title: typed.title,
7754
+ status: "open",
7755
+ created: todayStamp(),
7756
+ body: typeof typed.body === "string" ? typed.body : ""
7757
+ },
7758
+ ...record.issues
7759
+ ];
7760
+ record.nextId += 1;
7761
+ } else if (typed.type === "setStatus") {
7762
+ if (typeof typed.id !== "number")
7763
+ throw new Error("setStatus requires a numeric id");
7764
+ if (!isValidStatus(typed.status))
7765
+ throw new Error(`invalid status: must be one of ${ISSUE_STATUSES.join(", ")}`);
7766
+ const issue = record.issues.find((i) => i.id === typed.id);
7767
+ if (!issue)
7768
+ throw new Error(`no issue #${typed.id}`);
7769
+ issue.status = typed.status;
7770
+ } else if (typed.type === "update") {
7771
+ if (typeof typed.id !== "number")
7772
+ throw new Error("update requires a numeric id");
7773
+ if (typed.title !== undefined && (typeof typed.title !== "string" || typed.title.trim().length === 0)) {
7774
+ throw new Error("title must be a non-empty string");
7775
+ }
7776
+ if (typed.body !== undefined && typeof typed.body !== "string")
7777
+ throw new Error("body must be a string");
7778
+ const issue = record.issues.find((i) => i.id === typed.id);
7779
+ if (!issue)
7780
+ throw new Error(`no issue #${typed.id}`);
7781
+ if (typeof typed.title === "string")
7782
+ issue.title = typed.title;
7783
+ if (typeof typed.body === "string")
7784
+ issue.body = typed.body;
7785
+ } else if (typed.type === "link") {
7786
+ if (typeof typed.id !== "number")
7787
+ throw new Error("link requires a numeric id");
7788
+ if (typeof typed.taskId !== "string" || typed.taskId.length === 0) {
7789
+ throw new Error("link requires a non-empty taskId");
7790
+ }
7791
+ const issue = record.issues.find((i) => i.id === typed.id);
7792
+ if (!issue)
7793
+ throw new Error(`no issue #${typed.id}`);
7794
+ issue.taskId = typed.taskId;
7795
+ } else if (typed.type === "unlink") {
7796
+ if (typeof typed.id !== "number")
7797
+ throw new Error("unlink requires a numeric id");
7798
+ const issue = record.issues.find((i) => i.id === typed.id);
7799
+ if (!issue)
7800
+ throw new Error(`no issue #${typed.id}`);
7801
+ issue.taskId = undefined;
7802
+ } else if (typed.type === "delete") {
7803
+ if (typeof typed.id !== "number")
7804
+ throw new Error("delete requires a numeric id");
7805
+ const nextIssues = record.issues.filter((i) => i.id !== typed.id);
7806
+ if (nextIssues.length === record.issues.length)
7807
+ throw new Error(`no issue #${typed.id}`);
7808
+ record.issues = nextIssues;
7809
+ } else {
7810
+ throw new Error(`unknown op type: ${typed.type}`);
7811
+ }
7812
+ await writeStore(this.path, store);
7813
+ return response(repoRoot, record);
7814
+ });
7815
+ }
7816
+ }
7817
+ var execFileAsync, ISSUE_STATUSES, locks;
7818
+ var init_issues_store = __esm(() => {
7819
+ execFileAsync = promisify(execFile);
7820
+ ISSUE_STATUSES = ["open", "doing", "hold", "done"];
7821
+ locks = new Map;
7822
+ });
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
+
7778
7894
  // ../kobe-daemon/src/daemon/keybindings-watcher.ts
7779
7895
  import { mkdirSync as mkdirSync2, watch } from "fs";
7780
- import { homedir as homedir13 } from "os";
7781
- import { basename as basename3, dirname as dirname5, join as join4 } from "path";
7782
- function defaultKeybindingsPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir13()) {
7783
- return join4(homeDir2, ".kobe", "settings", "keybindings.yaml");
7896
+ import { homedir as homedir14 } from "os";
7897
+ import { basename as basename3, dirname as dirname6, join as join5 } from "path";
7898
+ function defaultKeybindingsPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir14()) {
7899
+ return join5(homeDir2, ".kobe", "settings", "keybindings.yaml");
7784
7900
  }
7785
7901
  function startKeybindingsWatcher(bus, options = {}) {
7786
7902
  const debounceMs = options.debounceMs ?? DEFAULT_KEYBINDINGS_DEBOUNCE_MS;
7787
7903
  if (debounceMs <= 0)
7788
7904
  return () => {};
7789
7905
  const filePath = options.path ?? defaultKeybindingsPath();
7790
- const dir = dirname5(filePath);
7906
+ const dir = dirname6(filePath);
7791
7907
  const baseYaml = basename3(filePath);
7792
7908
  const baseYml = baseYaml.replace(/\.yaml$/, ".yml");
7793
7909
  let rev = 0;
@@ -7831,10 +7947,10 @@ var init_keybindings_watcher = () => {};
7831
7947
 
7832
7948
  // ../kobe-daemon/src/daemon/ui-prefs-watcher.ts
7833
7949
  import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, watch as watch2 } from "fs";
7834
- import { homedir as homedir14 } from "os";
7835
- import { basename as basename4, dirname as dirname6, join as join5 } from "path";
7836
- function defaultUiPrefsStatePath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir14()) {
7837
- return join5(homeDir2, ".config", "kobe", "state.json");
7950
+ import { homedir as homedir15 } from "os";
7951
+ import { basename as basename4, dirname as dirname7, join as join6 } from "path";
7952
+ function defaultUiPrefsStatePath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir15()) {
7953
+ return join6(homeDir2, ".config", "kobe", "state.json");
7838
7954
  }
7839
7955
  function readUiPrefsFromStateFile(statePath2) {
7840
7956
  let parsed = {};
@@ -7858,7 +7974,7 @@ function startUiPrefsWatcher(bus, options = {}) {
7858
7974
  if (debounceMs <= 0)
7859
7975
  return () => {};
7860
7976
  const statePath2 = options.statePath ?? defaultUiPrefsStatePath();
7861
- const stateDir = dirname6(statePath2);
7977
+ const stateDir = dirname7(statePath2);
7862
7978
  const stateFile = basename4(statePath2);
7863
7979
  let last = readUiPrefsFromStateFile(statePath2);
7864
7980
  bus.publish("ui-prefs", last);
@@ -7904,6 +8020,65 @@ var init_ui_prefs_watcher = __esm(() => {
7904
8020
  FOCUS_ACCENT_SLOT_NAMES = ["primary", "success", "info"];
7905
8021
  });
7906
8022
 
8023
+ // src/lib/poll-scheduling.ts
8024
+ import { spawn as spawn2 } from "child_process";
8025
+ function computeNextAllowedAt(startedAt, finishedAt, timedOut, cfg) {
8026
+ if (timedOut)
8027
+ return startedAt + cfg.slowRetryMs;
8028
+ return finishedAt + Math.max(cfg.minIntervalMs, (finishedAt - startedAt) * 5);
8029
+ }
8030
+ function shouldPoll(state, now) {
8031
+ return !state.inFlight && now >= state.nextAllowedAt;
8032
+ }
8033
+ function maybeStartScheduledRun(state, cfg, run, onValue) {
8034
+ const startedAt = Date.now();
8035
+ if (!shouldPoll(state, startedAt))
8036
+ return false;
8037
+ state.inFlight = true;
8038
+ const controller = new AbortController;
8039
+ const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
8040
+ (async () => {
8041
+ let value;
8042
+ let ok = false;
8043
+ try {
8044
+ value = await run(controller.signal);
8045
+ ok = true;
8046
+ } catch {}
8047
+ clearTimeout(timer);
8048
+ const timedOut = controller.signal.aborted;
8049
+ state.nextAllowedAt = computeNextAllowedAt(startedAt, Date.now(), timedOut, cfg);
8050
+ state.inFlight = false;
8051
+ if (ok && !timedOut)
8052
+ onValue(value);
8053
+ })();
8054
+ return true;
8055
+ }
8056
+ function spawnCapture(cmd, args, opts) {
8057
+ return new Promise((resolve3) => {
8058
+ let out = "";
8059
+ let settled = false;
8060
+ const finish = (status) => {
8061
+ if (settled)
8062
+ return;
8063
+ settled = true;
8064
+ resolve3({ status, stdout: out });
8065
+ };
8066
+ const child = spawn2(cmd, args.slice(), {
8067
+ cwd: opts.cwd,
8068
+ stdio: ["ignore", "pipe", "ignore"],
8069
+ env: opts.env,
8070
+ signal: opts.signal,
8071
+ killSignal: "SIGKILL"
8072
+ });
8073
+ child.stdout?.on("data", (chunk) => {
8074
+ out += String(chunk);
8075
+ });
8076
+ child.on("error", () => finish(null));
8077
+ child.on("close", (code) => finish(code));
8078
+ });
8079
+ }
8080
+ var init_poll_scheduling = () => {};
8081
+
7907
8082
  // src/tui/panes/sidebar/worktree-changes.ts
7908
8083
  var exports_worktree_changes = {};
7909
8084
  __export(exports_worktree_changes, {
@@ -8074,9 +8249,10 @@ var init_worktree_changes_collector = __esm(() => {
8074
8249
  });
8075
8250
 
8076
8251
  // ../kobe-daemon/src/daemon/server.ts
8077
- import { mkdir as mkdir6, readFile as readFile7, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
8252
+ import { mkdir as mkdir7, readFile as readFile8, unlink as unlink4, writeFile as writeFile5 } from "fs/promises";
8078
8253
  import { createServer } from "net";
8079
- import { dirname as dirname7 } from "path";
8254
+ import { dirname as dirname8 } from "path";
8255
+ import { StringDecoder as StringDecoder2 } from "string_decoder";
8080
8256
  function resolveIdleGraceMs() {
8081
8257
  const raw = process.env.KOBE_DAEMON_IDLE_GRACE_MS;
8082
8258
  if (raw === undefined)
@@ -8090,49 +8266,19 @@ async function startDaemonServer(orch, options = {}) {
8090
8266
  const startedAt = options.startedAt ?? new Date;
8091
8267
  const clients = new Set;
8092
8268
  let nextClientId = 1;
8093
- const idleGraceMs = resolveIdleGraceMs();
8094
- let idleTimer = null;
8095
- let stopping = false;
8096
- function guiCount() {
8097
- let n = 0;
8098
- for (const c of clients)
8099
- if (c.holdsLifetime)
8100
- n++;
8101
- return n;
8102
- }
8103
- function hasSubscribers() {
8104
- for (const c of clients)
8105
- if (c.subscribed)
8106
- return true;
8107
- return false;
8108
- }
8109
- function cancelIdleTimer() {
8110
- if (idleTimer) {
8111
- clearTimeout(idleTimer);
8112
- idleTimer = null;
8113
- }
8114
- }
8115
- function maybeArmIdleShutdown() {
8116
- if (stopping || guiCount() > 0)
8117
- return;
8118
- cancelIdleTimer();
8119
- logDaemonInfo("idle", `last gui gone \u2014 arming ${idleGraceMs}ms idle-stop grace`);
8120
- idleTimer = setTimeout(() => {
8121
- idleTimer = null;
8122
- if (stopping || guiCount() > 0)
8123
- return;
8124
- logDaemonInfo("idle", "grace elapsed with no gui \u2014 self-stopping");
8125
- stopSoon().catch((err) => logDaemonError("daemon-idle-shutdown", err));
8126
- }, idleGraceMs);
8127
- idleTimer.unref?.();
8128
- }
8269
+ const lifetime = new DaemonLifetime({
8270
+ clients: () => clients,
8271
+ idleGraceMs: resolveIdleGraceMs(),
8272
+ onIdleStop: () => void stopSoon().catch((err) => logDaemonError("daemon-idle-shutdown", err))
8273
+ });
8129
8274
  const bus = new DaemonEventBus;
8130
8275
  bus.onPublish((event) => {
8131
8276
  broadcast(clients, { type: "event", name: event.channel, payload: event.payload });
8132
8277
  });
8133
8278
  const activity = new DaemonActivityRegistry(bus);
8134
- await mkdir6(dirname7(socketPath), { recursive: true });
8135
- await mkdir6(dirname7(pidPath), { recursive: true });
8279
+ const issues = new IssuesStore(defaultIssuesStorePath(options.homeDir));
8280
+ await mkdir7(dirname8(socketPath), { recursive: true });
8281
+ await mkdir7(dirname8(pidPath), { recursive: true });
8136
8282
  await unlink4(socketPath).catch(() => {});
8137
8283
  const server = createServer((socket) => {
8138
8284
  const client = {
@@ -8145,18 +8291,18 @@ async function startDaemonServer(orch, options = {}) {
8145
8291
  channels: null
8146
8292
  };
8147
8293
  clients.add(client);
8294
+ const decoder = new StringDecoder2("utf8");
8148
8295
  socket.on("data", (chunk) => {
8149
- client.buffer += chunk.toString("utf8");
8296
+ client.buffer += decoder.write(chunk);
8150
8297
  drainClientBuffer(client);
8151
8298
  });
8152
8299
  socket.on("error", () => {});
8153
8300
  socket.on("close", () => {
8154
8301
  clients.delete(client);
8155
8302
  if (client.subscribed) {
8156
- 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`);
8157
8304
  }
8158
- if (client.holdsLifetime)
8159
- maybeArmIdleShutdown();
8305
+ lifetime.clientDisconnected(client.holdsLifetime);
8160
8306
  });
8161
8307
  });
8162
8308
  const unsubscribeStore = orch.subscribeTasks((snapshot) => {
@@ -8174,7 +8320,7 @@ async function startDaemonServer(orch, options = {}) {
8174
8320
  updateTimer.unref?.();
8175
8321
  }
8176
8322
  const autoTitlePollMs = options.autoTitlePollMs ?? DEFAULT_AUTO_TITLE_POLL_MS;
8177
- const stopAutoTitlePoller = startAutoTitlePoller(orch, autoTitlePollMs, hasSubscribers);
8323
+ const stopAutoTitlePoller = startAutoTitlePoller(orch, autoTitlePollMs, () => lifetime.hasSubscribers());
8178
8324
  const stopUiPrefsWatcher = startUiPrefsWatcher(bus, {
8179
8325
  statePath: defaultUiPrefsStatePath(options.homeDir),
8180
8326
  debounceMs: options.uiPrefsDebounceMs ?? DEFAULT_UI_PREFS_DEBOUNCE_MS
@@ -8183,16 +8329,14 @@ async function startDaemonServer(orch, options = {}) {
8183
8329
  path: defaultKeybindingsPath(options.homeDir),
8184
8330
  debounceMs: options.keybindingsDebounceMs ?? DEFAULT_KEYBINDINGS_DEBOUNCE_MS
8185
8331
  });
8186
- const stopWorktreeChangesCollector = startWorktreeChangesCollector(orch, bus, options.worktreeChangesTickMs ?? DEFAULT_WORKTREE_CHANGES_TICK_MS, hasSubscribers);
8187
- const stopConflictCollector = startConflictCollector(orch, bus, options.conflictsTickMs ?? DEFAULT_CONFLICTS_TICK_MS, hasSubscribers);
8332
+ const stopWorktreeChangesCollector = startWorktreeChangesCollector(orch, bus, options.worktreeChangesTickMs ?? DEFAULT_WORKTREE_CHANGES_TICK_MS, () => lifetime.hasSubscribers());
8188
8333
  const serverApi = {
8189
8334
  socketPath,
8190
8335
  pidPath,
8191
8336
  startedAt,
8192
8337
  clients,
8193
8338
  async close() {
8194
- stopping = true;
8195
- cancelIdleTimer();
8339
+ lifetime.markStopping();
8196
8340
  unsubscribeStore();
8197
8341
  if (updateTimer)
8198
8342
  clearInterval(updateTimer);
@@ -8200,32 +8344,30 @@ async function startDaemonServer(orch, options = {}) {
8200
8344
  stopUiPrefsWatcher();
8201
8345
  stopKeybindingsWatcher();
8202
8346
  stopWorktreeChangesCollector();
8203
- stopConflictCollector();
8204
8347
  activity.close();
8205
8348
  broadcast(clients, { type: "event", name: "daemon.stopping", payload: {} });
8206
8349
  for (const client of Array.from(clients)) {
8207
8350
  client.socket.destroy();
8208
8351
  }
8209
- await new Promise((resolve2) => server.close(() => resolve2()));
8352
+ await new Promise((resolve3) => server.close(() => resolve3()));
8210
8353
  await unlink4(socketPath).catch(() => {});
8211
8354
  await unlink4(pidPath).catch(() => {});
8212
8355
  }
8213
8356
  };
8214
- await new Promise((resolve2, reject) => {
8357
+ await new Promise((resolve3, reject) => {
8215
8358
  const evented = server;
8216
8359
  evented.once("error", reject);
8217
8360
  server.listen(socketPath, () => {
8218
8361
  evented.removeListener("error", reject);
8219
- resolve2();
8362
+ resolve3();
8220
8363
  });
8221
8364
  });
8222
- await writeFile4(pidPath, `${process.pid}
8365
+ await writeFile5(pidPath, `${process.pid}
8223
8366
  `, "utf8");
8224
8367
  async function stopSoon() {
8225
- if (stopping)
8368
+ if (lifetime.isStopping())
8226
8369
  return;
8227
- stopping = true;
8228
- cancelIdleTimer();
8370
+ lifetime.markStopping();
8229
8371
  await options.onStop?.();
8230
8372
  setTimeout(() => {
8231
8373
  serverApi.close().catch((err) => logDaemonError("daemon-shutdown", err));
@@ -8242,8 +8384,8 @@ async function startDaemonServer(orch, options = {}) {
8242
8384
  client.channels = normalizeChannelFilter(payload.channels);
8243
8385
  const firstSubscriber = !wasSubscribed;
8244
8386
  if (client.holdsLifetime)
8245
- cancelIdleTimer();
8246
- 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)" : ""}`);
8247
8389
  for (const event of bus.snapshot()) {
8248
8390
  if (client.channels && !client.channels.has(event.channel))
8249
8391
  continue;
@@ -8264,7 +8406,8 @@ async function startDaemonServer(orch, options = {}) {
8264
8406
  orch,
8265
8407
  bus,
8266
8408
  activity,
8267
- daemon: { startedAt, socketPath, pid: process.pid, guiCount, stopSoon },
8409
+ issues,
8410
+ daemon: { startedAt, socketPath, pid: process.pid, guiCount: () => lifetime.guiCount(), stopSoon },
8268
8411
  clientId: client.id
8269
8412
  });
8270
8413
  }
@@ -8304,7 +8447,7 @@ async function startDaemonServer(orch, options = {}) {
8304
8447
  }
8305
8448
  async function readPidFile(pidPath) {
8306
8449
  try {
8307
- const raw = await readFile7(pidPath, "utf8");
8450
+ const raw = await readFile8(pidPath, "utf8");
8308
8451
  const pid = Number(raw.trim());
8309
8452
  return Number.isFinite(pid) ? pid : null;
8310
8453
  } catch {
@@ -8331,14 +8474,16 @@ var init_server = __esm(() => {
8331
8474
  init_version();
8332
8475
  init_activity_registry();
8333
8476
  init_auto_title_poller();
8334
- init_conflict_collector();
8335
8477
  init_handlers();
8478
+ init_issues_store();
8479
+ init_lifetime();
8336
8480
  init_keybindings_watcher();
8337
8481
  init_paths2();
8338
8482
  init_protocol();
8339
8483
  init_ui_prefs_watcher();
8340
8484
  init_worktree_changes_collector();
8341
8485
  init_handlers();
8486
+ init_issues_store();
8342
8487
  DEFAULT_UPDATE_POLL_MS = 6 * 60 * 60 * 1000;
8343
8488
  });
8344
8489
 
@@ -8361,7 +8506,7 @@ async function stopDaemonProcess(socketPath, pidPath) {
8361
8506
  const stopRequest = client.request("daemon.stop").catch(() => {
8362
8507
  return;
8363
8508
  });
8364
- const stopTimeout = new Promise((resolve2) => setTimeout(resolve2, 2000));
8509
+ const stopTimeout = new Promise((resolve3) => setTimeout(resolve3, 2000));
8365
8510
  await Promise.race([stopRequest, stopTimeout]);
8366
8511
  client.close();
8367
8512
  if (wasAlive && targetPid !== null) {
@@ -8380,13 +8525,13 @@ async function stopDaemonProcess(socketPath, pidPath) {
8380
8525
  method = "sigterm";
8381
8526
  escalated = true;
8382
8527
  }
8383
- await new Promise((resolve2) => setTimeout(resolve2, 50));
8528
+ await new Promise((resolve3) => setTimeout(resolve3, 50));
8384
8529
  }
8385
8530
  try {
8386
8531
  process.kill(targetPid, 0);
8387
8532
  process.kill(targetPid, "SIGKILL");
8388
8533
  method = "sigkill";
8389
- await new Promise((resolve2) => setTimeout(resolve2, 100));
8534
+ await new Promise((resolve3) => setTimeout(resolve3, 100));
8390
8535
  } catch {}
8391
8536
  }
8392
8537
  await unlink5(socketPath).catch(() => {});
@@ -8408,13 +8553,13 @@ __export(exports_daemon_process, {
8408
8553
  });
8409
8554
  import { spawn as spawn3 } from "child_process";
8410
8555
  import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync4, openSync } from "fs";
8411
- import { dirname as dirname8, resolve as resolve2 } from "path";
8556
+ import { dirname as dirname9, resolve as resolve3 } from "path";
8412
8557
  import { fileURLToPath as fileURLToPath2 } from "url";
8413
8558
  function spawnDetachedDaemon(command, args, env, logPath) {
8414
8559
  let stdio = "ignore";
8415
8560
  let logFd;
8416
8561
  try {
8417
- mkdirSync4(dirname8(logPath), { recursive: true });
8562
+ mkdirSync4(dirname9(logPath), { recursive: true });
8418
8563
  logFd = openSync(logPath, "a");
8419
8564
  stdio = ["ignore", logFd, logFd];
8420
8565
  } catch {
@@ -8467,8 +8612,8 @@ async function testDaemonResponds(socketPath, timeoutMs = DAEMON_HELLO_TIMEOUT_M
8467
8612
  }
8468
8613
  const replied = probe2.request("hello", { protocolVersion: DAEMON_PROTOCOL_VERSION }).then(() => true).catch(() => true);
8469
8614
  let timer;
8470
- const timedOut = new Promise((resolve3) => {
8471
- timer = setTimeout(() => resolve3(false), timeoutMs);
8615
+ const timedOut = new Promise((resolve4) => {
8616
+ timer = setTimeout(() => resolve4(false), timeoutMs);
8472
8617
  });
8473
8618
  const alive = await Promise.race([replied, timedOut]);
8474
8619
  if (timer)
@@ -8481,11 +8626,11 @@ function resolveKobeSpawn(subcommand) {
8481
8626
  if (here.startsWith("/$bunfs") || here.startsWith("B:\\~BUN")) {
8482
8627
  return [process.execPath, ...subcommand];
8483
8628
  }
8484
- const dir = dirname8(here);
8629
+ const dir = dirname9(here);
8485
8630
  const candidates = [
8486
- resolve2(dir, "../cli/index.ts"),
8487
- resolve2(dir, "../../../kobe/src/cli/index.ts"),
8488
- resolve2(dir, "../cli/index.js")
8631
+ resolve3(dir, "../cli/index.ts"),
8632
+ resolve3(dir, "../../../kobe/src/cli/index.ts"),
8633
+ resolve3(dir, "../cli/index.js")
8489
8634
  ];
8490
8635
  const entry = candidates.find((candidate) => existsSync5(candidate));
8491
8636
  if (entry)
@@ -8507,7 +8652,7 @@ __export(exports_repo_cmd, {
8507
8652
  runRepoSubcommand: () => runRepoSubcommand
8508
8653
  });
8509
8654
  import { readFileSync as readFileSync4 } from "fs";
8510
- import { resolve as resolve3 } from "path";
8655
+ import { resolve as resolve4 } from "path";
8511
8656
  function usageError(message) {
8512
8657
  process.stderr.write(`kobe repo: ${message}
8513
8658
 
@@ -8517,7 +8662,7 @@ ${REPO_USAGE}
8517
8662
  }
8518
8663
  function readArgFile(path11) {
8519
8664
  try {
8520
- return readFileSync4(resolve3(process.cwd(), path11), "utf8");
8665
+ return readFileSync4(resolve4(process.cwd(), path11), "utf8");
8521
8666
  } catch (err) {
8522
8667
  usageError(`cannot read ${path11}: ${err instanceof Error ? err.message : String(err)}`);
8523
8668
  }
@@ -8580,13 +8725,13 @@ async function runRepoSubcommand(args) {
8580
8725
  }
8581
8726
  const { getRepoInitOverride: getRepoInitOverride2, setRepoInitOverride: setRepoInitOverride2, resolveRepoRoot: resolveRepoRoot2 } = await Promise.resolve().then(() => (init_repos(), exports_repos));
8582
8727
  const { existsSync: existsSync6 } = await import("fs");
8583
- const { join: join6 } = await import("path");
8728
+ const { join: join7 } = await import("path");
8584
8729
  if (verb === "show") {
8585
8730
  const [pathArg] = rest.filter((a) => !a.startsWith("-"));
8586
- const repo = resolveRepoRoot2(resolve3(process.cwd(), pathArg ?? "."));
8731
+ const repo = resolveRepoRoot2(resolve4(process.cwd(), pathArg ?? "."));
8587
8732
  const override = getRepoInitOverride2(repo);
8588
- const hasFileScript = existsSync6(join6(repo, ".kobe", "init.sh"));
8589
- const hasFilePrompt = existsSync6(join6(repo, ".kobe", "init-prompt.md"));
8733
+ const hasFileScript = existsSync6(join7(repo, ".kobe", "init.sh"));
8734
+ const hasFilePrompt = existsSync6(join7(repo, ".kobe", "init-prompt.md"));
8590
8735
  console.log(`repo: ${repo}`);
8591
8736
  console.log(` .kobe/init.sh: ${hasFileScript ? "present (wins)" : "absent"}`);
8592
8737
  console.log(` .kobe/init-prompt.md: ${hasFilePrompt ? "present (wins)" : "absent"}`);
@@ -8599,7 +8744,7 @@ async function runRepoSubcommand(args) {
8599
8744
  if (flags.initScript === undefined && flags.initPrompt === undefined) {
8600
8745
  usageError("set needs at least one of --init-script(-file) / --init-prompt(-file)");
8601
8746
  }
8602
- const repo = resolveRepoRoot2(resolve3(process.cwd(), flags.path ?? "."));
8747
+ const repo = resolveRepoRoot2(resolve4(process.cwd(), flags.path ?? "."));
8603
8748
  const next = setRepoInitOverride2(repo, {
8604
8749
  ...flags.initScript !== undefined ? { initScript: flags.initScript } : {},
8605
8750
  ...flags.initPrompt !== undefined ? { initPrompt: flags.initPrompt } : {}
@@ -8611,7 +8756,7 @@ async function runRepoSubcommand(args) {
8611
8756
  }
8612
8757
  if (verb === "unset") {
8613
8758
  const { path: path11, clearScript, clearPrompt } = parseUnsetArgs(rest);
8614
- const repo = resolveRepoRoot2(resolve3(process.cwd(), path11 ?? "."));
8759
+ const repo = resolveRepoRoot2(resolve4(process.cwd(), path11 ?? "."));
8615
8760
  const next = setRepoInitOverride2(repo, {
8616
8761
  ...clearScript ? { initScript: "" } : {},
8617
8762
  ...clearPrompt ? { initPrompt: "" } : {}
@@ -8684,15 +8829,30 @@ function parseEngineCommand(command) {
8684
8829
  }
8685
8830
  return out;
8686
8831
  }
8687
- function interactiveEngineCommand(vendor) {
8832
+ function interactiveEngineCommand(vendor, effort) {
8688
8833
  const v = vendor ?? "claude";
8689
8834
  const override = getPersistedString(engineCommandKey(v))?.trim();
8690
- if (override) {
8691
- const argv = parseEngineCommand(override);
8692
- if (argv.length > 0)
8693
- return argv;
8694
- }
8695
- return defaultEngineCommand(v);
8835
+ const base = (() => {
8836
+ if (override) {
8837
+ const argv = parseEngineCommand(override);
8838
+ if (argv.length > 0)
8839
+ return argv;
8840
+ }
8841
+ return defaultEngineCommand(v);
8842
+ })();
8843
+ return withEngineEffort(base, v, effort);
8844
+ }
8845
+ function withEngineEffort(argv, vendor, effort) {
8846
+ const trimmed = effort?.trim();
8847
+ if (!trimmed)
8848
+ return argv;
8849
+ const v = vendor ?? "claude";
8850
+ const levels = engineEntry(v).effortLevels;
8851
+ if (!levels?.includes(trimmed))
8852
+ return argv;
8853
+ if (v === "codex")
8854
+ return [...argv, "-c", `model_reasoning_effort=${trimmed}`];
8855
+ return argv;
8696
8856
  }
8697
8857
  function withClaudeSessionId(argv, vendor) {
8698
8858
  if ((vendor ?? "claude") !== "claude")
@@ -8929,7 +9089,7 @@ async function deliverFirstPrompt(session, prompt) {
8929
9089
  return;
8930
9090
  await pasteAndSubmit(pane, prompt);
8931
9091
  }
8932
- var sleep = (ms) => new Promise((resolve4) => setTimeout(resolve4, ms));
9092
+ var sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
8933
9093
  var init_prompt_delivery = __esm(() => {
8934
9094
  init_client2();
8935
9095
  });
@@ -10837,7 +10997,7 @@ function normalizeHex(value) {
10837
10997
  }
10838
10998
  function resolveThemeSlotHex(theme, slot, mode = "dark") {
10839
10999
  const defs = theme.defs ?? {};
10840
- function resolve4(c, chain) {
11000
+ function resolve5(c, chain) {
10841
11001
  if (typeof c === "string") {
10842
11002
  if (c === "transparent" || c === "none")
10843
11003
  return null;
@@ -10848,17 +11008,17 @@ function resolveThemeSlotHex(theme, slot, mode = "dark") {
10848
11008
  const next = defs[c] ?? theme.theme[c];
10849
11009
  if (next === undefined)
10850
11010
  return null;
10851
- return resolve4(next, [...chain, c]);
11011
+ return resolve5(next, [...chain, c]);
10852
11012
  }
10853
11013
  if (!c || typeof c !== "object")
10854
11014
  return null;
10855
11015
  const variant = c[mode];
10856
- return typeof variant === "string" ? resolve4(variant, chain) : null;
11016
+ return typeof variant === "string" ? resolve5(variant, chain) : null;
10857
11017
  }
10858
11018
  const value = theme.theme[slot];
10859
11019
  if (value === undefined)
10860
11020
  return null;
10861
- return resolve4(value, [slot]);
11021
+ return resolve5(value, [slot]);
10862
11022
  }
10863
11023
 
10864
11024
  // src/tui/context/theme/schema.ts
@@ -10913,9 +11073,9 @@ var init_schema = () => {};
10913
11073
 
10914
11074
  // src/tui/context/theme/loader.ts
10915
11075
  import { readFileSync as readFileSync6, readdirSync } from "fs";
10916
- import { join as join6 } from "path";
11076
+ import { join as join7 } from "path";
10917
11077
  function userThemesDir() {
10918
- return join6(kobeStateDir(), "themes");
11078
+ return join7(kobeStateDir(), "themes");
10919
11079
  }
10920
11080
  function loadUserThemes() {
10921
11081
  const dir = userThemesDir();
@@ -10929,7 +11089,7 @@ function loadUserThemes() {
10929
11089
  for (const file of entries) {
10930
11090
  if (!file.endsWith(".json"))
10931
11091
  continue;
10932
- const path11 = join6(dir, file);
11092
+ const path11 = join7(dir, file);
10933
11093
  let parsed;
10934
11094
  try {
10935
11095
  const text = readFileSync6(path11, "utf8");
@@ -11027,6 +11187,68 @@ var init_tmux_border_theme = __esm(() => {
11027
11187
  FOCUS_ACCENT_SLOT_NAMES2 = ["primary", "success", "info"];
11028
11188
  });
11029
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
+
11030
11252
  // src/tui/panes/terminal/pane-heal.ts
11031
11253
  function parseKobePaneRows(stdout) {
11032
11254
  const rows = [];
@@ -11140,6 +11362,7 @@ async function globalRightColumnResizeArgs() {
11140
11362
  return (await globalLayoutPrefs()).rcArgs;
11141
11363
  }
11142
11364
  async function healWorkspaceLayout(session, versions) {
11365
+ recordGen(session, "resize");
11143
11366
  const { tasksWidth, rcArgs } = await globalLayoutPrefs();
11144
11367
  const rows = await listKobePanes(session);
11145
11368
  if (!rows)
@@ -11177,7 +11400,7 @@ async function captureGlobalLayout(session) {
11177
11400
  "-t",
11178
11401
  `=${session}`,
11179
11402
  "-F",
11180
- "#{@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}"
11181
11404
  ]);
11182
11405
  if (code !== 0)
11183
11406
  return;
@@ -11185,6 +11408,8 @@ async function captureGlobalLayout(session) {
11185
11408
  `).map((line) => line.split("\t")).filter((cols) => (cols[0]?.trim() ?? "") !== "");
11186
11409
  if (rows.length === 0)
11187
11410
  return;
11411
+ if (rows.some((cols) => cols[5]?.trim() === "1"))
11412
+ return;
11188
11413
  const winW = Number.parseInt(rows[0][3]?.trim() ?? "", 10);
11189
11414
  const winH = Number.parseInt(rows[0][4]?.trim() ?? "", 10);
11190
11415
  const sets = [];
@@ -11208,6 +11433,28 @@ async function captureGlobalLayout(session) {
11208
11433
  if (sets.length > 0)
11209
11434
  await runTmuxSequence(sets);
11210
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
+ }
11211
11458
  async function refreshKobeWorkspacePanes(session) {
11212
11459
  const sessionOptions = await getSessionOptions(session, ["@kobe_worktree", "@kobe_task", "@kobe_vendor"]);
11213
11460
  const cwd = sessionOptions["@kobe_worktree"] || process.cwd();
@@ -11234,6 +11481,7 @@ var init_pane_heal = __esm(() => {
11234
11481
  init_tmux_border_theme();
11235
11482
  init_version();
11236
11483
  init_launch();
11484
+ init_layout_coord();
11237
11485
  KOBE_PANE_LIST_FORMAT = `#{window_id} #{pane_id} #{@kobe_role} #{${PANE_VERSION_OPTION}} #{pane_width}`;
11238
11486
  });
11239
11487
 
@@ -11463,6 +11711,7 @@ __export(exports_tmux, {
11463
11711
  selectTasksPane: () => selectTasksPane,
11464
11712
  refreshKobeWorkspacePanes: () => refreshKobeWorkspacePanes,
11465
11713
  quickCreate: () => quickCreate,
11714
+ prepareWindowForSwitch: () => prepareWindowForSwitch,
11466
11715
  prepareWindowForAttach: () => prepareWindowForAttach,
11467
11716
  parseObservedSession: () => parseObservedSession,
11468
11717
  openUpdateTab: () => openUpdateTab,
@@ -11480,6 +11729,7 @@ __export(exports_tmux, {
11480
11729
  chatTabRenameBinding: () => chatTabRenameBinding,
11481
11730
  chatTabCloseBinding: () => chatTabCloseBinding,
11482
11731
  chatTabChooseEngineBindings: () => chatTabChooseEngineBindings,
11732
+ captureGlobalLayoutOnDrag: () => captureGlobalLayoutOnDrag,
11483
11733
  captureGlobalLayout: () => captureGlobalLayout,
11484
11734
  attachArgv: () => attachArgv,
11485
11735
  PANE_VERSION_OPTION: () => PANE_VERSION_OPTION,
@@ -11498,11 +11748,26 @@ function positiveInt(value) {
11498
11748
  return Number.isInteger(n) && n > 0 ? n : undefined;
11499
11749
  }
11500
11750
  async function prepareWindowForAttach(session) {
11751
+ recordGen(session, "resize");
11501
11752
  const sizeArgs = tmuxInitialSizeArgs();
11502
11753
  if (sizeArgs.length > 0)
11503
11754
  await runTmux(["resize-window", "-t", `=${session}`, ...sizeArgs]);
11504
11755
  await healWorkspaceLayout(session);
11505
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
+ }
11506
11771
  function focusBindCommand(key, dir) {
11507
11772
  return [
11508
11773
  "bind-key",
@@ -11634,6 +11899,8 @@ async function ensureSessionImpl(opts) {
11634
11899
  const focusTasksTmuxCommand = `run-shell ${shellQuote(focusTasksCommand)}`;
11635
11900
  const healLayoutCommand = `${envStr}${invStr} heal-layout --session '#{session_name}'`;
11636
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)}`;
11637
11904
  const userKeys = resolveUserTmuxKeys();
11638
11905
  const unbinds = [];
11639
11906
  if (userKeys.overridden.has(TMUX_FOCUS_ID)) {
@@ -11676,6 +11943,7 @@ async function ensureSessionImpl(opts) {
11676
11943
  ],
11677
11944
  ["set-option", "-g", "mouse", "on"],
11678
11945
  ["set-hook", "-g", "window-resized", healLayoutTmuxCommand],
11946
+ ["set-hook", "-g", "window-layout-changed", captureLayoutTmuxCommand],
11679
11947
  ...unbinds,
11680
11948
  ...b["tmux.detach"] ? [
11681
11949
  [
@@ -11726,6 +11994,7 @@ var init_tmux = __esm(() => {
11726
11994
  init_tmux_border_theme();
11727
11995
  init_chattab();
11728
11996
  init_launch();
11997
+ init_layout_coord();
11729
11998
  init_pane_heal();
11730
11999
  init_client2();
11731
12000
  init_chattab();
@@ -11744,17 +12013,17 @@ var exports_repo_init = {};
11744
12013
  __export(exports_repo_init, {
11745
12014
  resolveRepoInit: () => resolveRepoInit
11746
12015
  });
11747
- import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
11748
- import { join as join7 } from "path";
12016
+ import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
12017
+ import { join as join9 } from "path";
11749
12018
  function repoFileScript(worktreePath) {
11750
- return existsSync7(join7(worktreePath, INIT_SCRIPT_REL)) ? `sh ${INIT_SCRIPT_REL}` : undefined;
12019
+ return existsSync7(join9(worktreePath, INIT_SCRIPT_REL)) ? `sh ${INIT_SCRIPT_REL}` : undefined;
11751
12020
  }
11752
12021
  function repoFilePrompt(worktreePath) {
11753
- const p = join7(worktreePath, INIT_PROMPT_REL);
12022
+ const p = join9(worktreePath, INIT_PROMPT_REL);
11754
12023
  if (!existsSync7(p))
11755
12024
  return;
11756
12025
  try {
11757
- const text = readFileSync8(p, "utf8");
12026
+ const text = readFileSync9(p, "utf8");
11758
12027
  return text.trim().length > 0 ? text : undefined;
11759
12028
  } catch {
11760
12029
  return;
@@ -11772,8 +12041,8 @@ function resolveRepoInit(repoRoot, worktreePath) {
11772
12041
  var INIT_SCRIPT_REL, INIT_PROMPT_REL;
11773
12042
  var init_repo_init = __esm(() => {
11774
12043
  init_repos();
11775
- INIT_SCRIPT_REL = join7(".kobe", "init.sh");
11776
- INIT_PROMPT_REL = join7(".kobe", "init-prompt.md");
12044
+ INIT_SCRIPT_REL = join9(".kobe", "init.sh");
12045
+ INIT_PROMPT_REL = join9(".kobe", "init-prompt.md");
11777
12046
  });
11778
12047
 
11779
12048
  // src/cli/api-cmd.ts
@@ -11800,7 +12069,7 @@ __export(exports_api_cmd, {
11800
12069
  API_VERBS: () => API_VERBS,
11801
12070
  API_SCHEMA_VERSION: () => API_SCHEMA_VERSION
11802
12071
  });
11803
- import { resolve as resolve4 } from "path";
12072
+ import { resolve as resolve5 } from "path";
11804
12073
  function groupOf(verbName) {
11805
12074
  for (const [group, names] of Object.entries(VERB_GROUPS)) {
11806
12075
  if (names.includes(verbName))
@@ -11965,10 +12234,10 @@ class VerbArgs {
11965
12234
  }
11966
12235
  path(name) {
11967
12236
  const v = this.str(name);
11968
- return v === undefined ? undefined : resolve4(process.cwd(), v);
12237
+ return v === undefined ? undefined : resolve5(process.cwd(), v);
11969
12238
  }
11970
12239
  requirePath(name) {
11971
- return resolve4(process.cwd(), this.require(name));
12240
+ return resolve5(process.cwd(), this.require(name));
11972
12241
  }
11973
12242
  }
11974
12243
  function parseAgentsSpec(spec) {
@@ -11988,6 +12257,9 @@ function parseAgentsSpec(spec) {
11988
12257
  if (!Number.isInteger(count) || count <= 0) {
11989
12258
  throw new ApiError(`--agents count for "${vendor}" must be a positive integer`, "BAD_FLAG");
11990
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
+ }
11991
12263
  for (let i = 0;i < count; i++)
11992
12264
  out.push(vendor);
11993
12265
  }
@@ -12154,10 +12426,22 @@ function daemonOf(ctx) {
12154
12426
  async function simpleRpc(ctx, name, payload) {
12155
12427
  return daemonOf(ctx).request(name, payload);
12156
12428
  }
12429
+ async function issueUpdate(ctx) {
12430
+ const title = ctx.args.str("title");
12431
+ const body = ctx.args.str("body");
12432
+ if (title === undefined && body === undefined) {
12433
+ throw new ApiError("issue-update requires --title and/or --body", "MISSING_FLAG");
12434
+ }
12435
+ return simpleRpc(ctx, "issue.mutate", {
12436
+ repoRoot: ctx.args.requirePath("repo"),
12437
+ op: { type: "update", id: ctx.args.int("id"), title, body }
12438
+ });
12439
+ }
12157
12440
  async function add(ctx) {
12158
12441
  const daemon = daemonOf(ctx);
12159
- const { args } = ctx;
12160
- const payload = { repo: args.requirePath("repo") };
12442
+ const { args, runtime } = ctx;
12443
+ const repo = await runtime.resolveRepoRoot(args.requirePath("repo"));
12444
+ const payload = { repo };
12161
12445
  const title = args.str("title");
12162
12446
  if (title)
12163
12447
  payload.title = title;
@@ -12167,11 +12451,12 @@ async function add(ctx) {
12167
12451
  const baseRef = args.str("base-branch");
12168
12452
  if (baseRef)
12169
12453
  payload.baseRef = baseRef;
12170
- const vendor = args.vendor();
12454
+ const vendor = args.vendor() ?? await runtime.defaultVendor();
12171
12455
  if (vendor)
12172
12456
  payload.vendor = vendor;
12173
12457
  const res = await daemon.request("task.create", payload);
12174
12458
  const taskId = res.taskId;
12459
+ await daemon.request("task.setActive", { taskId });
12175
12460
  const status = args.enumOf("status");
12176
12461
  if (status)
12177
12462
  await daemon.request("task.status", { taskId, status });
@@ -12186,6 +12471,7 @@ async function add(ctx) {
12186
12471
  if (!prompt)
12187
12472
  return { taskId, task, started: false };
12188
12473
  const delivered = await ctx.runtime.deliverPrompt(daemon, { id: taskId, worktreePath: task.worktreePath, vendor: task.vendor, repo: task.repo }, prompt);
12474
+ task = (await daemon.request("task.get", { taskId })).task;
12189
12475
  return { taskId, task, started: delivered.started, engineReady: delivered.engineReady, session: delivered.session };
12190
12476
  }
12191
12477
  async function send(ctx) {
@@ -12281,13 +12567,14 @@ async function adopt(ctx) {
12281
12567
  }
12282
12568
  async function fanOut(ctx) {
12283
12569
  const daemon = daemonOf(ctx);
12284
- const { args } = ctx;
12285
- const repo = args.requirePath("repo");
12570
+ const { args, runtime } = ctx;
12571
+ const repo = await runtime.resolveRepoRoot(args.requirePath("repo"));
12286
12572
  const prompt = args.require("prompt");
12287
12573
  const title = args.str("title");
12288
12574
  const baseRef = args.str("base-branch");
12289
12575
  const agentsSpec = args.str("agents");
12290
- const plan = agentsSpec ? parseAgentsSpec(agentsSpec) : new Array(args.int("count") ?? 1).fill(args.vendor() ?? "claude");
12576
+ const defaultVendor = await runtime.defaultVendor();
12577
+ const plan = agentsSpec ? parseAgentsSpec(agentsSpec) : new Array(args.int("count") ?? 1).fill(args.vendor() ?? defaultVendor ?? "claude");
12291
12578
  if (plan.length > FANOUT_CAP) {
12292
12579
  throw new ApiError(`fan-out of ${plan.length} exceeds the cap of ${FANOUT_CAP} \u2014 spawn in batches`, "BAD_FLAG");
12293
12580
  }
@@ -12422,7 +12709,7 @@ ${apiUsage()}`, "BAD_VERB", 2);
12422
12709
  session?.close();
12423
12710
  }
12424
12711
  }
12425
- var API_SCHEMA_VERSION = 2, FANOUT_CAP = 10, TASK_STATUSES, 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;
12426
12713
  var init_api_cmd = __esm(() => {
12427
12714
  init_interactive_command();
12428
12715
  init_feedback();
@@ -12431,7 +12718,8 @@ var init_api_cmd = __esm(() => {
12431
12718
  init_vendor();
12432
12719
  init_version();
12433
12720
  init_daemon_session();
12434
- TASK_STATUSES = ["backlog", "in_progress", "in_review", "done", "canceled", "error"];
12721
+ TASK_STATUSES2 = ["backlog", "in_progress", "in_review", "done", "canceled", "error"];
12722
+ ISSUE_STATUSES2 = ["open", "doing", "hold", "done"];
12435
12723
  ApiError = class ApiError extends Error {
12436
12724
  code;
12437
12725
  constructor(message, code) {
@@ -12477,6 +12765,7 @@ var init_api_cmd = __esm(() => {
12477
12765
  create: ["add", "fan-out"],
12478
12766
  drive: ["send", "dispatch", "note", "set-active"],
12479
12767
  edit: ["rename", "set-branch", "set-vendor", "set-status"],
12768
+ issues: ["issue-list", "issue-create", "issue-set-status", "issue-update"],
12480
12769
  lifecycle: ["archive", "pin", "delete"],
12481
12770
  worktree: ["ensure-worktree", "adopt", "discover-adoptable"],
12482
12771
  feedback: ["feedback"]
@@ -12521,7 +12810,7 @@ var init_api_cmd = __esm(() => {
12521
12810
  {
12522
12811
  name: "status",
12523
12812
  type: "enum",
12524
- values: TASK_STATUSES,
12813
+ values: TASK_STATUSES2,
12525
12814
  default: "backlog",
12526
12815
  description: "Initial lifecycle status."
12527
12816
  },
@@ -12590,8 +12879,51 @@ var init_api_cmd = __esm(() => {
12590
12879
  description: "Discussion category slug."
12591
12880
  }
12592
12881
  ],
12593
- offline: true,
12594
- handler: feedback
12882
+ offline: true,
12883
+ handler: feedback
12884
+ },
12885
+ {
12886
+ name: "issue-list",
12887
+ summary: "List daemon-owned issues for a repo.",
12888
+ flags: [F.repo()],
12889
+ handler: (ctx) => simpleRpc(ctx, "issue.list", { repoRoot: ctx.args.requirePath("repo") })
12890
+ },
12891
+ {
12892
+ name: "issue-create",
12893
+ summary: "Create a daemon-owned issue for a repo.",
12894
+ flags: [
12895
+ F.repo(),
12896
+ { name: "title", type: "string", required: true, placeholder: "T", description: "Issue title." },
12897
+ { name: "body", type: "string", placeholder: "TEXT", description: "Issue body." }
12898
+ ],
12899
+ handler: (ctx) => simpleRpc(ctx, "issue.mutate", {
12900
+ repoRoot: ctx.args.requirePath("repo"),
12901
+ op: { type: "create", title: ctx.args.require("title"), body: ctx.args.str("body") }
12902
+ })
12903
+ },
12904
+ {
12905
+ name: "issue-set-status",
12906
+ summary: "Set a daemon-owned issue's status.",
12907
+ flags: [
12908
+ F.repo(),
12909
+ { name: "id", type: "int", required: true, placeholder: "N", description: "Issue id." },
12910
+ { name: "status", type: "enum", required: true, values: ISSUE_STATUSES2, description: "New issue status." }
12911
+ ],
12912
+ handler: (ctx) => simpleRpc(ctx, "issue.mutate", {
12913
+ repoRoot: ctx.args.requirePath("repo"),
12914
+ op: { type: "setStatus", id: ctx.args.int("id"), status: ctx.args.requireEnum("status") }
12915
+ })
12916
+ },
12917
+ {
12918
+ name: "issue-update",
12919
+ summary: "Update a daemon-owned issue's title and/or body.",
12920
+ flags: [
12921
+ F.repo(),
12922
+ { name: "id", type: "int", required: true, placeholder: "N", description: "Issue id." },
12923
+ { name: "title", type: "string", placeholder: "T", description: "New title." },
12924
+ { name: "body", type: "string", placeholder: "TEXT", description: "New body." }
12925
+ ],
12926
+ handler: issueUpdate
12595
12927
  },
12596
12928
  {
12597
12929
  name: "collect",
@@ -12631,7 +12963,7 @@ var init_api_cmd = __esm(() => {
12631
12963
  summary: "Set a task's lifecycle status.",
12632
12964
  flags: [
12633
12965
  F.taskId(),
12634
- { 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." }
12635
12967
  ],
12636
12968
  handler: (ctx) => simpleRpc(ctx, "task.status", {
12637
12969
  taskId: ctx.args.require("task-id"),
@@ -12718,7 +13050,12 @@ var init_api_cmd = __esm(() => {
12718
13050
  defaultApiRuntime = {
12719
13051
  isTaskRunning: (taskId) => sessionExists(tmuxSessionName(taskId)),
12720
13052
  deliverPrompt: (client, target, prompt) => deliverPrompt(client, target, prompt),
12721
- resolveRepoRoot: async (absPath) => (await Promise.resolve().then(() => (init_repos(), exports_repos))).resolveRepoRoot(absPath),
13053
+ resolveRepoRoot: async (absPath) => (await Promise.resolve().then(() => (init_repos(), exports_repos))).resolveMainRepoRoot(absPath),
13054
+ defaultVendor: async () => {
13055
+ const { getPersistedString: getPersistedString2 } = await Promise.resolve().then(() => (init_repos(), exports_repos));
13056
+ const value = getPersistedString2("lastSelectedVendor")?.trim();
13057
+ return value ? value : undefined;
13058
+ },
12722
13059
  readWorktreeChanges: async (worktreePath) => (await Promise.resolve().then(() => (init_worktree_changes(), exports_worktree_changes))).readWorktreeChanges(worktreePath),
12723
13060
  tearDownSession: async (taskId) => {
12724
13061
  const session = tmuxSessionName(taskId);
@@ -12819,8 +13156,8 @@ var exports_theme = {};
12819
13156
  __export(exports_theme, {
12820
13157
  runThemeSubcommand: () => runThemeSubcommand
12821
13158
  });
12822
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync9, readdirSync as readdirSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
12823
- import { basename as basename5, join as join8, resolve as resolve5 } 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";
12824
13161
  function fail3(message) {
12825
13162
  process.stderr.write(`kobe theme: ${message}
12826
13163
  `);
@@ -12851,7 +13188,7 @@ function listThemes() {
12851
13188
  } else {
12852
13189
  for (const f of userFiles) {
12853
13190
  const name = f.slice(0, -".json".length);
12854
- const path11 = join8(dir, f);
13191
+ const path11 = join10(dir, f);
12855
13192
  const overridesBundled = BUNDLED_NAMES.includes(name) ? " (overrides built-in)" : "";
12856
13193
  lines.push(` ${name}${overridesBundled} ${path11}`);
12857
13194
  }
@@ -12877,10 +13214,10 @@ async function readSource(source) {
12877
13214
  const defaultName2 = file2.endsWith(".json") ? file2.slice(0, -".json".length) : file2;
12878
13215
  return { text: text2, defaultName: defaultName2 };
12879
13216
  }
12880
- const abs = resolve5(process.cwd(), source);
13217
+ const abs = resolve6(process.cwd(), source);
12881
13218
  let text;
12882
13219
  try {
12883
- text = readFileSync9(abs, "utf8");
13220
+ text = readFileSync10(abs, "utf8");
12884
13221
  } catch (err) {
12885
13222
  fail3(`failed to read ${abs}: ${err instanceof Error ? err.message : String(err)}`);
12886
13223
  }
@@ -12940,12 +13277,12 @@ async function addTheme(args) {
12940
13277
  fail3(`invalid theme name "${name}" (use letters, digits, '.', '_', '-')`);
12941
13278
  }
12942
13279
  const dir = userThemesDir();
12943
- mkdirSync5(dir, { recursive: true });
12944
- const dest = join8(dir, `${name}.json`);
13280
+ mkdirSync6(dir, { recursive: true });
13281
+ const dest = join10(dir, `${name}.json`);
12945
13282
  if (existsSync8(dest) && !opts.force) {
12946
13283
  fail3(`${dest} already exists (pass --force to overwrite)`);
12947
13284
  }
12948
- writeFileSync2(dest, `${JSON.stringify(result.theme, null, 2)}
13285
+ writeFileSync3(dest, `${JSON.stringify(result.theme, null, 2)}
12949
13286
  `, "utf8");
12950
13287
  process.stdout.write(`installed theme "${name}" -> ${dest}
12951
13288
  `);
@@ -12959,7 +13296,7 @@ function removeTheme(args) {
12959
13296
  if (BUNDLED_NAMES.includes(name)) {
12960
13297
  fail3(`"${name}" is a built-in theme and cannot be removed`);
12961
13298
  }
12962
- const dest = join8(userThemesDir(), `${name}.json`);
13299
+ const dest = join10(userThemesDir(), `${name}.json`);
12963
13300
  if (!existsSync8(dest)) {
12964
13301
  fail3(`no user theme named "${name}" (looked for ${dest})`);
12965
13302
  }
@@ -13029,7 +13366,7 @@ __export(exports_feedback_cmd, {
13029
13366
  runFeedbackSubcommand: () => runFeedbackSubcommand,
13030
13367
  parseFeedbackArgs: () => parseFeedbackArgs
13031
13368
  });
13032
- import { readFileSync as readFileSync10 } from "fs";
13369
+ import { readFileSync as readFileSync11 } from "fs";
13033
13370
  function usageError2(message) {
13034
13371
  process.stderr.write(`kobe feedback: ${message}
13035
13372
 
@@ -13039,8 +13376,8 @@ ${FEEDBACK_USAGE}
13039
13376
  }
13040
13377
  function readBodyFile(path11) {
13041
13378
  if (path11 === "-")
13042
- return readFileSync10(0, "utf8");
13043
- return readFileSync10(path11, "utf8");
13379
+ return readFileSync11(0, "utf8");
13380
+ return readFileSync11(path11, "utf8");
13044
13381
  }
13045
13382
  function parseFeedbackArgs(args) {
13046
13383
  const parsed = { help: false };
@@ -13120,9 +13457,9 @@ var init_feedback_cmd = __esm(() => {
13120
13457
  });
13121
13458
 
13122
13459
  // src/core/index.ts
13123
- import { homedir as homedir15 } from "os";
13460
+ import { homedir as homedir16 } from "os";
13124
13461
  async function createKobeCore(options = {}) {
13125
- const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir15();
13462
+ const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir16();
13126
13463
  const store = new TaskIndexStore({ homeDir: homeDir2 });
13127
13464
  await store.load();
13128
13465
  const worktrees = new GitWorktreeManager;
@@ -13191,6 +13528,8 @@ async function runDaemonSubcommand(argv) {
13191
13528
  try {
13192
13529
  await client.request("daemon.stop");
13193
13530
  console.log("kobe daemon: stop requested");
13531
+ } catch {
13532
+ console.log(`kobe daemon: no daemon running at ${socketPath}`);
13194
13533
  } finally {
13195
13534
  client.close();
13196
13535
  }
@@ -13239,9 +13578,9 @@ var init_daemon_cmd = __esm(() => {
13239
13578
  });
13240
13579
 
13241
13580
  // src/lib/skill-install.ts
13242
- import { existsSync as existsSync9, readFileSync as readFileSync11 } from "fs";
13243
- import { homedir as homedir16 } from "os";
13244
- import { join as join9 } from "path";
13581
+ import { existsSync as existsSync9, readFileSync as readFileSync12 } from "fs";
13582
+ import { homedir as homedir17 } from "os";
13583
+ import { join as join11 } from "path";
13245
13584
  function npxSkillsArgv(opts = {}) {
13246
13585
  return ["skills", "add", SKILL_SOURCE_SLUG, "--skill", "kobe", "--agent", opts.agent ?? DEFAULT_SKILL_AGENT];
13247
13586
  }
@@ -13249,9 +13588,9 @@ function npxSkillsCommand(opts = {}) {
13249
13588
  return `npx ${npxSkillsArgv(opts).join(" ")}`;
13250
13589
  }
13251
13590
  function kobeSkillPaths(opts = {}) {
13252
- const home = opts.home ?? homedir16();
13591
+ const home = opts.home ?? homedir17();
13253
13592
  const cwd = opts.cwd ?? process.cwd();
13254
- return [join9(home, SKILL_REL_PATH), join9(cwd, SKILL_REL_PATH)];
13593
+ return [join11(home, SKILL_REL_PATH), join11(cwd, SKILL_REL_PATH)];
13255
13594
  }
13256
13595
  function parseSkillVersion(content) {
13257
13596
  const m = content.match(/kobe-skill-version:\s*(\d+)/);
@@ -13264,7 +13603,7 @@ function kobeSkillState(opts) {
13264
13603
  }
13265
13604
  let installedVersion = null;
13266
13605
  try {
13267
- installedVersion = parseSkillVersion(readFileSync11(path11, "utf8"));
13606
+ installedVersion = parseSkillVersion(readFileSync12(path11, "utf8"));
13268
13607
  } catch {
13269
13608
  installedVersion = null;
13270
13609
  }
@@ -13298,7 +13637,7 @@ kobe: your kobe agent skill is out of date (${was}; this kobe wants v${state.cur
13298
13637
  `);
13299
13638
  }
13300
13639
  }
13301
- var KOBE_SKILL_VERSION = 1, SKILL_REL_PATH = ".claude/skills/kobe/SKILL.md", SKILL_INSTALL_COMMAND = "kobe skill install", SKILL_SOURCE_SLUG = "Sma1lboy/kobe", DEFAULT_SKILL_AGENT = "claude-code", HINT_SEEN_KEY = "skillHintSeen";
13640
+ var KOBE_SKILL_VERSION = 2, SKILL_REL_PATH = ".claude/skills/kobe/SKILL.md", SKILL_INSTALL_COMMAND = "kobe skill install", SKILL_SOURCE_SLUG = "Sma1lboy/kobe", DEFAULT_SKILL_AGENT = "claude-code", HINT_SEEN_KEY = "skillHintSeen";
13302
13641
  var init_skill_install = __esm(() => {
13303
13642
  init_repos();
13304
13643
  });
@@ -13310,9 +13649,9 @@ __export(exports_maintenance, {
13310
13649
  runReloadSubcommand: () => runReloadSubcommand,
13311
13650
  runDoctorSubcommand: () => runDoctorSubcommand
13312
13651
  });
13313
- 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";
13314
13653
  import { unlink as unlink6 } from "fs/promises";
13315
- import { join as join10 } from "path";
13654
+ import { join as join12 } from "path";
13316
13655
  import { createInterface as createInterface2 } from "readline";
13317
13656
  function isProcessAlive2(pid) {
13318
13657
  try {
@@ -13359,7 +13698,7 @@ function describeFile(path11) {
13359
13698
  }
13360
13699
  function taskCount(tasksPath) {
13361
13700
  try {
13362
- const parsed = JSON.parse(readFileSync12(tasksPath, "utf8"));
13701
+ const parsed = JSON.parse(readFileSync13(tasksPath, "utf8"));
13363
13702
  return Array.isArray(parsed.tasks) ? parsed.tasks.length : null;
13364
13703
  } catch {
13365
13704
  return null;
@@ -13367,7 +13706,7 @@ function taskCount(tasksPath) {
13367
13706
  }
13368
13707
  function tailFile(path11, n) {
13369
13708
  try {
13370
- const lines = readFileSync12(path11, "utf8").split(`
13709
+ const lines = readFileSync13(path11, "utf8").split(`
13371
13710
  `).filter((l) => l.trim().length > 0);
13372
13711
  return lines.slice(-n).join(`
13373
13712
  `);
@@ -13409,7 +13748,7 @@ async function runDoctorSubcommand(argv = []) {
13409
13748
  const socketPath = defaultDaemonSocketPath();
13410
13749
  const pidPath = defaultDaemonPidPath();
13411
13750
  const logPath = defaultDaemonLogPath();
13412
- const tasksPath = join10(kobeStateDir(), "tasks.json");
13751
+ const tasksPath = join12(kobeStateDir(), "tasks.json");
13413
13752
  const statePath2 = kvStatePath();
13414
13753
  const out = ["kobe doctor", ` home: ${homeDir()}`, ` socket: ${socketPath}`, ""];
13415
13754
  const status = await probeDaemonStatus(socketPath);
@@ -13476,7 +13815,7 @@ async function runDoctorSubcommand(argv = []) {
13476
13815
  async function confirmTty(prompt) {
13477
13816
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
13478
13817
  try {
13479
- const answer = await new Promise((resolve6) => rl.question(prompt, resolve6));
13818
+ const answer = await new Promise((resolve7) => rl.question(prompt, resolve7));
13480
13819
  return /^y(es)?$/i.test(answer.trim());
13481
13820
  } finally {
13482
13821
  rl.close();
@@ -13528,7 +13867,7 @@ async function runResetSubcommand(argv) {
13528
13867
  const yes = argv.includes("--yes") || argv.includes("-y");
13529
13868
  const socketPath = defaultDaemonSocketPath();
13530
13869
  const pidPath = defaultDaemonPidPath();
13531
- const tasksPath = join10(kobeStateDir(), "tasks.json");
13870
+ const tasksPath = join12(kobeStateDir(), "tasks.json");
13532
13871
  const statePath2 = kvStatePath();
13533
13872
  console.log("kobe reset will:");
13534
13873
  console.log(" \u2022 stop the kobe daemon (graceful \u2192 SIGTERM \u2192 SIGKILL)");
@@ -13632,6 +13971,22 @@ var init_maintenance = __esm(() => {
13632
13971
  init_version();
13633
13972
  });
13634
13973
 
13974
+ // src/tui/lib/editor-prefs.ts
13975
+ function normalizeEditorKind(value) {
13976
+ return EDITOR_KINDS.includes(value) ? value : DEFAULT_EDITOR_KIND;
13977
+ }
13978
+ var EDITOR_KINDS, AUTO_EDITOR_CANDIDATES, EDITOR_KIND_KEY = "editor.kind", EDITOR_CUSTOM_KEY = "editor.customCommand", DEFAULT_EDITOR_KIND = "auto";
13979
+ var init_editor_prefs = __esm(() => {
13980
+ EDITOR_KINDS = ["auto", "vim", "nvim", "nano", "emacs", "custom"];
13981
+ AUTO_EDITOR_CANDIDATES = ["nvim", "vim", "emacs", "nano"];
13982
+ });
13983
+
13984
+ // src/tui/lib/settings-surface.ts
13985
+ function normalizeSettingsSurface(value) {
13986
+ return value === "taskpanel" ? "taskpanel" : "chattab";
13987
+ }
13988
+ var SETTINGS_SURFACE_KEY = "settings.surface", DEFAULT_SETTINGS_SURFACE = "chattab";
13989
+
13635
13990
  // src/web/diff.ts
13636
13991
  async function runGit(cwd, args) {
13637
13992
  try {
@@ -13748,8 +14103,8 @@ async function handleDiffRequest(req, url) {
13748
14103
  return Response.json({ error: "worktreePath must be an absolute path" }, { status: 400 });
13749
14104
  }
13750
14105
  try {
13751
- const stat4 = await Bun.file(worktreePath).stat();
13752
- if (!stat4.isDirectory()) {
14106
+ const stat5 = await Bun.file(worktreePath).stat();
14107
+ if (!stat5.isDirectory()) {
13753
14108
  return Response.json({ error: "worktreePath is not a directory" }, { status: 400 });
13754
14109
  }
13755
14110
  } catch {
@@ -13817,7 +14172,7 @@ async function handleDiffRequest(req, url) {
13817
14172
  var GIT_TIMEOUT_MS = 15000, UNTRACKED_DIFF_CONCURRENCY = 8;
13818
14173
 
13819
14174
  // src/web/history.ts
13820
- import { isAbsolute } from "path";
14175
+ import { isAbsolute as isAbsolute2 } from "path";
13821
14176
  function isSafeVendor(value) {
13822
14177
  return typeof value === "string" && value.length > 0 && /^[A-Za-z0-9_-]+$/.test(value);
13823
14178
  }
@@ -13827,7 +14182,7 @@ function isSafeSessionId(value) {
13827
14182
  async function handleSessions(url) {
13828
14183
  const worktreePath = url.searchParams.get("worktreePath");
13829
14184
  const vendor = url.searchParams.get("vendor") ?? "claude";
13830
- if (!worktreePath || !isAbsolute(worktreePath)) {
14185
+ if (!worktreePath || !isAbsolute2(worktreePath)) {
13831
14186
  return Response.json({ error: "worktreePath must be an absolute path" }, { status: 400 });
13832
14187
  }
13833
14188
  if (!isSafeVendor(vendor)) {
@@ -13873,16 +14228,16 @@ var init_history4 = __esm(() => {
13873
14228
  });
13874
14229
 
13875
14230
  // src/web/notes.ts
13876
- import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
13877
- import { join as join11 } from "path";
14231
+ import { mkdir as mkdir8, readFile as readFile9, writeFile as writeFile6 } from "fs/promises";
14232
+ import { join as join13 } from "path";
13878
14233
  function notesDir() {
13879
- return join11(kobeStateDir(), "notes");
14234
+ return join13(kobeStateDir(), "notes");
13880
14235
  }
13881
14236
  function isSafeTaskId(taskId) {
13882
14237
  return typeof taskId === "string" && taskId.length > 0 && /^[A-Za-z0-9_-]+$/.test(taskId);
13883
14238
  }
13884
14239
  function noteFilePath(taskId) {
13885
- return join11(notesDir(), `${taskId}.md`);
14240
+ return join13(notesDir(), `${taskId}.md`);
13886
14241
  }
13887
14242
  async function handleGet(url) {
13888
14243
  const taskId = url.searchParams.get("taskId");
@@ -13892,7 +14247,7 @@ async function handleGet(url) {
13892
14247
  try {
13893
14248
  let markdown = "";
13894
14249
  try {
13895
- markdown = await readFile8(noteFilePath(taskId), "utf8");
14250
+ markdown = await readFile9(noteFilePath(taskId), "utf8");
13896
14251
  } catch (err) {
13897
14252
  if (err.code !== "ENOENT")
13898
14253
  throw err;
@@ -13916,8 +14271,8 @@ async function handlePut(req) {
13916
14271
  return Response.json({ error: "markdown must be a string" }, { status: 400 });
13917
14272
  }
13918
14273
  try {
13919
- await mkdir7(notesDir(), { recursive: true });
13920
- await writeFile5(noteFilePath(body.taskId), body.markdown, "utf8");
14274
+ await mkdir8(notesDir(), { recursive: true });
14275
+ await writeFile6(noteFilePath(body.taskId), body.markdown, "utf8");
13921
14276
  return Response.json({ ok: true });
13922
14277
  } catch (err) {
13923
14278
  return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 });
@@ -14020,17 +14375,37 @@ var init_themes = __esm(() => {
14020
14375
  WEB_THEMES = Object.fromEntries(Object.entries(THEME_JSONS).map(([name, json]) => [name, toWebPalette(json)]).filter((entry) => entry[1] !== null));
14021
14376
  });
14022
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
+
14023
14398
  // ../kobe-web/server/spa-channels.ts
14024
14399
  var SPA_CHANNELS, SPA_CHANNEL_SET;
14025
14400
  var init_spa_channels = __esm(() => {
14026
14401
  SPA_CHANNELS = [
14027
14402
  "task.snapshot",
14403
+ "issue.snapshot",
14028
14404
  "active-task",
14029
14405
  "engine-state",
14030
14406
  "update",
14031
14407
  "task.jobs",
14032
14408
  "worktree.changes",
14033
- "task.conflicts",
14034
14409
  "session.deliver",
14035
14410
  "ui-prefs"
14036
14411
  ];
@@ -14039,7 +14414,7 @@ var init_spa_channels = __esm(() => {
14039
14414
 
14040
14415
  // ../kobe-web/server/daemon-link.ts
14041
14416
  function sleep2(ms) {
14042
- return new Promise((resolve6) => setTimeout(resolve6, ms));
14417
+ return new Promise((resolve7) => setTimeout(resolve7, ms));
14043
14418
  }
14044
14419
 
14045
14420
  class DaemonLink {
@@ -14055,7 +14430,7 @@ class DaemonLink {
14055
14430
  update = null;
14056
14431
  jobs = {};
14057
14432
  worktreeChanges = {};
14058
- conflicts = [];
14433
+ issueSnapshots = {};
14059
14434
  deliver = null;
14060
14435
  uiPrefs = null;
14061
14436
  async start() {
@@ -14069,7 +14444,7 @@ class DaemonLink {
14069
14444
  update: this.update,
14070
14445
  jobs: this.jobs,
14071
14446
  worktreeChanges: this.worktreeChanges,
14072
- conflicts: this.conflicts,
14447
+ issueSnapshots: this.issueSnapshots,
14073
14448
  deliver: this.deliver,
14074
14449
  uiPrefs: this.uiPrefs,
14075
14450
  connected: this.connected
@@ -14181,9 +14556,15 @@ class DaemonLink {
14181
14556
  case "worktree.changes":
14182
14557
  this.worktreeChanges = payload.changes;
14183
14558
  break;
14184
- case "task.conflicts":
14185
- this.conflicts = payload.pairs;
14559
+ case "issue.snapshot": {
14560
+ const state = payload;
14561
+ const next = { ...this.issueSnapshots };
14562
+ for (const alias of repoSnapshotAliases(this.tasks, state.repoRoot)) {
14563
+ next[alias] = { ...state, repoRoot: alias };
14564
+ }
14565
+ this.issueSnapshots = next;
14186
14566
  break;
14567
+ }
14187
14568
  case "session.deliver":
14188
14569
  this.deliver = payload;
14189
14570
  break;
@@ -14218,6 +14599,169 @@ var init_daemon_link = __esm(() => {
14218
14599
  init_spa_channels();
14219
14600
  });
14220
14601
 
14602
+ // ../kobe-web/server/issue-assets-route.ts
14603
+ import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
14604
+ import { mkdir as mkdir9 } from "fs/promises";
14605
+ import { join as join14, resolve as resolve7 } from "path";
14606
+ function repoHashOf(repoRoot) {
14607
+ return createHash6("sha1").update(repoRoot).digest("hex").slice(0, 16);
14608
+ }
14609
+ async function handlePost(req) {
14610
+ const declared = Number.parseInt(req.headers.get("content-length") ?? "", 10);
14611
+ if (Number.isFinite(declared) && declared > MAX_ASSET_BYTES) {
14612
+ return Response.json({ error: "asset too large" }, { status: 413 });
14613
+ }
14614
+ let form;
14615
+ try {
14616
+ form = await req.formData();
14617
+ } catch {
14618
+ return Response.json({ error: "invalid form data" }, { status: 400 });
14619
+ }
14620
+ const repoRoot = form.get("repoRoot");
14621
+ if (typeof repoRoot !== "string" || repoRoot.length === 0) {
14622
+ return Response.json({ error: "missing repoRoot" }, { status: 400 });
14623
+ }
14624
+ const file = form.get("file");
14625
+ if (!(file instanceof File)) {
14626
+ return Response.json({ error: "file must be a File" }, { status: 400 });
14627
+ }
14628
+ if (file.size > MAX_ASSET_BYTES) {
14629
+ return Response.json({ error: "asset too large" }, { status: 413 });
14630
+ }
14631
+ const ext = CONTENT_TYPE_EXT[file.type];
14632
+ if (!ext) {
14633
+ return Response.json({ error: `unsupported content-type: ${file.type || "unknown"}` }, { status: 415 });
14634
+ }
14635
+ try {
14636
+ const repoHash = repoHashOf(repoRoot);
14637
+ const dir = join14(issueAssetsDir(), repoHash);
14638
+ await mkdir9(dir, { recursive: true });
14639
+ const assetId = randomUUID3();
14640
+ const name = `${assetId}.${ext}`;
14641
+ await Bun.write(join14(dir, name), file);
14642
+ return Response.json({ url: `${ASSETS_ROUTE}/${repoHash}/${name}` });
14643
+ } catch (err) {
14644
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 });
14645
+ }
14646
+ }
14647
+ async function handleGet2(pathname) {
14648
+ const rest = pathname.slice(ASSETS_ROUTE.length + 1);
14649
+ const slash = rest.indexOf("/");
14650
+ if (slash < 0)
14651
+ return Response.json({ error: "not found" }, { status: 404 });
14652
+ const repoHash = rest.slice(0, slash);
14653
+ const fileSeg = rest.slice(slash + 1);
14654
+ if (!REPO_HASH_RE.test(repoHash) || !ASSET_FILE_RE.test(fileSeg)) {
14655
+ return Response.json({ error: "invalid asset path" }, { status: 400 });
14656
+ }
14657
+ const root = issueAssetsDir();
14658
+ const resolved = resolve7(root, repoHash, fileSeg);
14659
+ if (resolved !== join14(root, repoHash, fileSeg) || !resolved.startsWith(`${root}/`)) {
14660
+ return Response.json({ error: "invalid asset path" }, { status: 400 });
14661
+ }
14662
+ const ext = fileSeg.slice(fileSeg.lastIndexOf(".") + 1).toLowerCase();
14663
+ const contentType = EXT_CONTENT_TYPE[ext];
14664
+ if (!contentType)
14665
+ return Response.json({ error: "invalid asset path" }, { status: 400 });
14666
+ const file = Bun.file(resolved);
14667
+ if (!await file.exists())
14668
+ return Response.json({ error: "not found" }, { status: 404 });
14669
+ return new Response(file, {
14670
+ headers: {
14671
+ "content-type": contentType,
14672
+ "cache-control": "public, max-age=31536000, immutable",
14673
+ "x-content-type-options": "nosniff"
14674
+ }
14675
+ });
14676
+ }
14677
+ async function handleIssueAssetsRequest(req, url) {
14678
+ if (url.pathname === ASSETS_ROUTE) {
14679
+ if (req.method === "POST")
14680
+ return handlePost(req);
14681
+ return Response.json({ error: "method not allowed" }, { status: 405 });
14682
+ }
14683
+ if (url.pathname.startsWith(`${ASSETS_ROUTE}/`)) {
14684
+ if (req.method === "GET")
14685
+ return handleGet2(url.pathname);
14686
+ return Response.json({ error: "method not allowed" }, { status: 405 });
14687
+ }
14688
+ return null;
14689
+ }
14690
+ var ASSETS_ROUTE = "/api/issue-assets", MAX_ASSET_BYTES, CONTENT_TYPE_EXT, EXT_CONTENT_TYPE, REPO_HASH_RE, ASSET_FILE_RE;
14691
+ var init_issue_assets_route = __esm(() => {
14692
+ init_env();
14693
+ MAX_ASSET_BYTES = 10 * 1024 * 1024;
14694
+ CONTENT_TYPE_EXT = {
14695
+ "image/png": "png",
14696
+ "image/jpeg": "jpg",
14697
+ "image/gif": "gif",
14698
+ "image/webp": "webp"
14699
+ };
14700
+ EXT_CONTENT_TYPE = {
14701
+ png: "image/png",
14702
+ jpg: "image/jpeg",
14703
+ jpeg: "image/jpeg",
14704
+ gif: "image/gif",
14705
+ webp: "image/webp"
14706
+ };
14707
+ REPO_HASH_RE = /^[a-f0-9]{16}$/;
14708
+ ASSET_FILE_RE = /^[A-Za-z0-9_-]+\.[a-z0-9]+$/;
14709
+ });
14710
+
14711
+ // ../kobe-web/server/issues-route.ts
14712
+ function errorResponse(err, status = 500) {
14713
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status });
14714
+ }
14715
+ function statusForIssueError(err) {
14716
+ const message = err instanceof Error ? err.message : String(err);
14717
+ if (/^no issue #\d+$/.test(message))
14718
+ return 404;
14719
+ if (message === "repoRoot is required" || message === "repoRoot does not exist" || message === "repoRoot is not a git repository" || message === "missing op" || message === "create requires a non-empty title" || message === "body must be a string" || message === "setStatus requires a numeric id" || message.startsWith("invalid status:") || message === "update requires a numeric id" || message === "title must be a non-empty string" || message === "link requires a numeric id" || message === "link requires a non-empty taskId" || message === "unlink requires a numeric id" || message === "delete requires a numeric id" || message.startsWith("unknown op type:")) {
14720
+ return 400;
14721
+ }
14722
+ return 500;
14723
+ }
14724
+ async function handleGet3(link, url) {
14725
+ const repoRoot = url.searchParams.get("repoRoot");
14726
+ if (!repoRoot)
14727
+ return Response.json({ error: "missing repoRoot" }, { status: 400 });
14728
+ try {
14729
+ return Response.json(await link.request("issue.list", { repoRoot }));
14730
+ } catch (err) {
14731
+ return errorResponse(err, statusForIssueError(err));
14732
+ }
14733
+ }
14734
+ async function handlePost2(link, req) {
14735
+ let parsed;
14736
+ try {
14737
+ parsed = await req.json();
14738
+ } catch {
14739
+ return Response.json({ error: "invalid JSON body" }, { status: 400 });
14740
+ }
14741
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
14742
+ return Response.json({ error: "body must be a JSON object" }, { status: 400 });
14743
+ }
14744
+ const body = parsed;
14745
+ if (typeof body.repoRoot !== "string" || body.repoRoot.length === 0) {
14746
+ return Response.json({ error: "missing repoRoot" }, { status: 400 });
14747
+ }
14748
+ try {
14749
+ return Response.json(await link.request("issue.mutate", { repoRoot: body.repoRoot, op: body.op }));
14750
+ } catch (err) {
14751
+ return errorResponse(err, statusForIssueError(err));
14752
+ }
14753
+ }
14754
+ async function handleIssuesRequest(req, url, link) {
14755
+ if (url.pathname !== ISSUES_ROUTE)
14756
+ return null;
14757
+ if (req.method === "GET")
14758
+ return handleGet3(link, url);
14759
+ if (req.method === "POST")
14760
+ return handlePost2(link, req);
14761
+ return Response.json({ error: "method not allowed" }, { status: 405 });
14762
+ }
14763
+ var ISSUES_ROUTE = "/api/issues";
14764
+
14221
14765
  // ../kobe-web/server/rpc-allowlist.ts
14222
14766
  var WEB_RPC_ALLOWLIST, WEB_RPC_ALLOWSET;
14223
14767
  var init_rpc_allowlist = __esm(() => {
@@ -14266,7 +14810,7 @@ async function ensureTaskSession(link, taskId) {
14266
14810
  const ok = await ensureSession({
14267
14811
  name: session,
14268
14812
  cwd: worktreePath,
14269
- command: interactiveEngineCommand(task.vendor),
14813
+ command: interactiveEngineCommand(task.vendor, task.modelEffort),
14270
14814
  taskId,
14271
14815
  vendor: task.vendor,
14272
14816
  initScript: init.initScript
@@ -14284,7 +14828,7 @@ async function engineSpec(link, taskId) {
14284
14828
  const protocolTaskId = task.kind === "main" ? undefined : taskId;
14285
14829
  const dispatcherTaskId = task.kind === "main" ? taskId : undefined;
14286
14830
  const argv = [
14287
- ...withDispatcherProtocol(withWorktreeProtocol(interactiveEngineCommand(task.vendor), task.vendor, protocolTaskId), task.vendor, dispatcherTaskId)
14831
+ ...withDispatcherProtocol(withWorktreeProtocol(interactiveEngineCommand(task.vendor, task.modelEffort), task.vendor, protocolTaskId), task.vendor, dispatcherTaskId)
14288
14832
  ];
14289
14833
  const init = resolveRepoInit(task.repo ?? "", worktreePath);
14290
14834
  const quoted = shellQuote2(argv);
@@ -14312,7 +14856,21 @@ var init_session = __esm(() => {
14312
14856
 
14313
14857
  // ../kobe-web/server/bridge.ts
14314
14858
  import { existsSync as existsSync11 } from "fs";
14315
- import { join as join12, 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
+ }
14316
14874
  function sseResponse(register) {
14317
14875
  let unregister = null;
14318
14876
  let heartbeat = null;
@@ -14374,12 +14932,140 @@ async function rpcResponse(req, link, tearDown) {
14374
14932
  async function enginesResponse() {
14375
14933
  try {
14376
14934
  const ids = await availableEngineIds();
14377
- const engines = ids.map((id) => ({ id, label: engineDisplayName(id) }));
14935
+ const engines = ids.map((id) => ({
14936
+ id,
14937
+ label: engineDisplayName(id),
14938
+ effortLevels: engineEntry(id).effortLevels
14939
+ }));
14378
14940
  return Response.json({ engines: engines.length > 0 ? engines : [{ id: "claude", label: "Claude" }] });
14379
14941
  } catch (err) {
14380
14942
  return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 });
14381
14943
  }
14382
14944
  }
14945
+ function cliInvocationResponse() {
14946
+ return Response.json({ api: kobeApiInvocation() });
14947
+ }
14948
+ function stringValue(value, fallback = "") {
14949
+ return typeof value === "string" ? value : fallback;
14950
+ }
14951
+ function boolValue(value, fallback) {
14952
+ return typeof value === "boolean" ? value : fallback;
14953
+ }
14954
+ function customEngineIdsFrom(state) {
14955
+ const raw = state.customEngineIds;
14956
+ return Array.isArray(raw) ? raw.filter((s) => typeof s === "string" && s.trim().length > 0) : [];
14957
+ }
14958
+ function humanizeSlug(id) {
14959
+ return id.split(/[-_]+/).filter((word) => word.length > 0).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
14960
+ }
14961
+ function engineCommandText(state, id) {
14962
+ const override = stringValue(state[engineCommandKey(id)]).trim();
14963
+ return override || defaultEngineCommand(id).join(" ");
14964
+ }
14965
+ function engineLabelText(state, id) {
14966
+ const override = stringValue(state[engineNameKey(id)]).trim();
14967
+ return override || engineDisplayName(id);
14968
+ }
14969
+ function settingsSnapshot() {
14970
+ const state = loadStateFile();
14971
+ const custom = customEngineIdsFrom(state);
14972
+ const engineIds = [...BUILTIN_VENDORS, ...custom];
14973
+ const defaultEngine = stringValue(state.lastSelectedVendor, "claude");
14974
+ const focusAccent = stringValue(state.focusAccent, "primary");
14975
+ return Response.json({
14976
+ activeTheme: stringValue(state.activeTheme, "claude"),
14977
+ transparentBackground: boolValue(state.transparentBackground, false),
14978
+ focusAccent: FOCUS_ACCENTS.includes(focusAccent) ? focusAccent : "primary",
14979
+ notificationsToast: state["notifications.toast.enabled"] !== false,
14980
+ notificationsSound: state["notifications.sound.enabled"] !== false,
14981
+ settingsSurface: normalizeSettingsSurface(state[SETTINGS_SURFACE_KEY] ?? DEFAULT_SETTINGS_SURFACE),
14982
+ editorKind: normalizeEditorKind(state[EDITOR_KIND_KEY] ?? DEFAULT_EDITOR_KIND),
14983
+ editorCustomCommand: stringValue(state[EDITOR_CUSTOM_KEY]),
14984
+ remoteProjects: state["experimental.remoteProjects"] === true,
14985
+ autoStatus: state[AUTO_STATUS_KEY] === true,
14986
+ dispatcher: state[DISPATCHER_KEY] === true,
14987
+ defaultEngine,
14988
+ engines: engineIds.map((id) => ({
14989
+ id,
14990
+ label: engineLabelText(state, id),
14991
+ command: engineCommandText(state, id),
14992
+ isBuiltin: isBuiltinVendor(id),
14993
+ isCustom: !isBuiltinVendor(id),
14994
+ isDefault: id === defaultEngine
14995
+ }))
14996
+ });
14997
+ }
14998
+ function putIfString(patch, key, value) {
14999
+ if (typeof value === "string")
15000
+ patch[key] = value.trim();
15001
+ }
15002
+ function putIfBool(patch, key, value) {
15003
+ if (typeof value === "boolean")
15004
+ patch[key] = value;
15005
+ }
15006
+ async function settingsPatch(req) {
15007
+ try {
15008
+ const body = await req.json();
15009
+ const patch = {};
15010
+ putIfString(patch, "activeTheme", body.activeTheme);
15011
+ putIfBool(patch, "transparentBackground", body.transparentBackground);
15012
+ if (FOCUS_ACCENTS.includes(body.focusAccent)) {
15013
+ patch.focusAccent = body.focusAccent;
15014
+ }
15015
+ putIfBool(patch, "notifications.toast.enabled", body.notificationsToast);
15016
+ putIfBool(patch, "notifications.sound.enabled", body.notificationsSound);
15017
+ if (body.settingsSurface === "chattab" || body.settingsSurface === "taskpanel") {
15018
+ patch[SETTINGS_SURFACE_KEY] = body.settingsSurface;
15019
+ }
15020
+ if (EDITOR_KINDS.includes(body.editorKind))
15021
+ patch[EDITOR_KIND_KEY] = body.editorKind;
15022
+ putIfString(patch, EDITOR_CUSTOM_KEY, body.editorCustomCommand);
15023
+ putIfBool(patch, "experimental.remoteProjects", body.remoteProjects);
15024
+ putIfBool(patch, AUTO_STATUS_KEY, body.autoStatus);
15025
+ putIfBool(patch, DISPATCHER_KEY, body.dispatcher);
15026
+ putIfString(patch, "lastSelectedVendor", body.defaultEngine);
15027
+ const state = loadStateFile();
15028
+ const custom = customEngineIdsFrom(state);
15029
+ const known = new Set([...BUILTIN_VENDORS, ...custom]);
15030
+ const updates = Array.isArray(body.engineUpdates) ? body.engineUpdates : [];
15031
+ for (const raw of updates) {
15032
+ if (!raw || typeof raw !== "object")
15033
+ continue;
15034
+ const update = raw;
15035
+ if (typeof update.id !== "string" || !known.has(update.id))
15036
+ continue;
15037
+ putIfString(patch, engineCommandKey(update.id), update.command);
15038
+ putIfString(patch, engineNameKey(update.id), update.label);
15039
+ }
15040
+ if (body.addEngine && typeof body.addEngine === "object") {
15041
+ const add2 = body.addEngine;
15042
+ const id = typeof add2.id === "string" ? add2.id.trim().toLowerCase() : "";
15043
+ if (!ENGINE_ID_RE.test(id) || isBuiltinVendor(id) || known.has(id)) {
15044
+ return Response.json({ error: "invalid or duplicate engine id" }, { status: 400 });
15045
+ }
15046
+ const nextCustom = [...custom, id];
15047
+ patch.customEngineIds = nextCustom;
15048
+ patch[engineCommandKey(id)] = stringValue(add2.command).trim();
15049
+ const label = stringValue(add2.label).trim();
15050
+ patch[engineNameKey(id)] = label && label !== id ? label : humanizeSlug(id);
15051
+ }
15052
+ if (typeof body.removeEngine === "string") {
15053
+ const id = body.removeEngine;
15054
+ if (!isBuiltinVendor(id)) {
15055
+ patch.customEngineIds = custom.filter((engine) => engine !== id);
15056
+ patch[engineCommandKey(id)] = undefined;
15057
+ patch[engineNameKey(id)] = undefined;
15058
+ if (state.lastSelectedVendor === id)
15059
+ patch.lastSelectedVendor = "claude";
15060
+ }
15061
+ }
15062
+ if (Object.keys(patch).length > 0)
15063
+ patchStateFile(patch);
15064
+ return settingsSnapshot();
15065
+ } catch (err) {
15066
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 400 });
15067
+ }
15068
+ }
14383
15069
  async function sessionResponse(req, link) {
14384
15070
  try {
14385
15071
  const { taskId } = await req.json();
@@ -14407,6 +15093,9 @@ function createRequestHandler(deps) {
14407
15093
  const url = new URL(req.url);
14408
15094
  if (url.pathname === WEB_HEALTH_PATH)
14409
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
+ }
14410
15099
  if (url.pathname === "/events") {
14411
15100
  return sseResponse((send2) => {
14412
15101
  send2("snapshot", link.snapshot());
@@ -14426,6 +15115,12 @@ function createRequestHandler(deps) {
14426
15115
  return specResponse(url, link, terminalSpec);
14427
15116
  if (url.pathname === "/api/engines" && req.method === "GET")
14428
15117
  return enginesResponse();
15118
+ if (url.pathname === "/api/cli-invocation" && req.method === "GET")
15119
+ return cliInvocationResponse();
15120
+ if (url.pathname === "/api/settings" && req.method === "GET")
15121
+ return settingsSnapshot();
15122
+ if (url.pathname === "/api/settings" && req.method === "PATCH")
15123
+ return settingsPatch(req);
14429
15124
  if (url.pathname === "/api/quick-prompts" && req.method === "GET")
14430
15125
  return quickPromptsGet();
14431
15126
  if (url.pathname === "/api/quick-prompts" && req.method === "PUT")
@@ -14439,6 +15134,12 @@ function createRequestHandler(deps) {
14439
15134
  const history = await handleHistoryRequest(req, url);
14440
15135
  if (history)
14441
15136
  return history;
15137
+ const issues = await handleIssuesRequest(req, url, link);
15138
+ if (issues)
15139
+ return issues;
15140
+ const issueAssets = await handleIssueAssetsRequest(req, url);
15141
+ if (issueAssets)
15142
+ return issueAssets;
14442
15143
  const themes = handleThemesRequest(req, url);
14443
15144
  if (themes)
14444
15145
  return themes;
@@ -14467,10 +15168,10 @@ async function quickPromptsPut(req) {
14467
15168
  }
14468
15169
  async function staticResponse(pathname, staticDir) {
14469
15170
  const rel = pathname === "/" ? "/index.html" : pathname;
14470
- const resolved = normalize2(join12(staticDir, rel));
15171
+ const resolved = normalize2(join15(staticDir, rel));
14471
15172
  if (!resolved.startsWith(staticDir))
14472
15173
  return new Response("forbidden", { status: 403 });
14473
- const file = Bun.file(existsSync11(resolved) ? resolved : join12(staticDir, "index.html"));
15174
+ const file = Bun.file(existsSync11(resolved) ? resolved : join15(staticDir, "index.html"));
14474
15175
  if (!await file.exists()) {
14475
15176
  return new Response("kobe web assets not built \u2014 run `bun --filter kobe-web build`", { status: 503 });
14476
15177
  }
@@ -14531,8 +15232,9 @@ async function createBridgeServer(opts = {}) {
14531
15232
  for (const send2 of sseSends)
14532
15233
  send2("snapshot", link.snapshot());
14533
15234
  });
14534
- const handle = createRequestHandler({ link, sseSends, staticDir });
14535
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 });
14536
15238
  const server = Bun.serve({ port, hostname, idleTimeout: 0, fetch: handle });
14537
15239
  return {
14538
15240
  port: server.port ?? port,
@@ -14542,17 +15244,27 @@ async function createBridgeServer(opts = {}) {
14542
15244
  }
14543
15245
  };
14544
15246
  }
14545
- var WEB_HEALTH_MARKER = "kobe-web", WEB_HEALTH_PATH = "/__kobe_web", 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;
14546
15248
  var init_bridge = __esm(() => {
14547
15249
  init_account_detect();
14548
15250
  init_interactive_command();
15251
+ init_registry();
15252
+ init_auto_status();
15253
+ init_dispatcher();
14549
15254
  init_repos();
15255
+ init_store();
15256
+ init_editor_prefs();
15257
+ init_vendor();
14550
15258
  init_history4();
14551
15259
  init_notes();
14552
15260
  init_themes();
14553
15261
  init_daemon_link();
15262
+ init_issue_assets_route();
14554
15263
  init_rpc_allowlist();
14555
15264
  init_session();
15265
+ LOCAL_ORIGIN = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/;
15266
+ FOCUS_ACCENTS = ["primary", "success", "info"];
15267
+ ENGINE_ID_RE = /^[a-z][a-z0-9_-]{0,47}$/;
14556
15268
  QUICK_PROMPT_KEYS = {
14557
15269
  review: "boardPrompt.review",
14558
15270
  pr: "boardPrompt.pr"
@@ -14570,18 +15282,18 @@ __export(exports_web_cmd, {
14570
15282
  runWebSubcommand: () => runWebSubcommand
14571
15283
  });
14572
15284
  import { existsSync as existsSync12 } from "fs";
14573
- import { homedir as homedir17 } from "os";
14574
- import { resolve as resolve6 } from "path";
15285
+ import { homedir as homedir18 } from "os";
15286
+ import { resolve as resolve8 } from "path";
14575
15287
  import { fileURLToPath as fileURLToPath3 } from "url";
14576
15288
  function homeLabel() {
14577
15289
  const explicit = process.env.KOBE_HOME_DIR?.trim();
14578
- return explicit ? `sandbox: ${explicit}` : `${homedir17()}/.kobe (production)`;
15290
+ return explicit ? `sandbox: ${explicit}` : `${homedir18()}/.kobe (production)`;
14579
15291
  }
14580
15292
  function resolveStaticDir() {
14581
15293
  const here = fileURLToPath3(import.meta.url);
14582
15294
  const candidates = [
14583
- resolve6(here, "../../../../kobe-web/dist"),
14584
- resolve6(here, "../../web-ui")
15295
+ resolve8(here, "../../../../kobe-web/dist"),
15296
+ resolve8(here, "../../web-ui")
14585
15297
  ];
14586
15298
  for (const dir of candidates) {
14587
15299
  if (existsSync12(`${dir}/index.html`))
@@ -14592,8 +15304,8 @@ function resolveStaticDir() {
14592
15304
  function resolvePtyServer() {
14593
15305
  const here = fileURLToPath3(import.meta.url);
14594
15306
  const candidates = [
14595
- resolve6(here, "../../../../kobe-web/pty-server.mjs"),
14596
- resolve6(here, "../../web-ui/pty-server.mjs")
15307
+ resolve8(here, "../../../../kobe-web/pty-server.mjs"),
15308
+ resolve8(here, "../../web-ui/pty-server.mjs")
14597
15309
  ];
14598
15310
  for (const file of candidates) {
14599
15311
  if (existsSync12(file))
@@ -14855,15 +15567,15 @@ __export(exports_hook_cmd, {
14855
15567
  parseWorktreeAddPath: () => parseWorktreeAddPath,
14856
15568
  ensureGlobalKobeHooks: () => ensureGlobalKobeHooks
14857
15569
  });
14858
- import { homedir as homedir18 } from "os";
14859
- import { join as join13, resolve as resolve7 } from "path";
15570
+ import { homedir as homedir19 } from "os";
15571
+ import { join as join16, resolve as resolve9 } from "path";
14860
15572
  async function readTextWithTimeout(read, timeoutMs = STDIN_READ_TIMEOUT_MS) {
14861
15573
  let raceTimer;
14862
15574
  try {
14863
15575
  return await Promise.race([
14864
15576
  read(),
14865
- new Promise((resolve8) => {
14866
- raceTimer = setTimeout(() => resolve8(""), timeoutMs);
15577
+ new Promise((resolve10) => {
15578
+ raceTimer = setTimeout(() => resolve10(""), timeoutMs);
14867
15579
  })
14868
15580
  ]);
14869
15581
  } finally {
@@ -14932,7 +15644,7 @@ async function runWorktreeCreatedHook() {
14932
15644
  if (!rawPath)
14933
15645
  return;
14934
15646
  const cwd = typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
14935
- const worktreePath = resolve7(cwd, rawPath);
15647
+ const worktreePath = resolve9(cwd, rawPath);
14936
15648
  const client = await connectIfRunning();
14937
15649
  if (!client)
14938
15650
  return;
@@ -14987,7 +15699,7 @@ function activityHookAdapters() {
14987
15699
  return ALL_VENDORS.map((v) => createEngineHookAdapter(v)).filter((a) => a.supportsHooks());
14988
15700
  }
14989
15701
  function globalSettingsPath() {
14990
- return join13(homedir18(), ".claude", "settings.json");
15702
+ return join16(homedir19(), ".claude", "settings.json");
14991
15703
  }
14992
15704
  function persistedSyncPath(stored) {
14993
15705
  if (!stored || stored === "off")
@@ -14995,7 +15707,7 @@ function persistedSyncPath(stored) {
14995
15707
  if (stored === "global")
14996
15708
  return globalSettingsPath();
14997
15709
  if (stored.startsWith("repo:"))
14998
- return join13(resolve7(stored.slice(5)), ".claude", "settings.json");
15710
+ return join16(resolve9(stored.slice(5)), ".claude", "settings.json");
14999
15711
  return stored;
15000
15712
  }
15001
15713
  async function ensureGlobalKobeHooks() {
@@ -16544,7 +17256,11 @@ class RemoteOrchestrator {
16544
17256
  });
16545
17257
  }
16546
17258
  async createTask(input) {
16547
- const res = await this.client.request("task.create", input);
17259
+ const { modelEffort, ...rest } = input;
17260
+ const res = await this.client.request("task.create", {
17261
+ ...rest,
17262
+ effort: modelEffort
17263
+ });
16548
17264
  return deserializeTask(res.task);
16549
17265
  }
16550
17266
  async ensureMainTask(repo) {
@@ -16717,6 +17433,7 @@ function deserializeTask(s) {
16717
17433
  pinned: s.pinned,
16718
17434
  vendor: s.vendor,
16719
17435
  prStatus: s.prStatus,
17436
+ modelEffort: s.modelEffort,
16720
17437
  createdAt: s.createdAt,
16721
17438
  updatedAt: s.updatedAt
16722
17439
  };
@@ -16727,6 +17444,7 @@ var init_remote_orchestrator = __esm(() => {
16727
17444
  init_protocol();
16728
17445
  init_dev();
16729
17446
  init_worktree_changes();
17447
+ init_task();
16730
17448
  init_version();
16731
17449
  });
16732
17450
 
@@ -17043,7 +17761,7 @@ function addTheme2(name, theme) {
17043
17761
  }
17044
17762
  function resolveTheme(theme, mode = "dark") {
17045
17763
  const defs = theme.defs ?? {};
17046
- function resolve8(c, chain = []) {
17764
+ function resolve10(c, chain = []) {
17047
17765
  if (typeof c === "string") {
17048
17766
  if (c === "transparent" || c === "none")
17049
17767
  return RGBA.fromInts(0, 0, 0, 0);
@@ -17055,13 +17773,13 @@ function resolveTheme(theme, mode = "dark") {
17055
17773
  const next = defs[c] ?? theme.theme[c];
17056
17774
  if (next === undefined)
17057
17775
  return RGBA.fromInts(0, 0, 0);
17058
- return resolve8(next, [...chain, c]);
17776
+ return resolve10(next, [...chain, c]);
17059
17777
  }
17060
- return resolve8(c[mode], chain);
17778
+ return resolve10(c[mode], chain);
17061
17779
  }
17062
17780
  const out = {};
17063
17781
  for (const [k, v] of Object.entries(theme.theme)) {
17064
- out[k] = resolve8(v);
17782
+ out[k] = resolve10(v);
17065
17783
  }
17066
17784
  const text = out.text ?? RGBA.fromHex("#ffffff");
17067
17785
  const background = out.background ?? RGBA.fromHex("#000000");
@@ -17538,13 +18256,13 @@ function validateRepoPath(repo) {
17538
18256
  const trimmed = repo.trim();
17539
18257
  if (!trimmed)
17540
18258
  return "repo path is required";
17541
- let stat4;
18259
+ let stat5;
17542
18260
  try {
17543
- stat4 = fs3.statSync(trimmed);
18261
+ stat5 = fs3.statSync(trimmed);
17544
18262
  } catch {
17545
18263
  return `path does not exist: ${trimmed}`;
17546
18264
  }
17547
- if (!stat4.isDirectory())
18265
+ if (!stat5.isDirectory())
17548
18266
  return `not a directory: ${trimmed}`;
17549
18267
  try {
17550
18268
  const out = spawnSync10("git", ["rev-parse", "--git-dir"], {
@@ -17726,8 +18444,8 @@ function findAvailableFolderName(parentDir, base) {
17726
18444
  if (!parentExpanded)
17727
18445
  return base;
17728
18446
  try {
17729
- const stat4 = fs5.statSync(parentExpanded);
17730
- if (!stat4.isDirectory())
18447
+ const stat5 = fs5.statSync(parentExpanded);
18448
+ if (!stat5.isDirectory())
17731
18449
  return base;
17732
18450
  } catch {
17733
18451
  return base;
@@ -17742,7 +18460,7 @@ function findAvailableFolderName(parentDir, base) {
17742
18460
  return trimmed;
17743
18461
  }
17744
18462
  function cloneRepo(url, target, onProgress) {
17745
- return new Promise((resolve8) => {
18463
+ return new Promise((resolve10) => {
17746
18464
  let stderrBuf = "";
17747
18465
  try {
17748
18466
  const child = spawn4("git", ["clone", "--progress", url, target], {
@@ -17759,18 +18477,18 @@ function cloneRepo(url, target, onProgress) {
17759
18477
  }
17760
18478
  });
17761
18479
  child.on("error", (err) => {
17762
- resolve8({ ok: false, error: err.message });
18480
+ resolve10({ ok: false, error: err.message });
17763
18481
  });
17764
18482
  child.on("close", (code) => {
17765
18483
  if (code === 0) {
17766
- resolve8({ ok: true, path: target });
18484
+ resolve10({ ok: true, path: target });
17767
18485
  return;
17768
18486
  }
17769
18487
  const tail = stderrBuf.split(/[\r\n]+/).filter((s) => s.trim().length > 0).pop() ?? `git clone exited with ${code}`;
17770
- resolve8({ ok: false, error: tail });
18488
+ resolve10({ ok: false, error: tail });
17771
18489
  });
17772
18490
  } catch (err) {
17773
- resolve8({ ok: false, error: err instanceof Error ? err.message : String(err) });
18491
+ resolve10({ ok: false, error: err instanceof Error ? err.message : String(err) });
17774
18492
  }
17775
18493
  });
17776
18494
  }
@@ -17872,7 +18590,7 @@ function clampCursor(cursor, listLength) {
17872
18590
  return 0;
17873
18591
  return Math.max(0, Math.min(listLength - 1, cursor));
17874
18592
  }
17875
- function resolveBaseRef2(typed, filteredBranches, cursor) {
18593
+ function resolveBaseRef(typed, filteredBranches, cursor) {
17876
18594
  const picked = filteredBranches[cursor];
17877
18595
  if (picked)
17878
18596
  return picked;
@@ -18446,7 +19164,7 @@ function NewTaskDialogView(props) {
18446
19164
  setBaseRef(stripNewlines(v));
18447
19165
  });
18448
19166
  setProp(_el$28, "onSubmit", () => {
18449
- setBaseRef(resolveBaseRef2(baseRef(), branchFiltered(), branchCursor()));
19167
+ setBaseRef(resolveBaseRef(baseRef(), branchFiltered(), branchCursor()));
18450
19168
  setBaseRefTouched(true);
18451
19169
  setField("confirm");
18452
19170
  });
@@ -18960,7 +19678,7 @@ var init_dialog2 = __esm(() => {
18960
19678
 
18961
19679
  // src/tui/component/new-task-dialog/index.tsx
18962
19680
  function show(dialog, defaultRepo, savedRepos, options) {
18963
- return new Promise((resolve8) => {
19681
+ return new Promise((resolve10) => {
18964
19682
  dialog.replace(() => createComponent2(NewTaskDialogView, {
18965
19683
  defaultRepo,
18966
19684
  savedRepos,
@@ -18976,9 +19694,9 @@ function show(dialog, defaultRepo, savedRepos, options) {
18976
19694
  get discoverAdoptable() {
18977
19695
  return options?.discoverAdoptable;
18978
19696
  },
18979
- onSubmit: (v) => resolve8(v),
18980
- onCancel: () => resolve8(undefined)
18981
- }), () => resolve8(undefined));
19697
+ onSubmit: (v) => resolve10(v),
19698
+ onCancel: () => resolve10(undefined)
19699
+ }), () => resolve10(undefined));
18982
19700
  dialog.setSize("medium");
18983
19701
  });
18984
19702
  }
@@ -19147,11 +19865,11 @@ function QuickTaskComposerView(props) {
19147
19865
  })();
19148
19866
  }
19149
19867
  function show2(dialog, opts) {
19150
- return new Promise((resolve8) => {
19868
+ return new Promise((resolve10) => {
19151
19869
  dialog.replace(() => createComponent2(QuickTaskComposerView, mergeProps3(opts, {
19152
- onSubmit: (r) => resolve8(r),
19153
- onCancel: () => resolve8(undefined)
19154
- })), () => resolve8(undefined));
19870
+ onSubmit: (r) => resolve10(r),
19871
+ onCancel: () => resolve10(undefined)
19872
+ })), () => resolve10(undefined));
19155
19873
  dialog.setSize("medium");
19156
19874
  });
19157
19875
  }
@@ -19920,9 +20638,9 @@ var pulse_default = "../pulse-n3cq1btw.wav";
19920
20638
  var init_pulse = () => {};
19921
20639
 
19922
20640
  // src/tui/lib/sound.ts
19923
- import { existsSync as existsSync14, mkdirSync as mkdirSync6 } from "fs";
20641
+ import { existsSync as existsSync14, mkdirSync as mkdirSync7 } from "fs";
19924
20642
  import { tmpdir as tmpdir2 } from "os";
19925
- import { basename as basename6, isAbsolute as isAbsolute2, join as join15, resolve as resolve8 } from "path";
20643
+ import { basename as basename6, isAbsolute as isAbsolute3, join as join18, resolve as resolve10 } from "path";
19926
20644
  function args(player, file, volume) {
19927
20645
  if (player === "ffplay")
19928
20646
  return [player, "-autoexit", "-nodisp", "-af", `volume=${volume}`, file];
@@ -19945,13 +20663,13 @@ function pickPlayer() {
19945
20663
  return cachedPlayer;
19946
20664
  const path12 = process.env.PATH ?? "";
19947
20665
  const segments = path12.split(":").filter(Boolean);
19948
- cachedPlayer = PLAYERS.find((p) => segments.some((dir) => existsSync14(join15(dir, p)))) ?? null;
20666
+ cachedPlayer = PLAYERS.find((p) => segments.some((dir) => existsSync14(join18(dir, p)))) ?? null;
19949
20667
  return cachedPlayer;
19950
20668
  }
19951
20669
  async function ensureAsset() {
19952
20670
  cachedPath ??= (async () => {
19953
- mkdirSync6(DIR, { recursive: true });
19954
- const dest = join15(DIR, basename6(pulseAsset));
20671
+ mkdirSync7(DIR, { recursive: true });
20672
+ const dest = join18(DIR, basename6(pulseAsset));
19955
20673
  const out = Bun.file(dest);
19956
20674
  if (await out.exists())
19957
20675
  return dest;
@@ -19980,8 +20698,8 @@ function pulse(volume = 0.4) {
19980
20698
  var pulseAsset, DIR, PLAYERS, cachedPlayer, cachedPath;
19981
20699
  var init_sound = __esm(() => {
19982
20700
  init_pulse();
19983
- pulseAsset = isAbsolute2(pulse_default) ? pulse_default : resolve8(import.meta.dir, pulse_default);
19984
- DIR = join15(tmpdir2(), "kobe-sfx");
20701
+ pulseAsset = isAbsolute3(pulse_default) ? pulse_default : resolve10(import.meta.dir, pulse_default);
20702
+ DIR = join18(tmpdir2(), "kobe-sfx");
19985
20703
  PLAYERS = [
19986
20704
  "ffplay",
19987
20705
  "mpv",
@@ -20106,10 +20824,10 @@ var init_apply_ui_prefs = __esm(() => {
20106
20824
  });
20107
20825
 
20108
20826
  // src/tui/lib/persisted-ui-prefs.ts
20109
- import { readFileSync as readFileSync13 } from "fs";
20827
+ import { readFileSync as readFileSync14 } from "fs";
20110
20828
  function readPersistedUiPrefs(fallbackTheme) {
20111
20829
  try {
20112
- const parsed = JSON.parse(readFileSync13(kvStatePath(), "utf8"));
20830
+ const parsed = JSON.parse(readFileSync14(kvStatePath(), "utf8"));
20113
20831
  const theme = typeof parsed.activeTheme === "string" && hasTheme(parsed.activeTheme) ? parsed.activeTheme : fallbackTheme;
20114
20832
  const transparent = parsed.transparentBackground === true;
20115
20833
  const focusAccent = typeof parsed.focusAccent === "string" && FOCUS_ACCENT_SLOTS.includes(parsed.focusAccent) ? parsed.focusAccent : null;
@@ -20385,6 +21103,7 @@ var init_host = __esm(() => {
20385
21103
  init_remote_orchestrator();
20386
21104
  init_account_detect();
20387
21105
  init_repos();
21106
+ init_task();
20388
21107
  init_new_task_dialog();
20389
21108
  init_theme2();
20390
21109
  init_host_boot();
@@ -20566,6 +21285,10 @@ async function deliverFirstPromptToTask(orch, task, repo, vendor, prompt) {
20566
21285
  async function jumpToTask(orch, task, repo, vendor) {
20567
21286
  await ensureTaskSession2(orch, task, repo, vendor);
20568
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));
20569
21292
  await runTmux(["switch-client", "-t", `=${tmuxSessionName(task.id)}`]);
20570
21293
  }
20571
21294
  function QuickTaskPage(props) {
@@ -20660,6 +21383,7 @@ var init_host2 = __esm(() => {
20660
21383
  init_repos();
20661
21384
  init_client2();
20662
21385
  init_prompt_delivery();
21386
+ init_task();
20663
21387
  init_quick_task_composer();
20664
21388
  init_theme2();
20665
21389
  init_git_snapshot();
@@ -20999,7 +21723,7 @@ var init_dialog3 = __esm(() => {
20999
21723
 
21000
21724
  // src/tui/component/rename-task-dialog/index.tsx
21001
21725
  function show3(dialog, currentTitle, opts = {}) {
21002
- return new Promise((resolve9) => {
21726
+ return new Promise((resolve11) => {
21003
21727
  dialog.replace(() => createComponent2(RenameTaskDialogView, {
21004
21728
  currentTitle,
21005
21729
  get dialogTitle() {
@@ -21017,9 +21741,9 @@ function show3(dialog, currentTitle, opts = {}) {
21017
21741
  get allowEmpty() {
21018
21742
  return opts.allowEmpty;
21019
21743
  },
21020
- onSubmit: (v) => resolve9(v),
21021
- onCancel: () => resolve9(undefined)
21022
- }), () => resolve9(undefined));
21744
+ onSubmit: (v) => resolve11(v),
21745
+ onCancel: () => resolve11(undefined)
21746
+ }), () => resolve11(undefined));
21023
21747
  });
21024
21748
  }
21025
21749
  var RenameTaskDialog;
@@ -21031,22 +21755,6 @@ var init_rename_task_dialog = __esm(() => {
21031
21755
  };
21032
21756
  });
21033
21757
 
21034
- // src/tui/lib/editor-prefs.ts
21035
- function normalizeEditorKind(value) {
21036
- return EDITOR_KINDS.includes(value) ? value : DEFAULT_EDITOR_KIND;
21037
- }
21038
- var EDITOR_KINDS, AUTO_EDITOR_CANDIDATES, EDITOR_KIND_KEY = "editor.kind", EDITOR_CUSTOM_KEY = "editor.customCommand", DEFAULT_EDITOR_KIND = "auto";
21039
- var init_editor_prefs = __esm(() => {
21040
- EDITOR_KINDS = ["auto", "vim", "nvim", "nano", "emacs", "custom"];
21041
- AUTO_EDITOR_CANDIDATES = ["nvim", "vim", "emacs", "nano"];
21042
- });
21043
-
21044
- // src/tui/lib/settings-surface.ts
21045
- function normalizeSettingsSurface(value) {
21046
- return value === "taskpanel" ? "taskpanel" : "chattab";
21047
- }
21048
- var SETTINGS_SURFACE_KEY = "settings.surface", DEFAULT_SETTINGS_SURFACE = "chattab";
21049
-
21050
21758
  // src/tui/ui/dialog-confirm.tsx
21051
21759
  import { TextAttributes as TextAttributes6 } from "@opentui/core";
21052
21760
  function titlecase(s) {
@@ -21157,18 +21865,18 @@ var init_dialog_confirm = __esm(() => {
21157
21865
  init_keymap();
21158
21866
  init_dialog();
21159
21867
  DialogConfirm.show = (dialog, title, message, label, confirmLabel, options) => {
21160
- return new Promise((resolve9) => {
21868
+ return new Promise((resolve11) => {
21161
21869
  dialog.replace(() => createComponent2(DialogConfirm, {
21162
21870
  title,
21163
21871
  message,
21164
- onConfirm: () => resolve9(true),
21165
- onCancel: () => resolve9(false),
21872
+ onConfirm: () => resolve11(true),
21873
+ onCancel: () => resolve11(false),
21166
21874
  label,
21167
21875
  confirmLabel,
21168
21876
  get initialActive() {
21169
21877
  return options?.initialActive;
21170
21878
  }
21171
- }), () => resolve9(undefined));
21879
+ }), () => resolve11(undefined));
21172
21880
  dialog.setSize("small");
21173
21881
  });
21174
21882
  };
@@ -21176,7 +21884,7 @@ var init_dialog_confirm = __esm(() => {
21176
21884
 
21177
21885
  // src/tui/component/settings-dialog/actions.ts
21178
21886
  import { unlinkSync as unlinkSync2 } from "fs";
21179
- import { join as join16 } from "path";
21887
+ import { join as join19 } from "path";
21180
21888
  function hasRestartableDaemon(orchestrator) {
21181
21889
  return orchestrator instanceof RemoteOrchestrator;
21182
21890
  }
@@ -21193,7 +21901,7 @@ async function confirmResetState(dialog, kv, renderer) {
21193
21901
  return;
21194
21902
  kv.clear();
21195
21903
  try {
21196
- unlinkSync2(join16(homeDir(), ".kobe", "tasks.json"));
21904
+ unlinkSync2(join19(homeDir(), ".kobe", "tasks.json"));
21197
21905
  } catch (err) {
21198
21906
  if (err.code !== "ENOENT") {
21199
21907
  console.error("kobe: failed to delete tasks.json during reset:", err);
@@ -22507,7 +23215,7 @@ var init_sections = __esm(() => {
22507
23215
 
22508
23216
  // src/tui/component/settings-dialog.tsx
22509
23217
  import { TextAttributes as TextAttributes8 } from "@opentui/core";
22510
- function humanizeSlug(id) {
23218
+ function humanizeSlug2(id) {
22511
23219
  return id.split(/[-_]+/).filter((word) => word.length > 0).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
22512
23220
  }
22513
23221
  function SettingsDialog(props) {
@@ -22622,7 +23330,7 @@ function SettingsDialog(props) {
22622
23330
  const v = props.kv.get(engineCommandKey(vendor), "");
22623
23331
  return typeof v === "string" ? v.trim() : "";
22624
23332
  }
22625
- function engineCommandText(vendor) {
23333
+ function engineCommandText2(vendor) {
22626
23334
  return engineOverride(vendor) || defaultEngineCommand(vendor).join(" ");
22627
23335
  }
22628
23336
  function engineIsDefault(vendor) {
@@ -22648,7 +23356,7 @@ function SettingsDialog(props) {
22648
23356
  setDefaultEngineSig(vendor);
22649
23357
  }
22650
23358
  async function editEngine(vendor) {
22651
- const next = await RenameTaskDialog.show(dialog, engineCommandText(vendor), {
23359
+ const next = await RenameTaskDialog.show(dialog, engineCommandText2(vendor), {
22652
23360
  dialogTitle: `${engineName(vendor)} launch command`,
22653
23361
  fieldLabel: "command",
22654
23362
  submitLabel: "save",
@@ -22707,7 +23415,7 @@ function SettingsDialog(props) {
22707
23415
  if (command.trim())
22708
23416
  props.kv.set(engineCommandKey(id), command.trim());
22709
23417
  const typedName = name?.trim() ?? "";
22710
- props.kv.set(engineNameKey(id), typedName && typedName !== id ? typedName : humanizeSlug(id));
23418
+ props.kv.set(engineNameKey(id), typedName && typedName !== id ? typedName : humanizeSlug2(id));
22711
23419
  }
22712
23420
  function currentEngineRow() {
22713
23421
  if (section() !== "engines" || level() !== "body")
@@ -22973,7 +23681,7 @@ function SettingsDialog(props) {
22973
23681
  },
22974
23682
  isCustom: (v) => !isBuiltinVendor(v),
22975
23683
  displayName: engineName,
22976
- commandText: engineCommandText,
23684
+ commandText: engineCommandText2,
22977
23685
  isDefault: engineIsDefault,
22978
23686
  isDefaultEngine,
22979
23687
  editEngine: (v) => void editEngine(v),
@@ -23079,6 +23787,7 @@ var init_settings_dialog = __esm(() => {
23079
23787
  init_auto_status();
23080
23788
  init_dispatcher();
23081
23789
  init_repos();
23790
+ init_task();
23082
23791
  init_vendor();
23083
23792
  init_theme2();
23084
23793
  init_editor_prefs();
@@ -23090,17 +23799,17 @@ var init_settings_dialog = __esm(() => {
23090
23799
  init_sections();
23091
23800
  SettingsDialog.show = (dialog, kv, orchestrator) => {
23092
23801
  let visualPrefsChanged = false;
23093
- return new Promise((resolve9) => {
23802
+ return new Promise((resolve11) => {
23094
23803
  dialog.replace(() => createComponent2(SettingsDialog, {
23095
23804
  kv,
23096
23805
  orchestrator,
23097
23806
  onVisualPrefsChange: () => {
23098
23807
  visualPrefsChanged = true;
23099
23808
  },
23100
- onClose: () => resolve9({
23809
+ onClose: () => resolve11({
23101
23810
  visualPrefsChanged
23102
23811
  })
23103
- }), () => resolve9({
23812
+ }), () => resolve11({
23104
23813
  visualPrefsChanged
23105
23814
  }));
23106
23815
  });
@@ -23495,6 +24204,7 @@ var init_task_actions = __esm(() => {
23495
24204
  init_interactive_command();
23496
24205
  init_errors();
23497
24206
  init_repos();
24207
+ init_task();
23498
24208
  init_vendor();
23499
24209
  init_tmux();
23500
24210
  });
@@ -23502,15 +24212,15 @@ var init_task_actions = __esm(() => {
23502
24212
  // src/tui/lib/worktree-opener.ts
23503
24213
  import { spawn as spawn5 } from "child_process";
23504
24214
  import { existsSync as existsSync15 } from "fs";
23505
- import { basename as basename7, delimiter, isAbsolute as isAbsolute3, join as join17 } from "path";
24215
+ import { basename as basename7, delimiter, isAbsolute as isAbsolute4, join as join20 } from "path";
23506
24216
  function executableOnPath(command, env, exists) {
23507
- if (isAbsolute3(command))
24217
+ if (isAbsolute4(command))
23508
24218
  return exists(command);
23509
24219
  const pathEnv = env.PATH ?? "";
23510
24220
  for (const dir of pathEnv.split(delimiter)) {
23511
24221
  if (!dir)
23512
24222
  continue;
23513
- if (exists(join17(dir, command)))
24223
+ if (exists(join20(dir, command)))
23514
24224
  return true;
23515
24225
  }
23516
24226
  return false;
@@ -23643,11 +24353,11 @@ var init_background_poll = __esm(() => {
23643
24353
  });
23644
24354
 
23645
24355
  // src/tui/panes/sidebar/git-head.ts
23646
- import { stat as stat4 } from "fs/promises";
23647
- import { join as join18 } from "path";
24356
+ import { stat as stat5 } from "fs/promises";
24357
+ import { join as join21 } from "path";
23648
24358
  async function headFingerprint(repo) {
23649
24359
  try {
23650
- const st = await stat4(join18(repo, ".git", "HEAD"));
24360
+ const st = await stat5(join21(repo, ".git", "HEAD"));
23651
24361
  return `${st.mtimeMs}:${st.size}`;
23652
24362
  } catch {
23653
24363
  return null;
@@ -24130,6 +24840,28 @@ function Sidebar(props) {
24130
24840
  const dims = useTerminalDimensions();
24131
24841
  const [hover, setHover] = createSignal(null);
24132
24842
  const [cursorIndex, setCursorIndex] = createSignal(-1);
24843
+ let scrollRef;
24844
+ let outerBoxRef;
24845
+ const rowEls = new Map;
24846
+ createEffect(() => {
24847
+ const w = props.width ? props.width() : SIDEBAR_WIDTH;
24848
+ const el = outerBoxRef;
24849
+ if (!el)
24850
+ return;
24851
+ el.width = w;
24852
+ el.flexShrink = 1;
24853
+ el.minHeight = 0;
24854
+ });
24855
+ createEffect(on([cursorIndex, rows], ([i]) => {
24856
+ if (!scrollRef)
24857
+ return;
24858
+ if (scrollRef.viewport.height <= 0)
24859
+ return;
24860
+ const el = rowEls.get(i);
24861
+ if (!el)
24862
+ return;
24863
+ scrollRef.scrollChildIntoView(el.id);
24864
+ }));
24133
24865
  createEffect(on(() => [props.selectedId(), flatIds()], ([id, ids]) => {
24134
24866
  const cur = untrack(cursorIndex);
24135
24867
  if (id === null) {
@@ -24142,8 +24874,14 @@ function Sidebar(props) {
24142
24874
  return;
24143
24875
  }
24144
24876
  const idx = ids.indexOf(id);
24145
- if (idx >= 0 && idx !== cur)
24146
- 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
+ }
24147
24885
  }));
24148
24886
  createEffect(on(view, () => {
24149
24887
  const ids = flatIds();
@@ -24242,7 +24980,11 @@ function Sidebar(props) {
24242
24980
  insertNode(_el$5, _el$6);
24243
24981
  insertNode(_el$5, _el$22);
24244
24982
  insertNode(_el$5, _el$28);
24245
- setProp(_el$5, "flexShrink", 0);
24983
+ use((r) => {
24984
+ outerBoxRef = r;
24985
+ }, _el$5);
24986
+ setProp(_el$5, "flexGrow", 1);
24987
+ setProp(_el$5, "minHeight", 0);
24246
24988
  setProp(_el$5, "flexDirection", "column");
24247
24989
  setProp(_el$5, "paddingTop", 1);
24248
24990
  setProp(_el$5, "paddingBottom", 1);
@@ -24270,9 +25012,9 @@ function Sidebar(props) {
24270
25012
  setProp(_el$35, "onMouseUp", () => props.onHeaderStatusClick?.());
24271
25013
  insert(_el$35, () => status().label);
24272
25014
  effect((_p$) => {
24273
- var _v$20 = status().emphasize ? theme.warning : theme.textMuted, _v$21 = status().emphasize ? TextAttributes11.BOLD : TextAttributes11.DIM;
24274
- _v$20 !== _p$.e && (_p$.e = setProp(_el$35, "fg", _v$20, _p$.e));
24275
- _v$21 !== _p$.t && (_p$.t = setProp(_el$35, "attributes", _v$21, _p$.t));
25015
+ var _v$19 = status().emphasize ? theme.warning : theme.textMuted, _v$20 = status().emphasize ? TextAttributes11.BOLD : TextAttributes11.DIM;
25016
+ _v$19 !== _p$.e && (_p$.e = setProp(_el$35, "fg", _v$19, _p$.e));
25017
+ _v$20 !== _p$.t && (_p$.t = setProp(_el$35, "attributes", _v$20, _p$.t));
24276
25018
  return _p$;
24277
25019
  }, {
24278
25020
  e: undefined,
@@ -24384,9 +25126,9 @@ function Sidebar(props) {
24384
25126
  setProp(_el$36, "onMouseUp", () => setView(tab.view));
24385
25127
  insert(_el$36, () => tab.label);
24386
25128
  effect((_p$) => {
24387
- var _v$22 = active() ? theme.primary : theme.textMuted, _v$23 = active() ? TextAttributes11.BOLD : undefined;
24388
- _v$22 !== _p$.e && (_p$.e = setProp(_el$36, "fg", _v$22, _p$.e));
24389
- _v$23 !== _p$.t && (_p$.t = setProp(_el$36, "attributes", _v$23, _p$.t));
25129
+ var _v$21 = active() ? theme.primary : theme.textMuted, _v$22 = active() ? TextAttributes11.BOLD : undefined;
25130
+ _v$21 !== _p$.e && (_p$.e = setProp(_el$36, "fg", _v$21, _p$.e));
25131
+ _v$22 !== _p$.t && (_p$.t = setProp(_el$36, "attributes", _v$22, _p$.t));
24390
25132
  return _p$;
24391
25133
  }, {
24392
25134
  e: undefined,
@@ -24420,7 +25162,11 @@ function Sidebar(props) {
24420
25162
  }
24421
25163
  }), null);
24422
25164
  insertNode(_el$28, _el$29);
25165
+ use((r) => {
25166
+ scrollRef = r;
25167
+ }, _el$28);
24423
25168
  setProp(_el$28, "flexGrow", 1);
25169
+ setProp(_el$28, "minHeight", 0);
24424
25170
  setProp(_el$28, "verticalScrollbarOptions", {
24425
25171
  trackOptions: {
24426
25172
  foregroundColor: "transparent"
@@ -24529,6 +25275,13 @@ function Sidebar(props) {
24529
25275
  });
24530
25276
  }
24531
25277
  }), _el$38);
25278
+ use((r) => {
25279
+ rowEls.set(flatIndex, r);
25280
+ onCleanup(() => {
25281
+ if (rowEls.get(flatIndex) === r)
25282
+ rowEls.delete(flatIndex);
25283
+ });
25284
+ }, _el$38);
24532
25285
  setProp(_el$38, "flexDirection", "column");
24533
25286
  setProp(_el$38, "gap", 0);
24534
25287
  setProp(_el$38, "onMouseUp", () => {
@@ -24565,12 +25318,12 @@ function Sidebar(props) {
24565
25318
  setProp(_el$43, "flexGrow", 1);
24566
25319
  insert(_el$43, () => spacedTitle(rowView().titleText, titleBudget()));
24567
25320
  effect((_p$) => {
24568
- var _v$24 = barColor(), _v$25 = stateColor(), _v$26 = TextAttributes11.BOLD, _v$27 = theme.text, _v$28 = TextAttributes11.BOLD;
24569
- _v$24 !== _p$.e && (_p$.e = setProp(_el$40, "fg", _v$24, _p$.e));
24570
- _v$25 !== _p$.t && (_p$.t = setProp(_el$42, "fg", _v$25, _p$.t));
24571
- _v$26 !== _p$.a && (_p$.a = setProp(_el$42, "attributes", _v$26, _p$.a));
24572
- _v$27 !== _p$.o && (_p$.o = setProp(_el$43, "fg", _v$27, _p$.o));
24573
- _v$28 !== _p$.i && (_p$.i = setProp(_el$43, "attributes", _v$28, _p$.i));
25321
+ var _v$23 = barColor(), _v$24 = stateColor(), _v$25 = TextAttributes11.BOLD, _v$26 = theme.text, _v$27 = TextAttributes11.BOLD;
25322
+ _v$23 !== _p$.e && (_p$.e = setProp(_el$40, "fg", _v$23, _p$.e));
25323
+ _v$24 !== _p$.t && (_p$.t = setProp(_el$42, "fg", _v$24, _p$.t));
25324
+ _v$25 !== _p$.a && (_p$.a = setProp(_el$42, "attributes", _v$25, _p$.a));
25325
+ _v$26 !== _p$.o && (_p$.o = setProp(_el$43, "fg", _v$26, _p$.o));
25326
+ _v$27 !== _p$.i && (_p$.i = setProp(_el$43, "attributes", _v$27, _p$.i));
24574
25327
  return _p$;
24575
25328
  }, {
24576
25329
  e: undefined,
@@ -24624,10 +25377,10 @@ function Sidebar(props) {
24624
25377
  }
24625
25378
  }), null);
24626
25379
  effect((_p$) => {
24627
- var _v$29 = barColor(), _v$30 = theme.textMuted, _v$31 = TextAttributes11.DIM;
24628
- _v$29 !== _p$.e && (_p$.e = setProp(_el$45, "fg", _v$29, _p$.e));
24629
- _v$30 !== _p$.t && (_p$.t = setProp(_el$47, "fg", _v$30, _p$.t));
24630
- _v$31 !== _p$.a && (_p$.a = setProp(_el$47, "attributes", _v$31, _p$.a));
25380
+ var _v$28 = barColor(), _v$29 = theme.textMuted, _v$30 = TextAttributes11.DIM;
25381
+ _v$28 !== _p$.e && (_p$.e = setProp(_el$45, "fg", _v$28, _p$.e));
25382
+ _v$29 !== _p$.t && (_p$.t = setProp(_el$47, "fg", _v$29, _p$.t));
25383
+ _v$30 !== _p$.a && (_p$.a = setProp(_el$47, "attributes", _v$30, _p$.a));
24631
25384
  return _p$;
24632
25385
  }, {
24633
25386
  e: undefined,
@@ -24673,12 +25426,12 @@ function Sidebar(props) {
24673
25426
  }
24674
25427
  }), null);
24675
25428
  effect((_p$) => {
24676
- var _v$32 = barColor(), _v$33 = stateColor(), _v$34 = TextAttributes11.BOLD, _v$35 = theme.text, _v$36 = isSelected() || isCursor() ? TextAttributes11.BOLD : undefined;
24677
- _v$32 !== _p$.e && (_p$.e = setProp(_el$53, "fg", _v$32, _p$.e));
24678
- _v$33 !== _p$.t && (_p$.t = setProp(_el$55, "fg", _v$33, _p$.t));
24679
- _v$34 !== _p$.a && (_p$.a = setProp(_el$55, "attributes", _v$34, _p$.a));
24680
- _v$35 !== _p$.o && (_p$.o = setProp(_el$56, "fg", _v$35, _p$.o));
24681
- _v$36 !== _p$.i && (_p$.i = setProp(_el$56, "attributes", _v$36, _p$.i));
25429
+ var _v$31 = barColor(), _v$32 = stateColor(), _v$33 = TextAttributes11.BOLD, _v$34 = theme.text, _v$35 = isSelected() || isCursor() ? TextAttributes11.BOLD : undefined;
25430
+ _v$31 !== _p$.e && (_p$.e = setProp(_el$53, "fg", _v$31, _p$.e));
25431
+ _v$32 !== _p$.t && (_p$.t = setProp(_el$55, "fg", _v$32, _p$.t));
25432
+ _v$33 !== _p$.a && (_p$.a = setProp(_el$55, "attributes", _v$33, _p$.a));
25433
+ _v$34 !== _p$.o && (_p$.o = setProp(_el$56, "fg", _v$34, _p$.o));
25434
+ _v$35 !== _p$.i && (_p$.i = setProp(_el$56, "attributes", _v$35, _p$.i));
24682
25435
  return _p$;
24683
25436
  }, {
24684
25437
  e: undefined,
@@ -24744,10 +25497,10 @@ function Sidebar(props) {
24744
25497
  }
24745
25498
  }), null);
24746
25499
  effect((_p$) => {
24747
- var _v$37 = barColor(), _v$38 = theme.textMuted, _v$39 = TextAttributes11.DIM;
24748
- _v$37 !== _p$.e && (_p$.e = setProp(_el$60, "fg", _v$37, _p$.e));
24749
- _v$38 !== _p$.t && (_p$.t = setProp(_el$62, "fg", _v$38, _p$.t));
24750
- _v$39 !== _p$.a && (_p$.a = setProp(_el$62, "attributes", _v$39, _p$.a));
25500
+ var _v$36 = barColor(), _v$37 = theme.textMuted, _v$38 = TextAttributes11.DIM;
25501
+ _v$36 !== _p$.e && (_p$.e = setProp(_el$60, "fg", _v$36, _p$.e));
25502
+ _v$37 !== _p$.t && (_p$.t = setProp(_el$62, "fg", _v$37, _p$.t));
25503
+ _v$38 !== _p$.a && (_p$.a = setProp(_el$62, "attributes", _v$38, _p$.a));
24751
25504
  return _p$;
24752
25505
  }, {
24753
25506
  e: undefined,
@@ -24852,9 +25605,9 @@ function Sidebar(props) {
24852
25605
  return () => _c$2() ? truncatePathTail(l.text, innerW()) : truncateTitle(l.text, innerW());
24853
25606
  })());
24854
25607
  effect((_p$) => {
24855
- var _v$45 = l.dim ? theme.textMuted : theme.text, _v$46 = l.bold ? TextAttributes11.BOLD : l.dim ? TextAttributes11.DIM : undefined;
24856
- _v$45 !== _p$.e && (_p$.e = setProp(_el$70, "fg", _v$45, _p$.e));
24857
- _v$46 !== _p$.t && (_p$.t = setProp(_el$70, "attributes", _v$46, _p$.t));
25608
+ var _v$44 = l.dim ? theme.textMuted : theme.text, _v$45 = l.bold ? TextAttributes11.BOLD : l.dim ? TextAttributes11.DIM : undefined;
25609
+ _v$44 !== _p$.e && (_p$.e = setProp(_el$70, "fg", _v$44, _p$.e));
25610
+ _v$45 !== _p$.t && (_p$.t = setProp(_el$70, "attributes", _v$45, _p$.t));
24858
25611
  return _p$;
24859
25612
  }, {
24860
25613
  e: undefined,
@@ -24864,12 +25617,12 @@ function Sidebar(props) {
24864
25617
  })()
24865
25618
  }));
24866
25619
  effect((_p$) => {
24867
- var _v$40 = left(), _v$41 = top(), _v$42 = boxW(), _v$43 = theme.focusAccent, _v$44 = theme.backgroundElement;
24868
- _v$40 !== _p$.e && (_p$.e = setProp(_el$69, "left", _v$40, _p$.e));
24869
- _v$41 !== _p$.t && (_p$.t = setProp(_el$69, "top", _v$41, _p$.t));
24870
- _v$42 !== _p$.a && (_p$.a = setProp(_el$69, "width", _v$42, _p$.a));
24871
- _v$43 !== _p$.o && (_p$.o = setProp(_el$69, "borderColor", _v$43, _p$.o));
24872
- _v$44 !== _p$.i && (_p$.i = setProp(_el$69, "backgroundColor", _v$44, _p$.i));
25620
+ var _v$39 = left(), _v$40 = top(), _v$41 = boxW(), _v$42 = theme.focusAccent, _v$43 = theme.backgroundElement;
25621
+ _v$39 !== _p$.e && (_p$.e = setProp(_el$69, "left", _v$39, _p$.e));
25622
+ _v$40 !== _p$.t && (_p$.t = setProp(_el$69, "top", _v$40, _p$.t));
25623
+ _v$41 !== _p$.a && (_p$.a = setProp(_el$69, "width", _v$41, _p$.a));
25624
+ _v$42 !== _p$.o && (_p$.o = setProp(_el$69, "borderColor", _v$42, _p$.o));
25625
+ _v$43 !== _p$.i && (_p$.i = setProp(_el$69, "backgroundColor", _v$43, _p$.i));
24873
25626
  return _p$;
24874
25627
  }, {
24875
25628
  e: undefined,
@@ -24883,19 +25636,17 @@ function Sidebar(props) {
24883
25636
  }
24884
25637
  }), null);
24885
25638
  effect((_p$) => {
24886
- var _v$15 = props.width ? props.width() : SIDEBAR_WIDTH, _v$16 = focusedAccessor() ? theme.focusAccent : theme.textMuted, _v$17 = TextAttributes11.BOLD, _v$18 = theme.textMuted, _v$19 = TextAttributes11.DIM;
24887
- _v$15 !== _p$.e && (_p$.e = setProp(_el$5, "width", _v$15, _p$.e));
24888
- _v$16 !== _p$.t && (_p$.t = setProp(_el$8, "fg", _v$16, _p$.t));
24889
- _v$17 !== _p$.a && (_p$.a = setProp(_el$8, "attributes", _v$17, _p$.a));
24890
- _v$18 !== _p$.o && (_p$.o = setProp(_el$24, "fg", _v$18, _p$.o));
24891
- _v$19 !== _p$.i && (_p$.i = setProp(_el$24, "attributes", _v$19, _p$.i));
25639
+ var _v$15 = focusedAccessor() ? theme.focusAccent : theme.textMuted, _v$16 = TextAttributes11.BOLD, _v$17 = theme.textMuted, _v$18 = TextAttributes11.DIM;
25640
+ _v$15 !== _p$.e && (_p$.e = setProp(_el$8, "fg", _v$15, _p$.e));
25641
+ _v$16 !== _p$.t && (_p$.t = setProp(_el$8, "attributes", _v$16, _p$.t));
25642
+ _v$17 !== _p$.a && (_p$.a = setProp(_el$24, "fg", _v$17, _p$.a));
25643
+ _v$18 !== _p$.o && (_p$.o = setProp(_el$24, "attributes", _v$18, _p$.o));
24892
25644
  return _p$;
24893
25645
  }, {
24894
25646
  e: undefined,
24895
25647
  t: undefined,
24896
25648
  a: undefined,
24897
- o: undefined,
24898
- i: undefined
25649
+ o: undefined
24899
25650
  });
24900
25651
  return _el$5;
24901
25652
  })();
@@ -24911,6 +25662,7 @@ var init_Sidebar = __esm(() => {
24911
25662
  init_solid();
24912
25663
  init_solid();
24913
25664
  init_solid();
25665
+ init_solid();
24914
25666
  init_dev();
24915
25667
  init_theme2();
24916
25668
  init_theme2();
@@ -24937,7 +25689,7 @@ __export(exports_host3, {
24937
25689
  legendCap: () => legendCap
24938
25690
  });
24939
25691
  import { existsSync as existsSync16 } from "fs";
24940
- import { stat as stat5 } from "fs/promises";
25692
+ import { stat as stat6 } from "fs/promises";
24941
25693
  import { TextAttributes as TextAttributes12 } from "@opentui/core";
24942
25694
  function worktreeCwdUsable(cwd) {
24943
25695
  return !!cwd && worktreeUsable(cwd);
@@ -25201,6 +25953,7 @@ function TasksShell(props) {
25201
25953
  initPrompt: init3.initPrompt
25202
25954
  });
25203
25955
  }
25956
+ await prepareWindowForSwitch(name);
25204
25957
  await runTmux(["switch-client", "-t", `=${name}`]);
25205
25958
  props.orch?.setActiveTask(id).catch(() => {});
25206
25959
  return;
@@ -25239,6 +25992,7 @@ function TasksShell(props) {
25239
25992
  notifyError("Couldn't start this task's session");
25240
25993
  return;
25241
25994
  }
25995
+ await prepareWindowForSwitch(name);
25242
25996
  await runTmux(["switch-client", "-t", `=${name}`]);
25243
25997
  props.orch?.setActiveTask(id).catch(() => {});
25244
25998
  }
@@ -25373,52 +26127,6 @@ function ShortcutHints(props) {
25373
26127
  label: "move panes"
25374
26128
  });
25375
26129
  }
25376
- const prev = b["tmux.tab.prev"];
25377
- const next = b["tmux.tab.next"];
25378
- if (prev?.chord === "ctrl+[" && next?.chord === "ctrl+]") {
25379
- out.push({
25380
- k: "ctrl+[/]",
25381
- label: "switch tabs"
25382
- });
25383
- } else {
25384
- if (prev)
25385
- out.push({
25386
- k: prev.chord,
25387
- label: "prev tab"
25388
- });
25389
- if (next)
25390
- out.push({
25391
- k: next.chord,
25392
- label: "next tab"
25393
- });
25394
- }
25395
- if (b["tmux.tab.new"])
25396
- out.push({
25397
- k: b["tmux.tab.new"].chord,
25398
- label: "new tab"
25399
- });
25400
- if (b["tmux.tab.chooseEngine"])
25401
- out.push({
25402
- k: b["tmux.tab.chooseEngine"].chord,
25403
- label: "engine tab"
25404
- });
25405
- out.push({
25406
- k: "prefix t",
25407
- label: "engine tab"
25408
- }, {
25409
- k: "prefix f",
25410
- label: "new task"
25411
- });
25412
- if (b["tmux.tab.rename"])
25413
- out.push({
25414
- k: b["tmux.tab.rename"].chord,
25415
- label: "rename tab"
25416
- });
25417
- if (b["tmux.tab.close"])
25418
- out.push({
25419
- k: b["tmux.tab.close"].chord,
25420
- label: "close tab"
25421
- });
25422
26130
  if (b["tmux.detach"])
25423
26131
  out.push({
25424
26132
  k: b["tmux.detach"].chord,
@@ -25430,12 +26138,8 @@ function ShortcutHints(props) {
25430
26138
  keymapVersion();
25431
26139
  const rows = [
25432
26140
  {
25433
- ids: ["sidebar.select"],
25434
- label: "open"
25435
- },
25436
- {
25437
- ids: ["tasks.focusEngine"],
25438
- label: "focus engine"
26141
+ ids: ["help.open"],
26142
+ label: "full help"
25439
26143
  },
25440
26144
  {
25441
26145
  ids: ["task.new"],
@@ -25446,34 +26150,24 @@ function ShortcutHints(props) {
25446
26150
  label: "settings"
25447
26151
  },
25448
26152
  {
25449
- ids: ["tasks.openWorktree"],
25450
- label: "open wt"
25451
- },
25452
- {
25453
- ids: ["sidebar.view"],
25454
- label: "views"
25455
- },
25456
- {
25457
- ids: ["sidebar.sort"],
25458
- label: "sort"
26153
+ ids: ["sidebar.select"],
26154
+ label: "open"
25459
26155
  },
25460
26156
  {
25461
- ids: ["sidebar.localMerge"],
25462
- label: "move task",
25463
- dimWhenMain: true
26157
+ ids: ["tasks.focusEngine"],
26158
+ label: "focus engine"
25464
26159
  },
25465
26160
  {
25466
- ids: ["sidebar.archive", "sidebar.delete"],
25467
- label: "un/archive\xB7delete"
26161
+ ids: ["tasks.openWorktree"],
26162
+ label: "open wt"
25468
26163
  },
25469
26164
  {
25470
- ids: ["sidebar.rename", "tasks.renameBranch", "tasks.cycleEngine"],
25471
- label: "name/branch/engine",
25472
- dimWhenMain: true
26165
+ ids: ["sidebar.delete"],
26166
+ label: "delete"
25473
26167
  },
25474
26168
  {
25475
- ids: ["help.open"],
25476
- label: "help"
26169
+ ids: ["sidebar.view"],
26170
+ label: "views"
25477
26171
  }
25478
26172
  ];
25479
26173
  const out = [];
@@ -25617,7 +26311,7 @@ async function setupTasksPane(opts) {
25617
26311
  (async () => {
25618
26312
  let fingerprint = "missing";
25619
26313
  try {
25620
- const st = await stat5(store2.filePath);
26314
+ const st = await stat6(store2.filePath);
25621
26315
  fingerprint = `${st.mtimeMs}:${st.size}`;
25622
26316
  } catch {}
25623
26317
  if (fingerprint === lastTasksFileFingerprint)
@@ -25889,18 +26583,18 @@ function openExternalUrl(url) {
25889
26583
  function releaseBodyLines(body) {
25890
26584
  return body.replace(/\r\n/g, `
25891
26585
  `).split(`
25892
- `).map((line) => line.trim()).filter(Boolean).slice(0, 40);
26586
+ `).map((line) => line.trim()).filter(Boolean);
25893
26587
  }
25894
26588
  function waitForKeypress() {
25895
26589
  if (!process.stdin.isTTY)
25896
26590
  return Promise.resolve();
25897
- return new Promise((resolve9) => {
26591
+ return new Promise((resolve11) => {
25898
26592
  const stdin = process.stdin;
25899
26593
  const done = () => {
25900
26594
  stdin.off("data", done);
25901
26595
  stdin.setRawMode?.(false);
25902
26596
  stdin.pause();
25903
- resolve9();
26597
+ resolve11();
25904
26598
  };
25905
26599
  stdin.setRawMode?.(true);
25906
26600
  stdin.resume();
@@ -25913,13 +26607,12 @@ function UpdatePage() {
25913
26607
  } = useTheme();
25914
26608
  const renderer = useRenderer();
25915
26609
  const [info, setInfo] = createSignal(null);
25916
- const [notes, setNotes] = createSignal(null);
26610
+ const [releaseNotes, setReleaseNotes] = createSignal([]);
25917
26611
  const [loadingNotes, setLoadingNotes] = createSignal(true);
25918
26612
  const [selected, setSelected] = createSignal("update");
25919
26613
  const [status, setStatus] = createSignal(null);
25920
26614
  const latest = createMemo(() => info()?.latest ?? CURRENT_VERSION);
25921
- const releaseUrl = createMemo(() => notes()?.url ?? releasePageUrl(latest()));
25922
- const lines = createMemo(() => releaseBodyLines(notes()?.body ?? ""));
26615
+ const releaseUrl = createMemo(() => releaseNotes()[0]?.url ?? releasePageUrl(latest()));
25923
26616
  const actions = createMemo(() => [{
25924
26617
  id: "update",
25925
26618
  key: "U",
@@ -25944,9 +26637,12 @@ function UpdatePage() {
25944
26637
  force: true
25945
26638
  });
25946
26639
  setInfo(next);
25947
- const version = next?.latest ?? CURRENT_VERSION;
25948
- const fetched = await fetchReleaseNotes(version);
25949
- setNotes(fetched);
26640
+ const latestVersion = next?.latest ?? CURRENT_VERSION;
26641
+ const fetched = await fetchReleaseNotesRange({
26642
+ current: CURRENT_VERSION,
26643
+ latest: latestVersion
26644
+ });
26645
+ setReleaseNotes(fetched);
25950
26646
  setLoadingNotes(false);
25951
26647
  }
25952
26648
  function move(delta) {
@@ -25966,7 +26662,7 @@ function UpdatePage() {
25966
26662
  }
25967
26663
  async function runUpdater() {
25968
26664
  setStatus("Leaving the TUI page and running the updater in this tmux window...");
25969
- await new Promise((resolve9) => setTimeout(resolve9, 30));
26665
+ await new Promise((resolve11) => setTimeout(resolve11, 30));
25970
26666
  renderer?.destroy();
25971
26667
  process.stdout.write(`
25972
26668
  kobe ${CURRENT_VERSION} -> latest
@@ -26025,12 +26721,12 @@ kobe update failed with exit code ${code}.
26025
26721
  }]
26026
26722
  }));
26027
26723
  return (() => {
26028
- var _el$ = createElement("box"), _el$2 = createElement("box"), _el$3 = createElement("text"), _el$5 = createElement("text"), _el$7 = createElement("box"), _el$8 = createElement("text"), _el$0 = createElement("text"), _el$1 = createTextNode(`v`), _el$10 = createElement("text"), _el$12 = createElement("text"), _el$13 = createTextNode(`v`), _el$14 = createElement("box"), _el$16 = createElement("box"), _el$17 = createElement("text"), _el$19 = createElement("scrollbox"), _el$20 = createElement("box");
26724
+ var _el$ = createElement("box"), _el$2 = createElement("box"), _el$3 = createElement("text"), _el$5 = createElement("text"), _el$7 = createElement("box"), _el$8 = createElement("text"), _el$0 = createElement("text"), _el$1 = createTextNode(`v`), _el$10 = createElement("text"), _el$12 = createElement("text"), _el$13 = createTextNode(`v`), _el$14 = createElement("box"), _el$16 = createElement("box"), _el$17 = createElement("text"), _el$18 = createTextNode(`\u2500\u2500 changes from v`), _el$19 = createTextNode(` to v`), _el$20 = createTextNode(` \u2500\u2500`), _el$21 = createElement("scrollbox"), _el$22 = createElement("box");
26029
26725
  insertNode(_el$, _el$2);
26030
26726
  insertNode(_el$, _el$7);
26031
26727
  insertNode(_el$, _el$14);
26032
26728
  insertNode(_el$, _el$16);
26033
- insertNode(_el$, _el$19);
26729
+ insertNode(_el$, _el$21);
26034
26730
  setProp(_el$, "flexDirection", "column");
26035
26731
  setProp(_el$, "flexGrow", 1);
26036
26732
  setProp(_el$, "paddingTop", 1);
@@ -26073,36 +26769,36 @@ kobe update failed with exit code ${code}.
26073
26769
  return actions();
26074
26770
  },
26075
26771
  children: (action) => (() => {
26076
- var _el$25 = createElement("box"), _el$26 = createElement("box"), _el$27 = createElement("text"), _el$28 = createTextNode(`[`), _el$29 = createTextNode(`]`), _el$30 = createElement("box"), _el$31 = createElement("text"), _el$32 = createElement("text");
26077
- insertNode(_el$25, _el$26);
26078
- insertNode(_el$25, _el$30);
26079
- insertNode(_el$25, _el$32);
26080
- setProp(_el$25, "flexDirection", "row");
26081
- setProp(_el$25, "gap", 1);
26082
- setProp(_el$25, "paddingLeft", 1);
26083
- setProp(_el$25, "paddingRight", 1);
26084
- setProp(_el$25, "onMouseUp", () => activate(action.id));
26085
- insertNode(_el$26, _el$27);
26086
- setProp(_el$26, "width", 4);
26087
- setProp(_el$26, "flexShrink", 0);
26772
+ var _el$27 = createElement("box"), _el$28 = createElement("box"), _el$29 = createElement("text"), _el$30 = createTextNode(`[`), _el$31 = createTextNode(`]`), _el$32 = createElement("box"), _el$33 = createElement("text"), _el$34 = createElement("text");
26088
26773
  insertNode(_el$27, _el$28);
26089
- insertNode(_el$27, _el$29);
26090
- setProp(_el$27, "wrapMode", "none");
26091
- insert(_el$27, () => action.key, _el$29);
26092
- insertNode(_el$30, _el$31);
26093
- setProp(_el$30, "width", 14);
26094
- setProp(_el$30, "flexShrink", 0);
26095
- setProp(_el$31, "wrapMode", "none");
26096
- insert(_el$31, () => action.label);
26097
- setProp(_el$32, "wrapMode", "word");
26098
- insert(_el$32, () => action.detail);
26774
+ insertNode(_el$27, _el$32);
26775
+ insertNode(_el$27, _el$34);
26776
+ setProp(_el$27, "flexDirection", "row");
26777
+ setProp(_el$27, "gap", 1);
26778
+ setProp(_el$27, "paddingLeft", 1);
26779
+ setProp(_el$27, "paddingRight", 1);
26780
+ setProp(_el$27, "onMouseUp", () => activate(action.id));
26781
+ insertNode(_el$28, _el$29);
26782
+ setProp(_el$28, "width", 4);
26783
+ setProp(_el$28, "flexShrink", 0);
26784
+ insertNode(_el$29, _el$30);
26785
+ insertNode(_el$29, _el$31);
26786
+ setProp(_el$29, "wrapMode", "none");
26787
+ insert(_el$29, () => action.key, _el$31);
26788
+ insertNode(_el$32, _el$33);
26789
+ setProp(_el$32, "width", 14);
26790
+ setProp(_el$32, "flexShrink", 0);
26791
+ setProp(_el$33, "wrapMode", "none");
26792
+ insert(_el$33, () => action.label);
26793
+ setProp(_el$34, "wrapMode", "word");
26794
+ insert(_el$34, () => action.detail);
26099
26795
  effect((_p$) => {
26100
26796
  var _v$12 = selected() === action.id ? theme.primary : undefined, _v$13 = selected() === action.id ? theme.selectedListItemText : theme.accent, _v$14 = TextAttributes13.BOLD, _v$15 = selected() === action.id ? theme.selectedListItemText : theme.text, _v$16 = selected() === action.id ? theme.selectedListItemText : theme.textMuted;
26101
- _v$12 !== _p$.e && (_p$.e = setProp(_el$25, "backgroundColor", _v$12, _p$.e));
26102
- _v$13 !== _p$.t && (_p$.t = setProp(_el$27, "fg", _v$13, _p$.t));
26103
- _v$14 !== _p$.a && (_p$.a = setProp(_el$27, "attributes", _v$14, _p$.a));
26104
- _v$15 !== _p$.o && (_p$.o = setProp(_el$31, "fg", _v$15, _p$.o));
26105
- _v$16 !== _p$.i && (_p$.i = setProp(_el$32, "fg", _v$16, _p$.i));
26797
+ _v$12 !== _p$.e && (_p$.e = setProp(_el$27, "backgroundColor", _v$12, _p$.e));
26798
+ _v$13 !== _p$.t && (_p$.t = setProp(_el$29, "fg", _v$13, _p$.t));
26799
+ _v$14 !== _p$.a && (_p$.a = setProp(_el$29, "attributes", _v$14, _p$.a));
26800
+ _v$15 !== _p$.o && (_p$.o = setProp(_el$33, "fg", _v$15, _p$.o));
26801
+ _v$16 !== _p$.i && (_p$.i = setProp(_el$34, "fg", _v$16, _p$.i));
26106
26802
  return _p$;
26107
26803
  }, {
26108
26804
  e: undefined,
@@ -26111,7 +26807,7 @@ kobe update failed with exit code ${code}.
26111
26807
  o: undefined,
26112
26808
  i: undefined
26113
26809
  });
26114
- return _el$25;
26810
+ return _el$27;
26115
26811
  })()
26116
26812
  }));
26117
26813
  insert(_el$, createComponent2(Show, {
@@ -26129,49 +26825,78 @@ kobe update failed with exit code ${code}.
26129
26825
  insertNode(_el$16, _el$17);
26130
26826
  setProp(_el$16, "flexShrink", 0);
26131
26827
  setProp(_el$16, "paddingTop", 1);
26132
- insertNode(_el$17, createTextNode(`\u2500\u2500 release notes \u2500\u2500`));
26828
+ insertNode(_el$17, _el$18);
26829
+ insertNode(_el$17, _el$19);
26830
+ insertNode(_el$17, _el$20);
26133
26831
  setProp(_el$17, "wrapMode", "none");
26134
- insertNode(_el$19, _el$20);
26135
- setProp(_el$19, "flexGrow", 1);
26136
- setProp(_el$19, "flexShrink", 1);
26137
- setProp(_el$19, "stickyScroll", false);
26138
- setProp(_el$20, "flexDirection", "column");
26139
- setProp(_el$20, "paddingRight", 1);
26140
- setProp(_el$20, "paddingBottom", 1);
26141
- setProp(_el$20, "gap", 0);
26142
- insert(_el$20, createComponent2(Show, {
26832
+ insert(_el$17, CURRENT_VERSION, _el$19);
26833
+ insert(_el$17, latest, _el$20);
26834
+ insertNode(_el$21, _el$22);
26835
+ setProp(_el$21, "flexGrow", 1);
26836
+ setProp(_el$21, "flexShrink", 1);
26837
+ setProp(_el$21, "stickyScroll", false);
26838
+ setProp(_el$22, "flexDirection", "column");
26839
+ setProp(_el$22, "paddingRight", 1);
26840
+ setProp(_el$22, "paddingBottom", 1);
26841
+ setProp(_el$22, "gap", 0);
26842
+ insert(_el$22, createComponent2(Show, {
26143
26843
  get when() {
26144
26844
  return loadingNotes();
26145
26845
  },
26146
26846
  get children() {
26147
- var _el$21 = createElement("text");
26148
- insertNode(_el$21, createTextNode(`Loading release notes...`));
26149
- effect((_$p) => setProp(_el$21, "fg", theme.textMuted, _$p));
26150
- return _el$21;
26847
+ var _el$23 = createElement("text");
26848
+ insertNode(_el$23, createTextNode(`Loading release notes...`));
26849
+ effect((_$p) => setProp(_el$23, "fg", theme.textMuted, _$p));
26850
+ return _el$23;
26151
26851
  }
26152
26852
  }), null);
26153
- insert(_el$20, createComponent2(Show, {
26853
+ insert(_el$22, createComponent2(Show, {
26154
26854
  get when() {
26155
- return memo2(() => !!!loadingNotes())() && lines().length === 0;
26855
+ return memo2(() => !!!loadingNotes())() && releaseNotes().length === 0;
26156
26856
  },
26157
26857
  get children() {
26158
- var _el$23 = createElement("text");
26159
- insertNode(_el$23, createTextNode(`Release notes are unavailable. Use Open release to view the GitHub release page.`));
26160
- setProp(_el$23, "wrapMode", "word");
26161
- effect((_$p) => setProp(_el$23, "fg", theme.textMuted, _$p));
26162
- return _el$23;
26858
+ var _el$25 = createElement("text");
26859
+ insertNode(_el$25, createTextNode(`Release notes are unavailable. Use Open release to view the GitHub release page.`));
26860
+ setProp(_el$25, "wrapMode", "word");
26861
+ effect((_$p) => setProp(_el$25, "fg", theme.textMuted, _$p));
26862
+ return _el$25;
26163
26863
  }
26164
26864
  }), null);
26165
- insert(_el$20, createComponent2(For, {
26865
+ insert(_el$22, createComponent2(For, {
26166
26866
  get each() {
26167
- return lines();
26168
- },
26169
- children: (line) => (() => {
26170
- var _el$33 = createElement("text");
26171
- setProp(_el$33, "wrapMode", "word");
26172
- insert(_el$33, line);
26173
- effect((_$p) => setProp(_el$33, "fg", theme.textMuted, _$p));
26174
- return _el$33;
26867
+ return releaseNotes();
26868
+ },
26869
+ children: (release) => (() => {
26870
+ var _el$35 = createElement("box"), _el$36 = createElement("text"), _el$37 = createTextNode(`v`);
26871
+ insertNode(_el$35, _el$36);
26872
+ setProp(_el$35, "flexDirection", "column");
26873
+ setProp(_el$35, "paddingBottom", 1);
26874
+ setProp(_el$35, "gap", 0);
26875
+ insertNode(_el$36, _el$37);
26876
+ setProp(_el$36, "wrapMode", "none");
26877
+ insert(_el$36, () => release.version, null);
26878
+ insert(_el$35, createComponent2(For, {
26879
+ get each() {
26880
+ return releaseBodyLines(release.body);
26881
+ },
26882
+ children: (line) => (() => {
26883
+ var _el$38 = createElement("text");
26884
+ setProp(_el$38, "wrapMode", "word");
26885
+ insert(_el$38, line);
26886
+ effect((_$p) => setProp(_el$38, "fg", theme.textMuted, _$p));
26887
+ return _el$38;
26888
+ })()
26889
+ }), null);
26890
+ effect((_p$) => {
26891
+ var _v$17 = theme.text, _v$18 = TextAttributes13.BOLD;
26892
+ _v$17 !== _p$.e && (_p$.e = setProp(_el$36, "fg", _v$17, _p$.e));
26893
+ _v$18 !== _p$.t && (_p$.t = setProp(_el$36, "attributes", _v$18, _p$.t));
26894
+ return _p$;
26895
+ }, {
26896
+ e: undefined,
26897
+ t: undefined
26898
+ });
26899
+ return _el$35;
26175
26900
  })()
26176
26901
  }), null);
26177
26902
  effect((_p$) => {
@@ -26193,7 +26918,7 @@ kobe update failed with exit code ${code}.
26193
26918
  _v$0 !== _p$.d && (_p$.d = setProp(_el$12, "attributes", _v$0, _p$.d));
26194
26919
  _v$1 !== _p$.l && (_p$.l = setProp(_el$17, "fg", _v$1, _p$.l));
26195
26920
  _v$10 !== _p$.u && (_p$.u = setProp(_el$17, "attributes", _v$10, _p$.u));
26196
- _v$11 !== _p$.c && (_p$.c = setProp(_el$19, "verticalScrollbarOptions", _v$11, _p$.c));
26921
+ _v$11 !== _p$.c && (_p$.c = setProp(_el$21, "verticalScrollbarOptions", _v$11, _p$.c));
26197
26922
  return _p$;
26198
26923
  }, {
26199
26924
  e: undefined,
@@ -26513,7 +27238,7 @@ var gitWrapper;
26513
27238
  var init_git2 = __esm(() => {
26514
27239
  gitWrapper = {
26515
27240
  spawn(args2, cwd) {
26516
- return new Promise((resolve9, reject) => {
27241
+ return new Promise((resolve11, reject) => {
26517
27242
  const child = nodeSpawn("git", [...args2], {
26518
27243
  cwd,
26519
27244
  shell: false,
@@ -26532,7 +27257,7 @@ var init_git2 = __esm(() => {
26532
27257
  });
26533
27258
  child.on("error", reject);
26534
27259
  child.on("close", (status, signal) => {
26535
- resolve9({ stdout, stderr, status, signal });
27260
+ resolve11({ stdout, stderr, status, signal });
26536
27261
  });
26537
27262
  });
26538
27263
  }
@@ -27471,7 +28196,7 @@ var init_filetree = __esm(() => {
27471
28196
  // src/tui/ops/pr-prompt.ts
27472
28197
  import { promises as fs6 } from "fs";
27473
28198
  import path12 from "path";
27474
- async function git2(cwd, args2) {
28199
+ async function git(cwd, args2) {
27475
28200
  const controller = new AbortController;
27476
28201
  const timer = setTimeout(() => controller.abort(), GIT_TIMEOUT_MS2);
27477
28202
  try {
@@ -27490,20 +28215,20 @@ async function git2(cwd, args2) {
27490
28215
  }
27491
28216
  }
27492
28217
  async function currentBranch2(cwd) {
27493
- return await git2(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD";
28218
+ return await git(cwd, ["rev-parse", "--abbrev-ref", "HEAD"]) || "HEAD";
27494
28219
  }
27495
28220
  async function targetBranch(cwd) {
27496
- const out = await git2(cwd, ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"]);
28221
+ const out = await git(cwd, ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"]);
27497
28222
  if (!out)
27498
28223
  return "main";
27499
28224
  return out.startsWith("origin/") ? out.slice("origin/".length) : out;
27500
28225
  }
27501
28226
  async function hasUpstream(cwd) {
27502
- const out = await git2(cwd, ["rev-parse", "--abbrev-ref", "@{u}"]);
28227
+ const out = await git(cwd, ["rev-parse", "--abbrev-ref", "@{u}"]);
27503
28228
  return out !== null && out.length > 0;
27504
28229
  }
27505
28230
  async function dirtyCount(cwd) {
27506
- const out = await git2(cwd, ["status", "--porcelain"]);
28231
+ const out = await git(cwd, ["status", "--porcelain"]);
27507
28232
  if (!out)
27508
28233
  return 0;
27509
28234
  return out.split(`
@@ -27584,7 +28309,7 @@ __export(exports_host7, {
27584
28309
  startOpsHost: () => startOpsHost,
27585
28310
  nextActivityPollDelay: () => nextActivityPollDelay
27586
28311
  });
27587
- import { createHash as createHash5 } from "crypto";
28312
+ import { createHash as createHash7 } from "crypto";
27588
28313
  import { SyntaxStyle } from "@opentui/core";
27589
28314
  function nextActivityPollDelay(currentMs, idleStreak) {
27590
28315
  if (idleStreak < ACTIVITY_IDLE_RAMP_POLLS)
@@ -27757,7 +28482,7 @@ function basename8(p) {
27757
28482
  return i >= 0 ? p.slice(i + 1) : p;
27758
28483
  }
27759
28484
  function fingerprint(text) {
27760
- return createHash5("sha1").update(text).digest("hex");
28485
+ return createHash7("sha1").update(text).digest("hex");
27761
28486
  }
27762
28487
  async function startOpsHost(args2) {
27763
28488
  await bootPaneHost({
@@ -28025,7 +28750,7 @@ __export(exports_direct, {
28025
28750
  startDirectTmux: () => startDirectTmux,
28026
28751
  chooseInitialTask: () => chooseInitialTask
28027
28752
  });
28028
- import { resolve as resolve9 } from "path";
28753
+ import { resolve as resolve11 } from "path";
28029
28754
  function chooseInitialTask(tasks, choice = {}) {
28030
28755
  const byId = (id) => id ? tasks.find((t) => t.id === id) : undefined;
28031
28756
  const active = byId(choice.activeTaskId);
@@ -28061,7 +28786,7 @@ async function ensureRepos(orchestrator) {
28061
28786
  normalizeSavedRepos();
28062
28787
  let repos = [...getSavedRepos()];
28063
28788
  if (repos.length === 0) {
28064
- const added = addSavedRepo(resolve9(process.cwd()));
28789
+ const added = addSavedRepo(resolve11(process.cwd()));
28065
28790
  repos = [added.path];
28066
28791
  }
28067
28792
  for (const repo of repos) {
@@ -28071,7 +28796,7 @@ async function ensureRepos(orchestrator) {
28071
28796
  console.error(`[kobe] ensureMainTask failed for ${repo}:`, err);
28072
28797
  }
28073
28798
  }
28074
- return repos[0] ?? resolve9(process.cwd());
28799
+ return repos[0] ?? resolve11(process.cwd());
28075
28800
  }
28076
28801
  async function startDirectTmux() {
28077
28802
  setClientLogContext("gui");
@@ -28182,7 +28907,7 @@ var init_tui = __esm(() => {
28182
28907
  // src/cli/index.ts
28183
28908
  init_path_glob();
28184
28909
  init_vendor();
28185
- import { resolve as resolve10 } from "path";
28910
+ import { resolve as resolve12 } from "path";
28186
28911
 
28187
28912
  // src/cli/usage.ts
28188
28913
  init_version();
@@ -28241,7 +28966,7 @@ async function runAddSubcommand(rest) {
28241
28966
  ${ADD_USAGE}`);
28242
28967
  process.exit(2);
28243
28968
  }
28244
- const target = resolve10(process.cwd(), arg && arg.length > 0 ? arg : ".");
28969
+ const target = resolve12(process.cwd(), arg && arg.length > 0 ? arg : ".");
28245
28970
  const { addSavedRepo: addSavedRepo2 } = await Promise.resolve().then(() => (init_repos(), exports_repos));
28246
28971
  const result = addSavedRepo2(target);
28247
28972
  if (result.added) {
@@ -28340,7 +29065,7 @@ async function runAdoptSubcommand(args2) {
28340
29065
  }
28341
29066
  }
28342
29067
  const { resolveRepoRoot: resolveRepoRoot2 } = await Promise.resolve().then(() => (init_repos(), exports_repos));
28343
- const repo = resolveRepoRoot2(resolve10(process.cwd(), repoArg && repoArg.length > 0 ? repoArg : "."));
29068
+ const repo = resolveRepoRoot2(resolve12(process.cwd(), repoArg && repoArg.length > 0 ? repoArg : "."));
28344
29069
  const vendor = coerceVendorId(vendorArg);
28345
29070
  const orch = await openLocalOrchestrator();
28346
29071
  const worktrees = await orch.discoverAdoptableWorktrees(repo);
@@ -28558,7 +29283,24 @@ async function main() {
28558
29283
  process.exit(2);
28559
29284
  }
28560
29285
  const { healSessionLayout: healSessionLayout2 } = await Promise.resolve().then(() => (init_tmux(), exports_tmux));
28561
- 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
+ });
28562
29304
  return;
28563
29305
  }
28564
29306
  if (subcommand === "quick-task") {