@sma1lboy/kobe 0.7.17 → 0.7.19

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 (2) hide show
  1. package/dist/cli/index.js +1116 -325
  2. package/package.json +1 -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.17",
93
+ version: "0.7.19",
94
94
  description: "TUI orchestrator for Claude Code (codename)",
95
95
  type: "module",
96
96
  packageManager: "bun@1.3.13",
@@ -3918,7 +3918,10 @@ var init_protocol = __esm(() => {
3918
3918
  "active-task",
3919
3919
  "update",
3920
3920
  "engine-state",
3921
- "ui-prefs"
3921
+ "ui-prefs",
3922
+ "keybindings",
3923
+ "task.jobs",
3924
+ "worktree.changes"
3922
3925
  ];
3923
3926
  });
3924
3927
 
@@ -4066,6 +4069,7 @@ class KobeDaemonClient {
4066
4069
  this.disposed = true;
4067
4070
  this.socket?.end();
4068
4071
  this.socket = null;
4072
+ this.failPending();
4069
4073
  }
4070
4074
  forceDisconnect() {
4071
4075
  const socket = this.socket;
@@ -4073,6 +4077,15 @@ class KobeDaemonClient {
4073
4077
  return;
4074
4078
  this.socket = null;
4075
4079
  socket.destroy();
4080
+ this.failPending();
4081
+ }
4082
+ failPending() {
4083
+ if (this.pending.size === 0)
4084
+ return;
4085
+ const err = new Error("daemon connection closed");
4086
+ for (const pending of this.pending.values())
4087
+ pending.reject(err);
4088
+ this.pending.clear();
4076
4089
  }
4077
4090
  on(name, handler) {
4078
4091
  let set = this.handlers.get(name);
@@ -4147,9 +4160,7 @@ class KobeDaemonClient {
4147
4160
  if (this.socket !== which)
4148
4161
  return;
4149
4162
  this.socket = null;
4150
- for (const pending of this.pending.values())
4151
- pending.reject(new Error("daemon connection closed"));
4152
- this.pending.clear();
4163
+ this.failPending();
4153
4164
  this.emitLifecycle("close");
4154
4165
  }
4155
4166
  emitLifecycle(name) {
@@ -5131,6 +5142,16 @@ function kobeCliInvocation() {
5131
5142
  var init_invocation = () => {};
5132
5143
 
5133
5144
  // src/tmux/session-layout.ts
5145
+ function clampTasksPaneWidth(width) {
5146
+ if (!Number.isFinite(width))
5147
+ return TASKS_PANE_WIDTH;
5148
+ return Math.max(TASKS_PANE_WIDTH_MIN, Math.min(TASKS_PANE_WIDTH_MAX, Math.round(width)));
5149
+ }
5150
+ function clampPanePercent(percent) {
5151
+ if (!Number.isFinite(percent))
5152
+ return null;
5153
+ return Math.max(PANE_PERCENT_MIN, Math.min(PANE_PERCENT_MAX, Math.round(percent)));
5154
+ }
5134
5155
  function shellQuote(s) {
5135
5156
  return `'${s.replace(/'/g, "'\\''")}'`;
5136
5157
  }
@@ -5201,7 +5222,7 @@ function opsPaneCommand(args) {
5201
5222
  }
5202
5223
  return fallbackOpsScript(args.cwd);
5203
5224
  }
5204
- var TASKS_PANE_WIDTH = 32, CLAUDE_PANE_PERCENT = 60, OPS_PANE_PERCENT = 50;
5225
+ var TASKS_PANE_WIDTH = 32, TASKS_WIDTH_OPTION = "@kobe_tasks_width", TASKS_PANE_WIDTH_MIN = 16, TASKS_PANE_WIDTH_MAX = 120, CLAUDE_PANE_PERCENT = 60, OPS_PANE_PERCENT = 50, RIGHT_COLUMN_WIDTH_OPTION = "@kobe_right_width_pct", OPS_HEIGHT_OPTION = "@kobe_ops_height_pct", PANE_PERCENT_MIN = 10, PANE_PERCENT_MAX = 90;
5205
5226
 
5206
5227
  // src/engine/claude-code-local/hook-adapter.ts
5207
5228
  import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
@@ -5930,7 +5951,7 @@ function parseEvents(raw, fallbackSessionId) {
5930
5951
  if (!text)
5931
5952
  continue;
5932
5953
  if (!firstUserMessage)
5933
- firstUserMessage = text.slice(0, PREVIEW_CHAR_CAP);
5954
+ firstUserMessage = Buffer.from(text.slice(0, PREVIEW_CHAR_CAP), "utf8").toString("utf8");
5934
5955
  messages.push({ role: "user", blocks: [{ type: "text", text }], timestamp, sessionId });
5935
5956
  continue;
5936
5957
  }
@@ -6266,7 +6287,8 @@ function titleFromMessages(messages) {
6266
6287
  if (!firstUser)
6267
6288
  return "";
6268
6289
  const text = firstUser.blocks.filter((b) => b.type === "text").map((b) => b.text).join(" ");
6269
- return deriveTitleFromPrompt(text);
6290
+ const title = deriveTitleFromPrompt(text);
6291
+ return title.length > 0 ? Buffer.from(title, "utf8").toString("utf8") : title;
6270
6292
  }
6271
6293
  async function deriveTitleFromSession(worktree, vendor = DEFAULT_TASK_VENDOR) {
6272
6294
  if (!worktree)
@@ -6317,8 +6339,10 @@ __export(exports_client, {
6317
6339
  paneIdByRole: () => paneIdByRole,
6318
6340
  newWindow: () => newWindow,
6319
6341
  killSession: () => killSession,
6342
+ globalTasksPaneWidth: () => globalTasksPaneWidth,
6320
6343
  getSessionOptions: () => getSessionOptions,
6321
6344
  getSessionOption: () => getSessionOption,
6345
+ getServerOption: () => getServerOption,
6322
6346
  ensureFallbackSession: () => ensureFallbackSession,
6323
6347
  currentSessionName: () => currentSessionName,
6324
6348
  claudePaneIdStrict: () => claudePaneIdStrict,
@@ -6452,6 +6476,15 @@ async function getSessionOptions(session, options) {
6452
6476
  }
6453
6477
  return values;
6454
6478
  }
6479
+ async function getServerOption(option) {
6480
+ const { code, stdout } = await runTmuxCapturing(["show-options", "-sqv", option]);
6481
+ return code === 0 ? stdout.trim() : "";
6482
+ }
6483
+ async function globalTasksPaneWidth() {
6484
+ const raw = await getServerOption(TASKS_WIDTH_OPTION);
6485
+ const n = Number.parseInt(raw, 10);
6486
+ return Number.isFinite(n) && n > 0 ? clampTasksPaneWidth(n) : TASKS_PANE_WIDTH;
6487
+ }
6455
6488
  async function tagPaneRole(paneId, role) {
6456
6489
  await runTmux(["set-option", "-p", "-t", paneId, PANE_ROLE_OPTION, role]);
6457
6490
  }
@@ -6551,6 +6584,7 @@ async function ensureFallbackSession() {
6551
6584
  ]);
6552
6585
  const mainPane = r0.stdout.trim();
6553
6586
  if (mainPane) {
6587
+ const tasksWidth = await globalTasksPaneWidth();
6554
6588
  const r1 = await runTmuxCapturing([
6555
6589
  "split-window",
6556
6590
  "-h",
@@ -6558,7 +6592,7 @@ async function ensureFallbackSession() {
6558
6592
  "-t",
6559
6593
  mainPane,
6560
6594
  "-l",
6561
- `${TASKS_PANE_WIDTH}`,
6595
+ `${tasksWidth}`,
6562
6596
  "-c",
6563
6597
  SAFE_SPAWN_CWD,
6564
6598
  "-P",
@@ -7048,8 +7082,20 @@ function createDaemonHandlerRegistry() {
7048
7082
  name: "task.ensureWorktree",
7049
7083
  async handle(payload, ctx) {
7050
7084
  const taskId = requireString(payload, "taskId");
7051
- const path11 = await ctx.orch.ensureWorktree(taskId);
7052
- return { worktreePath: path11 };
7085
+ ctx.bus.publish("task.jobs", { taskId, kind: "ensureWorktree", phase: "running" });
7086
+ try {
7087
+ const path11 = await ctx.orch.ensureWorktree(taskId);
7088
+ ctx.bus.publish("task.jobs", { taskId, kind: "ensureWorktree", phase: "done" });
7089
+ return { worktreePath: path11 };
7090
+ } catch (err) {
7091
+ ctx.bus.publish("task.jobs", {
7092
+ taskId,
7093
+ kind: "ensureWorktree",
7094
+ phase: "error",
7095
+ error: err instanceof Error ? err.message : String(err)
7096
+ });
7097
+ throw err;
7098
+ }
7053
7099
  }
7054
7100
  },
7055
7101
  {
@@ -7181,12 +7227,66 @@ var init_handlers = __esm(() => {
7181
7227
  init_protocol();
7182
7228
  });
7183
7229
 
7184
- // ../kobe-daemon/src/daemon/ui-prefs-watcher.ts
7185
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, watch } from "fs";
7230
+ // ../kobe-daemon/src/daemon/keybindings-watcher.ts
7231
+ import { mkdirSync as mkdirSync2, watch } from "fs";
7186
7232
  import { homedir as homedir13 } from "os";
7187
7233
  import { basename as basename3, dirname as dirname5, join as join4 } from "path";
7188
- function defaultUiPrefsStatePath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir13()) {
7189
- return join4(homeDir2, ".config", "kobe", "state.json");
7234
+ function defaultKeybindingsPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir13()) {
7235
+ return join4(homeDir2, ".kobe", "settings", "keybindings.yaml");
7236
+ }
7237
+ function startKeybindingsWatcher(bus, options = {}) {
7238
+ const debounceMs = options.debounceMs ?? DEFAULT_KEYBINDINGS_DEBOUNCE_MS;
7239
+ if (debounceMs <= 0)
7240
+ return () => {};
7241
+ const filePath = options.path ?? defaultKeybindingsPath();
7242
+ const dir = dirname5(filePath);
7243
+ const baseYaml = basename3(filePath);
7244
+ const baseYml = baseYaml.replace(/\.yaml$/, ".yml");
7245
+ let rev = 0;
7246
+ bus.publish("keybindings", { rev });
7247
+ let timer = null;
7248
+ const bump = () => {
7249
+ timer = null;
7250
+ try {
7251
+ rev += 1;
7252
+ bus.publish("keybindings", { rev });
7253
+ } catch (err) {
7254
+ logDaemonError("keybindings-watcher", err);
7255
+ }
7256
+ };
7257
+ let watcher = null;
7258
+ try {
7259
+ mkdirSync2(dir, { recursive: true });
7260
+ watcher = watch(dir, (_event, filename) => {
7261
+ if (filename !== null && filename !== baseYaml && filename !== baseYml)
7262
+ return;
7263
+ if (timer)
7264
+ clearTimeout(timer);
7265
+ timer = setTimeout(bump, debounceMs);
7266
+ timer.unref?.();
7267
+ });
7268
+ watcher.on("error", (err) => logDaemonError("keybindings-watcher", err));
7269
+ } catch (err) {
7270
+ logDaemonError("keybindings-watcher", err);
7271
+ }
7272
+ return () => {
7273
+ if (timer) {
7274
+ clearTimeout(timer);
7275
+ timer = null;
7276
+ }
7277
+ watcher?.close();
7278
+ watcher = null;
7279
+ };
7280
+ }
7281
+ var DEFAULT_KEYBINDINGS_DEBOUNCE_MS = 200;
7282
+ var init_keybindings_watcher = () => {};
7283
+
7284
+ // ../kobe-daemon/src/daemon/ui-prefs-watcher.ts
7285
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, watch as watch2 } from "fs";
7286
+ import { homedir as homedir14 } from "os";
7287
+ import { basename as basename4, dirname as dirname6, join as join5 } from "path";
7288
+ function defaultUiPrefsStatePath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir14()) {
7289
+ return join5(homeDir2, ".config", "kobe", "state.json");
7190
7290
  }
7191
7291
  function readUiPrefsFromStateFile(statePath2) {
7192
7292
  let parsed = {};
@@ -7198,18 +7298,20 @@ function readUiPrefsFromStateFile(statePath2) {
7198
7298
  const theme = typeof parsed.activeTheme === "string" && parsed.activeTheme.length > 0 ? parsed.activeTheme : "claude";
7199
7299
  const transparentBackground = parsed.transparentBackground === true;
7200
7300
  const focusAccent = typeof parsed.focusAccent === "string" && FOCUS_ACCENT_SLOT_NAMES.includes(parsed.focusAccent) ? parsed.focusAccent : null;
7201
- return { theme, transparentBackground, focusAccent };
7301
+ const sortMode = parsed.activeSortMode === "recent" ? "recent" : "default";
7302
+ const keysCollapsed = parsed["tasksPane.keysCollapsed"] === true;
7303
+ return { theme, transparentBackground, focusAccent, sortMode, keysCollapsed };
7202
7304
  }
7203
7305
  function samePrefs(a, b) {
7204
- return a.theme === b.theme && a.transparentBackground === b.transparentBackground && a.focusAccent === b.focusAccent;
7306
+ return a.theme === b.theme && a.transparentBackground === b.transparentBackground && a.focusAccent === b.focusAccent && a.sortMode === b.sortMode && a.keysCollapsed === b.keysCollapsed;
7205
7307
  }
7206
7308
  function startUiPrefsWatcher(bus, options = {}) {
7207
7309
  const debounceMs = options.debounceMs ?? DEFAULT_UI_PREFS_DEBOUNCE_MS;
7208
7310
  if (debounceMs <= 0)
7209
7311
  return () => {};
7210
7312
  const statePath2 = options.statePath ?? defaultUiPrefsStatePath();
7211
- const stateDir = dirname5(statePath2);
7212
- const stateFile = basename3(statePath2);
7313
+ const stateDir = dirname6(statePath2);
7314
+ const stateFile = basename4(statePath2);
7213
7315
  let last = readUiPrefsFromStateFile(statePath2);
7214
7316
  bus.publish("ui-prefs", last);
7215
7317
  let timer = null;
@@ -7227,8 +7329,8 @@ function startUiPrefsWatcher(bus, options = {}) {
7227
7329
  };
7228
7330
  let watcher = null;
7229
7331
  try {
7230
- mkdirSync2(stateDir, { recursive: true });
7231
- watcher = watch(stateDir, (_event, filename) => {
7332
+ mkdirSync3(stateDir, { recursive: true });
7333
+ watcher = watch2(stateDir, (_event, filename) => {
7232
7334
  if (filename !== null && filename !== stateFile)
7233
7335
  return;
7234
7336
  if (timer)
@@ -7254,10 +7356,236 @@ var init_ui_prefs_watcher = __esm(() => {
7254
7356
  FOCUS_ACCENT_SLOT_NAMES = ["primary", "success", "info"];
7255
7357
  });
7256
7358
 
7359
+ // src/lib/poll-scheduling.ts
7360
+ import { spawn as spawn2 } from "child_process";
7361
+ function computeNextAllowedAt(startedAt, finishedAt, timedOut, cfg) {
7362
+ if (timedOut)
7363
+ return startedAt + cfg.slowRetryMs;
7364
+ return finishedAt + Math.max(cfg.minIntervalMs, (finishedAt - startedAt) * 5);
7365
+ }
7366
+ function shouldPoll(state, now) {
7367
+ return !state.inFlight && now >= state.nextAllowedAt;
7368
+ }
7369
+ function maybeStartScheduledRun(state, cfg, run, onValue) {
7370
+ const startedAt = Date.now();
7371
+ if (!shouldPoll(state, startedAt))
7372
+ return false;
7373
+ state.inFlight = true;
7374
+ const controller = new AbortController;
7375
+ const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
7376
+ (async () => {
7377
+ let value;
7378
+ let ok = false;
7379
+ try {
7380
+ value = await run(controller.signal);
7381
+ ok = true;
7382
+ } catch {}
7383
+ clearTimeout(timer);
7384
+ const timedOut = controller.signal.aborted;
7385
+ state.nextAllowedAt = computeNextAllowedAt(startedAt, Date.now(), timedOut, cfg);
7386
+ state.inFlight = false;
7387
+ if (ok && !timedOut)
7388
+ onValue(value);
7389
+ })();
7390
+ return true;
7391
+ }
7392
+ function spawnCapture(cmd, args, opts) {
7393
+ return new Promise((resolve2) => {
7394
+ let out = "";
7395
+ let settled = false;
7396
+ const finish = (status) => {
7397
+ if (settled)
7398
+ return;
7399
+ settled = true;
7400
+ resolve2({ status, stdout: out });
7401
+ };
7402
+ const child = spawn2(cmd, args.slice(), {
7403
+ cwd: opts.cwd,
7404
+ stdio: ["ignore", "pipe", "ignore"],
7405
+ env: opts.env,
7406
+ signal: opts.signal,
7407
+ killSignal: "SIGKILL"
7408
+ });
7409
+ child.stdout?.on("data", (chunk) => {
7410
+ out += String(chunk);
7411
+ });
7412
+ child.on("error", () => finish(null));
7413
+ child.on("close", (code) => finish(code));
7414
+ });
7415
+ }
7416
+ var init_poll_scheduling = () => {};
7417
+
7418
+ // src/tui/panes/sidebar/worktree-changes.ts
7419
+ var exports_worktree_changes = {};
7420
+ __export(exports_worktree_changes, {
7421
+ sameWorktreeChanges: () => sameWorktreeChanges,
7422
+ readWorktreeChanges: () => readWorktreeChanges,
7423
+ pickPushedChanges: () => pickPushedChanges,
7424
+ parsePorcelain: () => parsePorcelain2
7425
+ });
7426
+ import { spawnSync as spawnSync7 } from "child_process";
7427
+ function sameWorktreeChanges(a, b) {
7428
+ return a.added === b.added && a.deleted === b.deleted;
7429
+ }
7430
+ function pickPushedChanges(pushed, worktreePath) {
7431
+ if (!pushed)
7432
+ return null;
7433
+ return pushed.get(worktreePath) ?? ZERO;
7434
+ }
7435
+ function readWorktreeChanges(worktreePath) {
7436
+ if (!worktreePath)
7437
+ return ZERO;
7438
+ try {
7439
+ const out = spawnSync7("git", ["status", "--porcelain=v1"], {
7440
+ cwd: worktreePath,
7441
+ encoding: "utf8",
7442
+ stdio: ["ignore", "pipe", "pipe"],
7443
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" }
7444
+ });
7445
+ if (out.status !== 0 || !out.stdout)
7446
+ return ZERO;
7447
+ return parsePorcelain2(out.stdout);
7448
+ } catch {
7449
+ return ZERO;
7450
+ }
7451
+ }
7452
+ function parsePorcelain2(text) {
7453
+ let added = 0;
7454
+ let deleted = 0;
7455
+ for (const line of text.split(`
7456
+ `)) {
7457
+ if (!line || line.startsWith("##"))
7458
+ continue;
7459
+ const x = line.charAt(0);
7460
+ const y = line.charAt(1);
7461
+ if (x === "D" || y === "D")
7462
+ deleted += 1;
7463
+ else
7464
+ added += 1;
7465
+ }
7466
+ return { added, deleted };
7467
+ }
7468
+ var ZERO;
7469
+ var init_worktree_changes = __esm(() => {
7470
+ ZERO = { added: 0, deleted: 0 };
7471
+ });
7472
+
7473
+ // ../kobe-daemon/src/daemon/worktree-changes-collector.ts
7474
+ async function runGitStatus(worktreePath, signal) {
7475
+ const res = await spawnCapture("git", ["status", "--porcelain=v1"], {
7476
+ cwd: worktreePath,
7477
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
7478
+ signal
7479
+ });
7480
+ if (res.status !== 0)
7481
+ throw new Error("git status failed");
7482
+ return parsePorcelain2(res.stdout);
7483
+ }
7484
+ function trackedWorktreePaths(tasks) {
7485
+ const paths = new Set;
7486
+ for (const task of tasks) {
7487
+ if (task.archived)
7488
+ continue;
7489
+ if (!task.worktreePath)
7490
+ continue;
7491
+ if (isRemoteRepoKey(task.repo) || isRemoteRepoKey(task.worktreePath))
7492
+ continue;
7493
+ paths.add(task.worktreePath);
7494
+ }
7495
+ return paths;
7496
+ }
7497
+
7498
+ class WorktreeChangesCollector {
7499
+ orch;
7500
+ bus;
7501
+ options;
7502
+ entries = new Map;
7503
+ stopped = false;
7504
+ constructor(orch, bus, options = {}) {
7505
+ this.orch = orch;
7506
+ this.bus = bus;
7507
+ this.options = options;
7508
+ }
7509
+ tick() {
7510
+ if (this.stopped)
7511
+ return;
7512
+ try {
7513
+ const tracked = trackedWorktreePaths(this.orch.listTasks());
7514
+ let pruned = false;
7515
+ for (const path11 of this.entries.keys()) {
7516
+ if (tracked.has(path11))
7517
+ continue;
7518
+ const entry = this.entries.get(path11);
7519
+ if (entry?.value)
7520
+ pruned = true;
7521
+ this.entries.delete(path11);
7522
+ }
7523
+ if (pruned)
7524
+ this.publish();
7525
+ for (const path11 of tracked)
7526
+ this.maybeCollect(path11);
7527
+ } catch (err) {
7528
+ logDaemonError("worktree-changes", err);
7529
+ }
7530
+ }
7531
+ stop() {
7532
+ this.stopped = true;
7533
+ }
7534
+ maybeCollect(worktreePath) {
7535
+ let entry = this.entries.get(worktreePath);
7536
+ if (!entry) {
7537
+ entry = { inFlight: false, nextAllowedAt: 0 };
7538
+ this.entries.set(worktreePath, entry);
7539
+ }
7540
+ const cadence = this.options.cadence ?? {
7541
+ timeoutMs: WORKTREE_CHANGES_TIMEOUT_MS,
7542
+ slowRetryMs: WORKTREE_CHANGES_SLOW_RETRY_MS,
7543
+ minIntervalMs: WORKTREE_CHANGES_MIN_INTERVAL_MS
7544
+ };
7545
+ const run = this.options.run ?? runGitStatus;
7546
+ maybeStartScheduledRun(entry, cadence, (signal) => run(worktreePath, signal), (value) => {
7547
+ if (this.stopped)
7548
+ return;
7549
+ if (this.entries.get(worktreePath) !== entry)
7550
+ return;
7551
+ if (entry.value && sameWorktreeChanges(entry.value, value))
7552
+ return;
7553
+ entry.value = value;
7554
+ this.publish();
7555
+ });
7556
+ }
7557
+ publish() {
7558
+ const changes = {};
7559
+ for (const [path11, entry] of this.entries) {
7560
+ if (entry.value)
7561
+ changes[path11] = entry.value;
7562
+ }
7563
+ this.bus.publish("worktree.changes", { changes });
7564
+ }
7565
+ }
7566
+ function startWorktreeChangesCollector(orch, bus, tickMs = DEFAULT_WORKTREE_CHANGES_TICK_MS) {
7567
+ if (tickMs <= 0)
7568
+ return () => {};
7569
+ const collector = new WorktreeChangesCollector(orch, bus);
7570
+ collector.tick();
7571
+ const timer = setInterval(() => collector.tick(), tickMs);
7572
+ timer.unref?.();
7573
+ return () => {
7574
+ clearInterval(timer);
7575
+ collector.stop();
7576
+ };
7577
+ }
7578
+ var DEFAULT_WORKTREE_CHANGES_TICK_MS = 2000, WORKTREE_CHANGES_TIMEOUT_MS = 4000, WORKTREE_CHANGES_SLOW_RETRY_MS = 60000, WORKTREE_CHANGES_MIN_INTERVAL_MS = 1500;
7579
+ var init_worktree_changes_collector = __esm(() => {
7580
+ init_poll_scheduling();
7581
+ init_repos();
7582
+ init_worktree_changes();
7583
+ });
7584
+
7257
7585
  // ../kobe-daemon/src/daemon/server.ts
7258
7586
  import { mkdir as mkdir6, readFile as readFile8, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
7259
7587
  import { createServer } from "net";
7260
- import { dirname as dirname6 } from "path";
7588
+ import { dirname as dirname7 } from "path";
7261
7589
  function resolveIdleGraceMs() {
7262
7590
  const raw = process.env.KOBE_DAEMON_IDLE_GRACE_MS;
7263
7591
  if (raw === undefined)
@@ -7306,8 +7634,8 @@ async function startDaemonServer(orch, options = {}) {
7306
7634
  broadcast(clients, { type: "event", name: event.channel, payload: event.payload });
7307
7635
  });
7308
7636
  const activity = new DaemonActivityRegistry(bus);
7309
- await mkdir6(dirname6(socketPath), { recursive: true });
7310
- await mkdir6(dirname6(pidPath), { recursive: true });
7637
+ await mkdir6(dirname7(socketPath), { recursive: true });
7638
+ await mkdir6(dirname7(pidPath), { recursive: true });
7311
7639
  await unlink4(socketPath).catch(() => {});
7312
7640
  const server = createServer((socket) => {
7313
7641
  const client = {
@@ -7353,6 +7681,11 @@ async function startDaemonServer(orch, options = {}) {
7353
7681
  statePath: defaultUiPrefsStatePath(options.homeDir),
7354
7682
  debounceMs: options.uiPrefsDebounceMs ?? DEFAULT_UI_PREFS_DEBOUNCE_MS
7355
7683
  });
7684
+ const stopKeybindingsWatcher = startKeybindingsWatcher(bus, {
7685
+ path: defaultKeybindingsPath(options.homeDir),
7686
+ debounceMs: options.keybindingsDebounceMs ?? DEFAULT_KEYBINDINGS_DEBOUNCE_MS
7687
+ });
7688
+ const stopWorktreeChangesCollector = startWorktreeChangesCollector(orch, bus, options.worktreeChangesTickMs ?? DEFAULT_WORKTREE_CHANGES_TICK_MS);
7356
7689
  const serverApi = {
7357
7690
  socketPath,
7358
7691
  pidPath,
@@ -7366,6 +7699,8 @@ async function startDaemonServer(orch, options = {}) {
7366
7699
  clearInterval(updateTimer);
7367
7700
  stopAutoTitlePoller();
7368
7701
  stopUiPrefsWatcher();
7702
+ stopKeybindingsWatcher();
7703
+ stopWorktreeChangesCollector();
7369
7704
  activity.close();
7370
7705
  broadcast(clients, { type: "event", name: "daemon.stopping", payload: {} });
7371
7706
  for (const client of Array.from(clients)) {
@@ -7485,9 +7820,11 @@ var init_server = __esm(() => {
7485
7820
  init_activity_registry();
7486
7821
  init_auto_title_poller();
7487
7822
  init_handlers();
7823
+ init_keybindings_watcher();
7488
7824
  init_paths2();
7489
7825
  init_protocol();
7490
7826
  init_ui_prefs_watcher();
7827
+ init_worktree_changes_collector();
7491
7828
  init_handlers();
7492
7829
  DEFAULT_UPDATE_POLL_MS = 6 * 60 * 60 * 1000;
7493
7830
  });
@@ -7556,21 +7893,21 @@ __export(exports_daemon_process, {
7556
7893
  connectOrStartDaemon: () => connectOrStartDaemon,
7557
7894
  connectIfRunning: () => connectIfRunning
7558
7895
  });
7559
- import { spawn as spawn2 } from "child_process";
7560
- import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync3, openSync } from "fs";
7561
- import { dirname as dirname7, resolve as resolve2 } from "path";
7896
+ import { spawn as spawn3 } from "child_process";
7897
+ import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync4, openSync } from "fs";
7898
+ import { dirname as dirname8, resolve as resolve2 } from "path";
7562
7899
  import { fileURLToPath as fileURLToPath2 } from "url";
7563
7900
  function spawnDetachedDaemon(command, args, env, logPath) {
7564
7901
  let stdio = "ignore";
7565
7902
  let logFd;
7566
7903
  try {
7567
- mkdirSync3(dirname7(logPath), { recursive: true });
7904
+ mkdirSync4(dirname8(logPath), { recursive: true });
7568
7905
  logFd = openSync(logPath, "a");
7569
7906
  stdio = ["ignore", logFd, logFd];
7570
7907
  } catch {
7571
7908
  stdio = "ignore";
7572
7909
  }
7573
- const child = spawn2(command, [...args], { detached: true, stdio, env });
7910
+ const child = spawn3(command, [...args], { detached: true, stdio, env });
7574
7911
  child.unref();
7575
7912
  if (logFd !== undefined) {
7576
7913
  try {
@@ -7631,7 +7968,7 @@ function resolveKobeSpawn(subcommand) {
7631
7968
  if (here.startsWith("/$bunfs") || here.startsWith("B:\\~BUN")) {
7632
7969
  return [process.execPath, ...subcommand];
7633
7970
  }
7634
- const dir = dirname7(here);
7971
+ const dir = dirname8(here);
7635
7972
  const candidates = [
7636
7973
  resolve2(dir, "../cli/index.ts"),
7637
7974
  resolve2(dir, "../../../kobe/src/cli/index.ts"),
@@ -7730,13 +8067,13 @@ async function runRepoSubcommand(args) {
7730
8067
  }
7731
8068
  const { getRepoInitOverride: getRepoInitOverride2, setRepoInitOverride: setRepoInitOverride2, resolveRepoRoot: resolveRepoRoot2 } = await Promise.resolve().then(() => (init_repos(), exports_repos));
7732
8069
  const { existsSync: existsSync6 } = await import("fs");
7733
- const { join: join5 } = await import("path");
8070
+ const { join: join6 } = await import("path");
7734
8071
  if (verb === "show") {
7735
8072
  const [pathArg] = rest.filter((a) => !a.startsWith("-"));
7736
8073
  const repo = resolveRepoRoot2(resolve3(process.cwd(), pathArg ?? "."));
7737
8074
  const override = getRepoInitOverride2(repo);
7738
- const hasFileScript = existsSync6(join5(repo, ".kobe", "init.sh"));
7739
- const hasFilePrompt = existsSync6(join5(repo, ".kobe", "init-prompt.md"));
8075
+ const hasFileScript = existsSync6(join6(repo, ".kobe", "init.sh"));
8076
+ const hasFilePrompt = existsSync6(join6(repo, ".kobe", "init-prompt.md"));
7740
8077
  console.log(`repo: ${repo}`);
7741
8078
  console.log(` .kobe/init.sh: ${hasFileScript ? "present (wins)" : "absent"}`);
7742
8079
  console.log(` .kobe/init-prompt.md: ${hasFilePrompt ? "present (wins)" : "absent"}`);
@@ -7853,7 +8190,7 @@ var init_interactive_command = __esm(() => {
7853
8190
  });
7854
8191
 
7855
8192
  // src/lib/feedback.ts
7856
- import { spawnSync as spawnSync7 } from "child_process";
8193
+ import { spawnSync as spawnSync8 } from "child_process";
7857
8194
  function parseRepoSlug(slug) {
7858
8195
  const [owner, name] = slug.split("/");
7859
8196
  if (!owner || !name)
@@ -7908,7 +8245,7 @@ function submitFeedback(input, deps = {}) {
7908
8245
  throw new Error("package repository is not a GitHub repository");
7909
8246
  const { owner, name } = parseRepoSlug(slug);
7910
8247
  const categorySlug = input.categorySlug?.trim() || DEFAULT_FEEDBACK_CATEGORY_SLUG;
7911
- const io = { spawn: deps.spawn ?? spawnSync7 };
8248
+ const io = { spawn: deps.spawn ?? spawnSync8 };
7912
8249
  const categoryData = runGhGraphql(DISCUSSION_CATEGORY_QUERY, { owner, name }, io);
7913
8250
  const repository = categoryData.repository;
7914
8251
  const repositoryId = repository?.id;
@@ -8009,6 +8346,9 @@ var init_daemon_session = __esm(() => {
8009
8346
 
8010
8347
  // src/state/keybindings-file.ts
8011
8348
  import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
8349
+ function resetKeybindingsFileCache() {
8350
+ cached = null;
8351
+ }
8012
8352
  function resolveConfigFile() {
8013
8353
  const canonical = keybindingsConfigPath();
8014
8354
  if (existsSync6(canonical))
@@ -8044,6 +8384,13 @@ var init_keybindings_file = __esm(() => {
8044
8384
  });
8045
8385
 
8046
8386
  // src/tui/lib/keymap-overrides.ts
8387
+ function pairContract(first, second) {
8388
+ const layout = `alternating [${first}, ${second}] pairs`;
8389
+ return {
8390
+ layout,
8391
+ validateCount: (count) => count >= 2 && count % 2 === 0 ? null : `needs ${layout} (an even number of chords \u2014 got ${count})`
8392
+ };
8393
+ }
8047
8394
  function normalizeChord(raw, opts) {
8048
8395
  const trimmed = raw.trim().toLowerCase();
8049
8396
  if (!trimmed)
@@ -8183,6 +8530,14 @@ function applyKeymapOverrides(keymap, entries) {
8183
8530
  warnings.push(`${entry.id}: not customizable \u2014 the key is handled outside the keymap (doc-only row)`);
8184
8531
  continue;
8185
8532
  }
8533
+ const contract = SLOT_CONTRACTS[entry.id];
8534
+ if (contract && entry.keys.length > 0) {
8535
+ const problem = contract.validateCount(entry.keys.length);
8536
+ if (problem) {
8537
+ warnings.push(`${entry.id}: ${problem} \u2014 keeping the default`);
8538
+ continue;
8539
+ }
8540
+ }
8186
8541
  const keys = entry.keys.filter((chord) => {
8187
8542
  if (chord.length === 1 && NO_BARE_LETTER_SCOPES.has(row.scope)) {
8188
8543
  warnings.push(`${entry.id}: "${chord}" dropped \u2014 a bare character on a ${row.scope}-scope binding would steal typed input (add a modifier)`);
@@ -8194,6 +8549,10 @@ function applyKeymapOverrides(keymap, entries) {
8194
8549
  warnings.push(`${entry.id}: no chords survived validation \u2014 keeping the default`);
8195
8550
  continue;
8196
8551
  }
8552
+ if (contract && keys.length !== entry.keys.length) {
8553
+ warnings.push(`${entry.id}: a dropped chord would shift the slot layout (${contract.layout}) \u2014 keeping the default`);
8554
+ continue;
8555
+ }
8197
8556
  const defaultKeys = row.keys;
8198
8557
  const mutable = row;
8199
8558
  mutable.keys = keys;
@@ -8224,21 +8583,23 @@ function applyKeymapOverrides(keymap, entries) {
8224
8583
  }
8225
8584
  return { applied, warnings };
8226
8585
  }
8227
- var FIXED_BINDING_IDS, NO_BARE_LETTER_SCOPES, MOD_ALIASES, KEY_ALIASES, KNOWN_NAMED_KEYS, MOD_ORDER;
8586
+ var FIXED_BINDING_IDS, SLOT_CONTRACTS, NO_BARE_LETTER_SCOPES, MOD_ALIASES, KEY_ALIASES, KNOWN_NAMED_KEYS, MOD_ORDER;
8228
8587
  var init_keymap_overrides = __esm(() => {
8229
8588
  FIXED_BINDING_IDS = {
8230
- "focus.numeric": "pane focus is positional (h/j/k/l \u2192 pane) and mirrors the tmux-layer ctrl+hjkl bindings",
8231
- "sidebar.nav": "handler maps j/down vs k/up by key name",
8232
- "sidebar.goto": "gg vs Shift+G is discriminated inside the handler",
8233
- "sidebar.pin": "Shift+P is discriminated inside the handler",
8234
- "sidebar.localMerge": "Shift+M is discriminated inside the handler",
8235
- "sidebar.view": "[ vs ] direction is read from the key name",
8236
- "sidebar.search.nav": "handler maps down vs up by key name",
8237
- "files.nav": "handler maps j/down vs k/up by key name",
8238
- "files.hierarchy": "handler maps h/left vs l/right by key name",
8239
- "files.tab": "[ vs ] direction is read from the key name",
8240
- "chat.question.nav": "handler maps j/down vs k/up by key name",
8241
- "chat.question.pick-number": "digits map to options by key name"
8589
+ "focus.numeric": "pane focus is positional (h/j/k/l \u2192 pane) and mirrors the tmux-layer ctrl+hjkl bindings \u2014 rebind tmux.focus instead",
8590
+ "sidebar.goto": "gg vs Shift+G is discriminated via evt.shift; shift+<letter> chords are inexpressible, so a rebind can't carry both halves",
8591
+ "sidebar.pin": "fires on Shift+P via evt.shift; shift+<letter> chords are inexpressible, so a rebind can't work",
8592
+ "sidebar.localMerge": "fires on Shift+M via evt.shift; shift+<letter> chords are inexpressible, so a rebind can't work",
8593
+ "chat.question.nav": "the question picker has no live registration site (display-only row) \u2014 rebinding would change Help without changing behavior",
8594
+ "chat.question.pick-number": "digits map to options positionally and the question picker has no live registration site (display-only row)"
8595
+ };
8596
+ SLOT_CONTRACTS = {
8597
+ "sidebar.nav": pairContract("down", "up"),
8598
+ "files.nav": pairContract("down", "up"),
8599
+ "sidebar.search.nav": pairContract("down", "up"),
8600
+ "files.hierarchy": pairContract("collapse", "expand"),
8601
+ "sidebar.view": pairContract("previous view", "next view"),
8602
+ "files.tab": pairContract("previous tab", "next tab")
8242
8603
  };
8243
8604
  NO_BARE_LETTER_SCOPES = new Set(["global", "workspace", "terminal"]);
8244
8605
  MOD_ALIASES = {
@@ -8399,6 +8760,9 @@ function resolveTmuxKeyEntries(entries) {
8399
8760
  function tmuxChordOptsFor(id) {
8400
8761
  return id.startsWith("tmux.") ? { allowShiftCharacter: true } : {};
8401
8762
  }
8763
+ function resetTmuxKeysCache() {
8764
+ cached2 = null;
8765
+ }
8402
8766
  function resolveUserTmuxKeys() {
8403
8767
  if (cached2)
8404
8768
  return cached2;
@@ -9948,9 +10312,9 @@ var init_schema = () => {};
9948
10312
 
9949
10313
  // src/tui/context/theme/loader.ts
9950
10314
  import { readFileSync as readFileSync6, readdirSync } from "fs";
9951
- import { join as join5 } from "path";
10315
+ import { join as join6 } from "path";
9952
10316
  function userThemesDir() {
9953
- return join5(kobeStateDir(), "themes");
10317
+ return join6(kobeStateDir(), "themes");
9954
10318
  }
9955
10319
  function loadUserThemes() {
9956
10320
  const dir = userThemesDir();
@@ -9964,7 +10328,7 @@ function loadUserThemes() {
9964
10328
  for (const file of entries) {
9965
10329
  if (!file.endsWith(".json"))
9966
10330
  continue;
9967
- const path11 = join5(dir, file);
10331
+ const path11 = join6(dir, file);
9968
10332
  let parsed;
9969
10333
  try {
9970
10334
  const text = readFileSync6(path11, "utf8");
@@ -10166,11 +10530,106 @@ async function relaunchEngineInAllWindows(session, cwd, command, remoteKey) {
10166
10530
  return true;
10167
10531
  }
10168
10532
  async function healTaskPaneWidths(session) {
10169
- const rows = await listKobePanes(session);
10170
- if (!rows)
10533
+ const target = await globalTasksPaneWidth();
10534
+ const { code, stdout } = await runTmuxCapturing([
10535
+ "list-panes",
10536
+ "-s",
10537
+ "-t",
10538
+ `=${session}`,
10539
+ "-F",
10540
+ "#{pane_id}\t#{@kobe_role}\t#{pane_width}"
10541
+ ]);
10542
+ if (code !== 0)
10543
+ return;
10544
+ const mismatched = stdout.split(`
10545
+ `).map((line) => line.split("\t")).filter(([, role]) => role?.trim() === "tasks").filter(([, , width]) => Number.parseInt(width?.trim() ?? "", 10) !== target).map(([id]) => id?.trim()).filter((id) => !!id);
10546
+ if (mismatched.length === 0)
10547
+ return;
10548
+ await runTmuxSequence(mismatched.map((pane) => ["resize-pane", "-t", pane, "-x", `${target}`]));
10549
+ }
10550
+ async function rightColumnPercents() {
10551
+ const [width, height] = await Promise.all([
10552
+ getServerOption(RIGHT_COLUMN_WIDTH_OPTION),
10553
+ getServerOption(OPS_HEIGHT_OPTION)
10554
+ ]);
10555
+ return {
10556
+ widthPct: clampPanePercent(Number.parseInt(width, 10)),
10557
+ heightPct: clampPanePercent(Number.parseInt(height, 10))
10558
+ };
10559
+ }
10560
+ function rightColumnResizeArgs(geom) {
10561
+ const args = [];
10562
+ if (geom.widthPct !== null)
10563
+ args.push("-x", `${geom.widthPct}%`);
10564
+ if (geom.heightPct !== null)
10565
+ args.push("-y", `${geom.heightPct}%`);
10566
+ return args;
10567
+ }
10568
+ async function globalRightColumnResizeArgs() {
10569
+ return rightColumnResizeArgs(await rightColumnPercents());
10570
+ }
10571
+ async function healRightColumn(session) {
10572
+ const args = await globalRightColumnResizeArgs();
10573
+ if (args.length === 0)
10574
+ return;
10575
+ const { code, stdout } = await runTmuxCapturing([
10576
+ "list-panes",
10577
+ "-s",
10578
+ "-t",
10579
+ `=${session}`,
10580
+ "-F",
10581
+ "#{pane_id}\t#{@kobe_role}"
10582
+ ]);
10583
+ if (code !== 0)
10584
+ return;
10585
+ const opsPanes = stdout.split(`
10586
+ `).map((line) => line.split("\t")).filter(([, role]) => role?.trim() === "ops").map(([id]) => id?.trim()).filter((id) => !!id);
10587
+ if (opsPanes.length === 0)
10588
+ return;
10589
+ await runTmuxSequence(opsPanes.map((pane) => ["resize-pane", "-t", pane, ...args]));
10590
+ }
10591
+ async function healSessionLayout(session) {
10592
+ if (!await sessionExists(session))
10593
+ return;
10594
+ await healTaskPaneWidths(session);
10595
+ await healRightColumn(session);
10596
+ }
10597
+ async function captureGlobalLayout(session) {
10598
+ const { code, stdout } = await runTmuxCapturing([
10599
+ "list-panes",
10600
+ "-t",
10601
+ `=${session}`,
10602
+ "-F",
10603
+ "#{@kobe_role}\t#{pane_width}\t#{pane_height}\t#{window_width}\t#{window_height}"
10604
+ ]);
10605
+ if (code !== 0)
10606
+ return;
10607
+ const rows = stdout.split(`
10608
+ `).map((line) => line.split("\t")).filter((cols) => (cols[0]?.trim() ?? "") !== "");
10609
+ if (rows.length === 0)
10171
10610
  return;
10172
- const taskPanes = paneIdsByRole(rows, "tasks");
10173
- await runTmuxSequence(taskPanes.map((pane) => ["resize-pane", "-t", pane, "-x", `${TASKS_PANE_WIDTH}`]));
10611
+ const winW = Number.parseInt(rows[0][3]?.trim() ?? "", 10);
10612
+ const winH = Number.parseInt(rows[0][4]?.trim() ?? "", 10);
10613
+ const sets = [];
10614
+ const tasks = rows.find(([role]) => role?.trim() === "tasks");
10615
+ if (tasks) {
10616
+ const width = Number.parseInt(tasks[1]?.trim() ?? "", 10);
10617
+ if (Number.isFinite(width) && width > 0)
10618
+ sets.push(["set-option", "-s", TASKS_WIDTH_OPTION, `${clampTasksPaneWidth(width)}`]);
10619
+ }
10620
+ const ops = rows.find(([role]) => role?.trim() === "ops");
10621
+ if (ops && Number.isFinite(winW) && winW > 0 && Number.isFinite(winH) && winH > 0) {
10622
+ const opsW = Number.parseInt(ops[1]?.trim() ?? "", 10);
10623
+ const opsH = Number.parseInt(ops[2]?.trim() ?? "", 10);
10624
+ const widthPct = Number.isFinite(opsW) ? clampPanePercent(100 * opsW / winW) : null;
10625
+ const heightPct = Number.isFinite(opsH) ? clampPanePercent(100 * opsH / winH) : null;
10626
+ if (widthPct !== null)
10627
+ sets.push(["set-option", "-s", RIGHT_COLUMN_WIDTH_OPTION, `${widthPct}`]);
10628
+ if (heightPct !== null)
10629
+ sets.push(["set-option", "-s", OPS_HEIGHT_OPTION, `${heightPct}`]);
10630
+ }
10631
+ if (sets.length > 0)
10632
+ await runTmuxSequence(sets);
10174
10633
  }
10175
10634
  async function healKobePaneVersions(session, cwd, taskId, vendor) {
10176
10635
  const rows = await listKobePanes(session);
@@ -10253,6 +10712,7 @@ function kobeStatusRight(keys) {
10253
10712
  }
10254
10713
  async function buildPanesAround(claudePane, args) {
10255
10714
  const envPrefix = inheritedEnvPrefix();
10715
+ const tasksWidth = await globalTasksPaneWidth();
10256
10716
  const opsCmd = keepAlive(args.opsCommand ?? envPrefix + opsPaneCommand({
10257
10717
  cwd: args.cwd,
10258
10718
  taskId: args.taskId,
@@ -10270,7 +10730,7 @@ async function buildPanesAround(claudePane, args) {
10270
10730
  "-t",
10271
10731
  claudePane,
10272
10732
  "-l",
10273
- `${TASKS_PANE_WIDTH}`,
10733
+ `${tasksWidth}`,
10274
10734
  "-c",
10275
10735
  localSpawnCwd(args.cwd),
10276
10736
  "-P",
@@ -10303,6 +10763,11 @@ async function buildPanesAround(claudePane, args) {
10303
10763
  ...ids.ops ? [["set-option", "-p", "-t", ids.ops, "@kobe_role", "ops"]] : [],
10304
10764
  ...ids.ops ? [["set-option", "-p", "-t", ids.ops, PANE_VERSION_OPTION, CURRENT_VERSION]] : []
10305
10765
  ]);
10766
+ if (ids.ops) {
10767
+ const rcArgs = await globalRightColumnResizeArgs();
10768
+ if (rcArgs.length > 0)
10769
+ await runTmux(["resize-pane", "-t", ids.ops, ...rcArgs]);
10770
+ }
10306
10771
  }
10307
10772
  async function newChatTab(session, vendorOverride) {
10308
10773
  if (!await sessionExists(session))
@@ -10350,6 +10815,16 @@ async function openSettingsTab(session) {
10350
10815
  const command = `${envPrefix}${inv.map(shellQuote).join(" ")} settings`;
10351
10816
  await newWindow(session, { cwd: localSpawnCwd(cwd), command, name: "settings" });
10352
10817
  }
10818
+ async function openHelpTab(session) {
10819
+ if (!await sessionExists(session))
10820
+ return;
10821
+ const sessionOptions = await getSessionOptions(session, ["@kobe_worktree"]);
10822
+ const cwd = sessionOptions["@kobe_worktree"] || process.cwd();
10823
+ const inv = kobeCliInvocation();
10824
+ const envPrefix = inheritedEnvPrefix();
10825
+ const command = `${envPrefix}${inv.map(shellQuote).join(" ")} help-page`;
10826
+ await newWindow(session, { cwd: localSpawnCwd(cwd), command, name: "help" });
10827
+ }
10353
10828
  async function openNewTaskTab(session, defaultRepo) {
10354
10829
  if (!await sessionExists(session))
10355
10830
  return;
@@ -10422,18 +10897,22 @@ __export(exports_tmux, {
10422
10897
  selectTasksPane: () => selectTasksPane,
10423
10898
  refreshKobeWorkspacePanes: () => refreshKobeWorkspacePanes,
10424
10899
  quickCreate: () => quickCreate,
10900
+ prepareWindowForAttach: () => prepareWindowForAttach,
10425
10901
  openUpdateTab: () => openUpdateTab,
10426
10902
  openSettingsTab: () => openSettingsTab,
10427
10903
  openNewTaskTab: () => openNewTaskTab,
10904
+ openHelpTab: () => openHelpTab,
10428
10905
  newChatTab: () => newChatTab,
10429
10906
  kobeStatusRight: () => kobeStatusRight,
10430
10907
  killSession: () => killSession,
10908
+ healSessionLayout: () => healSessionLayout,
10431
10909
  ensureSession: () => ensureSession,
10432
10910
  currentSessionName: () => currentSessionName,
10433
10911
  chatTabSwitchBindings: () => chatTabSwitchBindings,
10434
10912
  chatTabRenameBinding: () => chatTabRenameBinding,
10435
10913
  chatTabCloseBinding: () => chatTabCloseBinding,
10436
10914
  chatTabChooseEngineBindings: () => chatTabChooseEngineBindings,
10915
+ captureGlobalLayout: () => captureGlobalLayout,
10437
10916
  attachArgv: () => attachArgv,
10438
10917
  PANE_VERSION_OPTION: () => PANE_VERSION_OPTION,
10439
10918
  CHAT_TAB_STATUS_FORMAT: () => CHAT_TAB_STATUS_FORMAT,
@@ -10450,6 +10929,13 @@ function positiveInt(value) {
10450
10929
  const n = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value, 10) : Number.NaN;
10451
10930
  return Number.isInteger(n) && n > 0 ? n : undefined;
10452
10931
  }
10932
+ async function prepareWindowForAttach(session) {
10933
+ const sizeArgs = tmuxInitialSizeArgs();
10934
+ if (sizeArgs.length > 0)
10935
+ await runTmux(["resize-window", "-t", `=${session}`, ...sizeArgs]);
10936
+ await healTaskPaneWidths(session);
10937
+ await healRightColumn(session);
10938
+ }
10453
10939
  async function ensureSession(opts) {
10454
10940
  const inflight = ensureSessionLocks.get(opts.name);
10455
10941
  if (inflight)
@@ -10483,6 +10969,7 @@ async function ensureSessionImpl(opts) {
10483
10969
  const remoteKey = remoteKeyForRepo(opts.repo);
10484
10970
  if (action.kind === "reuse") {
10485
10971
  await healTaskPaneWidths(opts.name);
10972
+ await healRightColumn(opts.name);
10486
10973
  await healKobePaneVersions(opts.name, opts.cwd, opts.taskId, opts.vendor);
10487
10974
  return true;
10488
10975
  }
@@ -10491,6 +10978,7 @@ async function ensureSessionImpl(opts) {
10491
10978
  if (opts.vendor)
10492
10979
  await setSessionOption(opts.name, "@kobe_vendor", opts.vendor);
10493
10980
  await healTaskPaneWidths(opts.name);
10981
+ await healRightColumn(opts.name);
10494
10982
  await healKobePaneVersions(opts.name, opts.cwd, opts.taskId, opts.vendor);
10495
10983
  return true;
10496
10984
  }
@@ -10544,6 +11032,8 @@ async function ensureSessionImpl(opts) {
10544
11032
  const chooseEngineTmuxCommand = `run-shell ${shellQuote(chooseEngineCommand)}`;
10545
11033
  const focusTasksCommand = `${envStr}${invStr} focus-tasks --session '#{session_name}'`;
10546
11034
  const focusTasksTmuxCommand = `run-shell ${shellQuote(focusTasksCommand)}`;
11035
+ const healLayoutCommand = `${envStr}${invStr} heal-layout --session '#{session_name}'`;
11036
+ const healLayoutTmuxCommand = `run-shell -b ${shellQuote(healLayoutCommand)}`;
10547
11037
  const userKeys = resolveUserTmuxKeys();
10548
11038
  const unbinds = [];
10549
11039
  if (userKeys.overridden.has(TMUX_FOCUS_ID)) {
@@ -10585,6 +11075,7 @@ async function ensureSessionImpl(opts) {
10585
11075
  })
10586
11076
  ],
10587
11077
  ["set-option", "-g", "mouse", "on"],
11078
+ ["set-hook", "-g", "window-resized", healLayoutTmuxCommand],
10588
11079
  ...unbinds,
10589
11080
  ...b["tmux.detach"] ? [
10590
11081
  [
@@ -10648,12 +11139,12 @@ __export(exports_repo_init, {
10648
11139
  resolveRepoInit: () => resolveRepoInit
10649
11140
  });
10650
11141
  import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
10651
- import { join as join6 } from "path";
11142
+ import { join as join7 } from "path";
10652
11143
  function repoFileScript(worktreePath) {
10653
- return existsSync7(join6(worktreePath, INIT_SCRIPT_REL)) ? `sh ${INIT_SCRIPT_REL}` : undefined;
11144
+ return existsSync7(join7(worktreePath, INIT_SCRIPT_REL)) ? `sh ${INIT_SCRIPT_REL}` : undefined;
10654
11145
  }
10655
11146
  function repoFilePrompt(worktreePath) {
10656
- const p = join6(worktreePath, INIT_PROMPT_REL);
11147
+ const p = join7(worktreePath, INIT_PROMPT_REL);
10657
11148
  if (!existsSync7(p))
10658
11149
  return;
10659
11150
  try {
@@ -10675,53 +11166,8 @@ function resolveRepoInit(repoRoot, worktreePath) {
10675
11166
  var INIT_SCRIPT_REL, INIT_PROMPT_REL;
10676
11167
  var init_repo_init = __esm(() => {
10677
11168
  init_repos();
10678
- INIT_SCRIPT_REL = join6(".kobe", "init.sh");
10679
- INIT_PROMPT_REL = join6(".kobe", "init-prompt.md");
10680
- });
10681
-
10682
- // src/tui/panes/sidebar/worktree-changes.ts
10683
- var exports_worktree_changes = {};
10684
- __export(exports_worktree_changes, {
10685
- readWorktreeChanges: () => readWorktreeChanges,
10686
- parsePorcelain: () => parsePorcelain2
10687
- });
10688
- import { spawnSync as spawnSync8 } from "child_process";
10689
- function readWorktreeChanges(worktreePath) {
10690
- if (!worktreePath)
10691
- return ZERO;
10692
- try {
10693
- const out = spawnSync8("git", ["status", "--porcelain=v1"], {
10694
- cwd: worktreePath,
10695
- encoding: "utf8",
10696
- stdio: ["ignore", "pipe", "pipe"],
10697
- env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" }
10698
- });
10699
- if (out.status !== 0 || !out.stdout)
10700
- return ZERO;
10701
- return parsePorcelain2(out.stdout);
10702
- } catch {
10703
- return ZERO;
10704
- }
10705
- }
10706
- function parsePorcelain2(text) {
10707
- let added = 0;
10708
- let deleted = 0;
10709
- for (const line of text.split(`
10710
- `)) {
10711
- if (!line || line.startsWith("##"))
10712
- continue;
10713
- const x = line.charAt(0);
10714
- const y = line.charAt(1);
10715
- if (x === "D" || y === "D")
10716
- deleted += 1;
10717
- else
10718
- added += 1;
10719
- }
10720
- return { added, deleted };
10721
- }
10722
- var ZERO;
10723
- var init_worktree_changes = __esm(() => {
10724
- ZERO = { added: 0, deleted: 0 };
11169
+ INIT_SCRIPT_REL = join7(".kobe", "init.sh");
11170
+ INIT_PROMPT_REL = join7(".kobe", "init-prompt.md");
10725
11171
  });
10726
11172
 
10727
11173
  // src/cli/api-cmd.ts
@@ -11717,8 +12163,8 @@ var exports_theme = {};
11717
12163
  __export(exports_theme, {
11718
12164
  runThemeSubcommand: () => runThemeSubcommand
11719
12165
  });
11720
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync9, readdirSync as readdirSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
11721
- import { basename as basename4, join as join7, resolve as resolve5 } from "path";
12166
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync9, readdirSync as readdirSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
12167
+ import { basename as basename5, join as join8, resolve as resolve5 } from "path";
11722
12168
  function fail3(message) {
11723
12169
  process.stderr.write(`kobe theme: ${message}
11724
12170
  `);
@@ -11749,7 +12195,7 @@ function listThemes() {
11749
12195
  } else {
11750
12196
  for (const f of userFiles) {
11751
12197
  const name = f.slice(0, -".json".length);
11752
- const path11 = join7(dir, f);
12198
+ const path11 = join8(dir, f);
11753
12199
  const overridesBundled = BUNDLED_NAMES.includes(name) ? " (overrides built-in)" : "";
11754
12200
  lines.push(` ${name}${overridesBundled} ${path11}`);
11755
12201
  }
@@ -11771,7 +12217,7 @@ async function readSource(source) {
11771
12217
  }
11772
12218
  const text2 = await res.text();
11773
12219
  const cleanPath = source.split(/[?#]/)[0] ?? source;
11774
- const file2 = basename4(cleanPath) || "theme.json";
12220
+ const file2 = basename5(cleanPath) || "theme.json";
11775
12221
  const defaultName2 = file2.endsWith(".json") ? file2.slice(0, -".json".length) : file2;
11776
12222
  return { text: text2, defaultName: defaultName2 };
11777
12223
  }
@@ -11782,7 +12228,7 @@ async function readSource(source) {
11782
12228
  } catch (err) {
11783
12229
  fail3(`failed to read ${abs}: ${err instanceof Error ? err.message : String(err)}`);
11784
12230
  }
11785
- const file = basename4(abs);
12231
+ const file = basename5(abs);
11786
12232
  const defaultName = file.endsWith(".json") ? file.slice(0, -".json".length) : file;
11787
12233
  return { text, defaultName };
11788
12234
  }
@@ -11838,8 +12284,8 @@ async function addTheme(args) {
11838
12284
  fail3(`invalid theme name "${name}" (use letters, digits, '.', '_', '-')`);
11839
12285
  }
11840
12286
  const dir = userThemesDir();
11841
- mkdirSync4(dir, { recursive: true });
11842
- const dest = join7(dir, `${name}.json`);
12287
+ mkdirSync5(dir, { recursive: true });
12288
+ const dest = join8(dir, `${name}.json`);
11843
12289
  if (existsSync8(dest) && !opts.force) {
11844
12290
  fail3(`${dest} already exists (pass --force to overwrite)`);
11845
12291
  }
@@ -11857,7 +12303,7 @@ function removeTheme(args) {
11857
12303
  if (BUNDLED_NAMES.includes(name)) {
11858
12304
  fail3(`"${name}" is a built-in theme and cannot be removed`);
11859
12305
  }
11860
- const dest = join7(userThemesDir(), `${name}.json`);
12306
+ const dest = join8(userThemesDir(), `${name}.json`);
11861
12307
  if (!existsSync8(dest)) {
11862
12308
  fail3(`no user theme named "${name}" (looked for ${dest})`);
11863
12309
  }
@@ -12018,9 +12464,9 @@ var init_feedback_cmd = __esm(() => {
12018
12464
  });
12019
12465
 
12020
12466
  // src/core/index.ts
12021
- import { homedir as homedir14 } from "os";
12467
+ import { homedir as homedir15 } from "os";
12022
12468
  async function createKobeCore(options = {}) {
12023
- const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir14();
12469
+ const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir15();
12024
12470
  const store = new TaskIndexStore({ homeDir: homeDir2 });
12025
12471
  await store.load();
12026
12472
  const worktrees = new GitWorktreeManager;
@@ -12138,8 +12584,8 @@ var init_daemon_cmd = __esm(() => {
12138
12584
 
12139
12585
  // src/lib/skill-install.ts
12140
12586
  import { existsSync as existsSync9, readFileSync as readFileSync11 } from "fs";
12141
- import { homedir as homedir15 } from "os";
12142
- import { join as join8 } from "path";
12587
+ import { homedir as homedir16 } from "os";
12588
+ import { join as join9 } from "path";
12143
12589
  function npxSkillsArgv(opts = {}) {
12144
12590
  return ["skills", "add", SKILL_SOURCE_SLUG, "--skill", "kobe", "--agent", opts.agent ?? DEFAULT_SKILL_AGENT];
12145
12591
  }
@@ -12147,9 +12593,9 @@ function npxSkillsCommand(opts = {}) {
12147
12593
  return `npx ${npxSkillsArgv(opts).join(" ")}`;
12148
12594
  }
12149
12595
  function kobeSkillPaths(opts = {}) {
12150
- const home = opts.home ?? homedir15();
12596
+ const home = opts.home ?? homedir16();
12151
12597
  const cwd = opts.cwd ?? process.cwd();
12152
- return [join8(home, SKILL_REL_PATH), join8(cwd, SKILL_REL_PATH)];
12598
+ return [join9(home, SKILL_REL_PATH), join9(cwd, SKILL_REL_PATH)];
12153
12599
  }
12154
12600
  function parseSkillVersion(content) {
12155
12601
  const m = content.match(/kobe-skill-version:\s*(\d+)/);
@@ -12210,7 +12656,7 @@ __export(exports_maintenance, {
12210
12656
  });
12211
12657
  import { existsSync as existsSync10, readFileSync as readFileSync12, statSync as statSync5 } from "fs";
12212
12658
  import { unlink as unlink6 } from "fs/promises";
12213
- import { join as join9 } from "path";
12659
+ import { join as join10 } from "path";
12214
12660
  import { createInterface as createInterface2 } from "readline";
12215
12661
  function isProcessAlive2(pid) {
12216
12662
  try {
@@ -12307,7 +12753,7 @@ async function runDoctorSubcommand(argv = []) {
12307
12753
  const socketPath = defaultDaemonSocketPath();
12308
12754
  const pidPath = defaultDaemonPidPath();
12309
12755
  const logPath = defaultDaemonLogPath();
12310
- const tasksPath = join9(kobeStateDir(), "tasks.json");
12756
+ const tasksPath = join10(kobeStateDir(), "tasks.json");
12311
12757
  const statePath2 = kvStatePath();
12312
12758
  const out = ["kobe doctor", ` home: ${homeDir()}`, ` socket: ${socketPath}`, ""];
12313
12759
  const status = await probeDaemonStatus(socketPath);
@@ -12426,7 +12872,7 @@ async function runResetSubcommand(argv) {
12426
12872
  const yes = argv.includes("--yes") || argv.includes("-y");
12427
12873
  const socketPath = defaultDaemonSocketPath();
12428
12874
  const pidPath = defaultDaemonPidPath();
12429
- const tasksPath = join9(kobeStateDir(), "tasks.json");
12875
+ const tasksPath = join10(kobeStateDir(), "tasks.json");
12430
12876
  const statePath2 = kvStatePath();
12431
12877
  console.log("kobe reset will:");
12432
12878
  console.log(" \u2022 stop the kobe daemon (graceful \u2192 SIGTERM \u2192 SIGKILL)");
@@ -12696,15 +13142,15 @@ var GIT_TIMEOUT_MS = 15000;
12696
13142
 
12697
13143
  // src/web/notes.ts
12698
13144
  import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
12699
- import { join as join10 } from "path";
13145
+ import { join as join11 } from "path";
12700
13146
  function notesDir() {
12701
- return join10(kobeStateDir(), "notes");
13147
+ return join11(kobeStateDir(), "notes");
12702
13148
  }
12703
13149
  function isSafeTaskId(taskId) {
12704
13150
  return typeof taskId === "string" && taskId.length > 0 && /^[A-Za-z0-9_-]+$/.test(taskId);
12705
13151
  }
12706
13152
  function noteFilePath(taskId) {
12707
- return join10(notesDir(), `${taskId}.md`);
13153
+ return join11(notesDir(), `${taskId}.md`);
12708
13154
  }
12709
13155
  async function handleGet(url) {
12710
13156
  const taskId = url.searchParams.get("taskId");
@@ -12817,16 +13263,21 @@ class DaemonLink {
12817
13263
  const socketPath = allowSpawn ? await ensureDaemonReachable() : defaultDaemonSocketPath();
12818
13264
  const client = new KobeDaemonClient(socketPath);
12819
13265
  await client.connect();
12820
- const hello = await client.request("hello", {
12821
- protocolVersion: DAEMON_PROTOCOL_VERSION,
12822
- minProtocolVersion: MIN_COMPATIBLE_PROTOCOL_VERSION
12823
- });
12824
- if (hello.tasks)
12825
- this.tasks = hello.tasks;
12826
- this.engineStates = {};
12827
- client.on("*", (frame) => this.onFrame(frame.name, frame.payload));
12828
- client.onLifecycle("close", () => this.onDrop(client));
12829
- await client.subscribe({ role: "gui" });
13266
+ try {
13267
+ const hello = await client.request("hello", {
13268
+ protocolVersion: DAEMON_PROTOCOL_VERSION,
13269
+ minProtocolVersion: MIN_COMPATIBLE_PROTOCOL_VERSION
13270
+ });
13271
+ if (hello.tasks)
13272
+ this.tasks = hello.tasks;
13273
+ this.engineStates = {};
13274
+ client.on("*", (frame) => this.onFrame(frame.name, frame.payload));
13275
+ client.onLifecycle("close", () => this.onDrop(client));
13276
+ await client.subscribe({ role: "gui" });
13277
+ } catch (err) {
13278
+ client.close();
13279
+ throw err;
13280
+ }
12830
13281
  this.client = client;
12831
13282
  this.setConnected(true);
12832
13283
  }
@@ -12950,7 +13401,7 @@ var init_session = __esm(() => {
12950
13401
 
12951
13402
  // ../kobe-web/server/bridge.ts
12952
13403
  import { existsSync as existsSync11 } from "fs";
12953
- import { join as join11, normalize as normalize2 } from "path";
13404
+ import { join as join12, normalize as normalize2 } from "path";
12954
13405
  function sseResponse(register) {
12955
13406
  let unregister = null;
12956
13407
  let heartbeat = null;
@@ -13024,10 +13475,10 @@ async function specResponse(url, link, spec) {
13024
13475
  }
13025
13476
  async function staticResponse(pathname, staticDir) {
13026
13477
  const rel = pathname === "/" ? "/index.html" : pathname;
13027
- const resolved = normalize2(join11(staticDir, rel));
13478
+ const resolved = normalize2(join12(staticDir, rel));
13028
13479
  if (!resolved.startsWith(staticDir))
13029
13480
  return new Response("forbidden", { status: 403 });
13030
- const file = Bun.file(existsSync11(resolved) ? resolved : join11(staticDir, "index.html"));
13481
+ const file = Bun.file(existsSync11(resolved) ? resolved : join12(staticDir, "index.html"));
13031
13482
  if (!await file.exists()) {
13032
13483
  return new Response("kobe web assets not built \u2014 run `bun --filter kobe-web build`", { status: 503 });
13033
13484
  }
@@ -13424,8 +13875,8 @@ __export(exports_hook_cmd, {
13424
13875
  parseWorktreeAddPath: () => parseWorktreeAddPath,
13425
13876
  ensureGlobalKobeHooks: () => ensureGlobalKobeHooks
13426
13877
  });
13427
- import { homedir as homedir16 } from "os";
13428
- import { join as join12, resolve as resolve7 } from "path";
13878
+ import { homedir as homedir17 } from "os";
13879
+ import { join as join13, resolve as resolve7 } from "path";
13429
13880
  async function readStdinPayload() {
13430
13881
  try {
13431
13882
  const text = await Promise.race([
@@ -13545,7 +13996,7 @@ function activityHookAdapters() {
13545
13996
  return ALL_VENDORS.map((v) => createEngineHookAdapter(v)).filter((a) => a.supportsHooks());
13546
13997
  }
13547
13998
  function globalSettingsPath() {
13548
- return join12(homedir16(), ".claude", "settings.json");
13999
+ return join13(homedir17(), ".claude", "settings.json");
13549
14000
  }
13550
14001
  function persistedSyncPath(stored) {
13551
14002
  if (!stored || stored === "off")
@@ -13553,7 +14004,7 @@ function persistedSyncPath(stored) {
13553
14004
  if (stored === "global")
13554
14005
  return globalSettingsPath();
13555
14006
  if (stored.startsWith("repo:"))
13556
- return join12(resolve7(stored.slice(5)), ".claude", "settings.json");
14007
+ return join13(resolve7(stored.slice(5)), ".claude", "settings.json");
13557
14008
  return stored;
13558
14009
  }
13559
14010
  async function ensureGlobalKobeHooks() {
@@ -14883,6 +15334,30 @@ var init_solid = __esm(() => {
14883
15334
  });
14884
15335
 
14885
15336
  // src/client/remote-orchestrator.ts
15337
+ function parseWorktreeChangesPayload(payload) {
15338
+ const changes = payload?.changes;
15339
+ if (!changes || typeof changes !== "object" || Array.isArray(changes))
15340
+ return null;
15341
+ const map = new Map;
15342
+ for (const [path11, value] of Object.entries(changes)) {
15343
+ const counts = value;
15344
+ if (typeof counts?.added !== "number" || typeof counts.deleted !== "number")
15345
+ return null;
15346
+ map.set(path11, { added: counts.added, deleted: counts.deleted });
15347
+ }
15348
+ return map;
15349
+ }
15350
+ function sameWorktreeChangesMap(a, b) {
15351
+ if (a.size !== b.size)
15352
+ return false;
15353
+ for (const [path11, counts] of a) {
15354
+ const other = b.get(path11);
15355
+ if (!other || !sameWorktreeChanges(counts, other))
15356
+ return false;
15357
+ }
15358
+ return true;
15359
+ }
15360
+
14886
15361
  class RemoteOrchestrator {
14887
15362
  client;
14888
15363
  tasksAcc;
@@ -14895,8 +15370,14 @@ class RemoteOrchestrator {
14895
15370
  setDaemonVersionSig;
14896
15371
  engineStateAcc;
14897
15372
  setEngineStateSig;
15373
+ taskJobsAcc;
15374
+ setTaskJobsSig;
15375
+ worktreeChangesAcc;
15376
+ setWorktreeChangesSig;
14898
15377
  uiPrefsAcc;
14899
15378
  setUiPrefsSig;
15379
+ keybindingsRevAcc;
15380
+ setKeybindingsRevSig;
14900
15381
  connectionStateAcc;
14901
15382
  setConnectionState;
14902
15383
  ensureReachable;
@@ -14909,7 +15390,10 @@ class RemoteOrchestrator {
14909
15390
  const [update, setUpdate] = createSignal(null);
14910
15391
  const [daemonVersion, setDaemonVersion] = createSignal(null);
14911
15392
  const [engineState, setEngineState] = createSignal(new Map);
15393
+ const [taskJobs, setTaskJobs] = createSignal(new Map);
15394
+ const [worktreeChanges, setWorktreeChanges] = createSignal(null);
14912
15395
  const [uiPrefs, setUiPrefs] = createSignal(null);
15396
+ const [keybindingsRev, setKeybindingsRev] = createSignal(null);
14913
15397
  const [connectionState, setConnectionState] = createSignal("online");
14914
15398
  this.tasksAcc = tasks;
14915
15399
  this.setTasks = (next) => setTasks(() => next);
@@ -14921,8 +15405,14 @@ class RemoteOrchestrator {
14921
15405
  this.setDaemonVersionSig = (next) => setDaemonVersion(() => next);
14922
15406
  this.engineStateAcc = engineState;
14923
15407
  this.setEngineStateSig = (next) => setEngineState(() => next);
15408
+ this.taskJobsAcc = taskJobs;
15409
+ this.setTaskJobsSig = (next) => setTaskJobs(() => next);
15410
+ this.worktreeChangesAcc = worktreeChanges;
15411
+ this.setWorktreeChangesSig = (next) => setWorktreeChanges(() => next);
14924
15412
  this.uiPrefsAcc = uiPrefs;
14925
15413
  this.setUiPrefsSig = (next) => setUiPrefs(() => next);
15414
+ this.keybindingsRevAcc = keybindingsRev;
15415
+ this.setKeybindingsRevSig = (next) => setKeybindingsRev(() => next);
14926
15416
  this.connectionStateAcc = connectionState;
14927
15417
  this.setConnectionState = (next) => setConnectionState(() => next);
14928
15418
  this.ensureReachable = options.ensureReachable ?? ensureDaemonReachable;
@@ -14979,6 +15469,12 @@ class RemoteOrchestrator {
14979
15469
  if (hello.tasks)
14980
15470
  this.setTasks(hello.tasks.map(deserializeTask));
14981
15471
  await this.client.subscribe({ role: this.role });
15472
+ if (hello.capabilities?.includes("worktree.changes")) {
15473
+ if (this.worktreeChangesAcc() === null)
15474
+ this.setWorktreeChangesSig(new Map);
15475
+ } else {
15476
+ this.setWorktreeChangesSig(null);
15477
+ }
14982
15478
  this.setConnectionState("online");
14983
15479
  logClient("orch", `subscribed as ${this.role} (${this.tasksAcc().length} tasks)`);
14984
15480
  }
@@ -15011,9 +15507,18 @@ class RemoteOrchestrator {
15011
15507
  engineStateSignal() {
15012
15508
  return this.engineStateAcc;
15013
15509
  }
15510
+ taskJobsSignal() {
15511
+ return this.taskJobsAcc;
15512
+ }
15513
+ worktreeChangesSignal() {
15514
+ return this.worktreeChangesAcc;
15515
+ }
15014
15516
  uiPrefsSignal() {
15015
15517
  return this.uiPrefsAcc;
15016
15518
  }
15519
+ keybindingsRevSignal() {
15520
+ return this.keybindingsRevAcc;
15521
+ }
15017
15522
  listTasks() {
15018
15523
  return this.tasksAcc();
15019
15524
  }
@@ -15093,8 +15598,11 @@ class RemoteOrchestrator {
15093
15598
  handleEvent(name, payload) {
15094
15599
  if (name === "task.snapshot") {
15095
15600
  const value = payload?.tasks;
15096
- if (Array.isArray(value))
15601
+ if (Array.isArray(value)) {
15097
15602
  this.setTasks(value.map(deserializeTask));
15603
+ this.pruneEngineState(value);
15604
+ this.pruneTaskJobs(value);
15605
+ }
15098
15606
  return;
15099
15607
  }
15100
15608
  if (name === "active-task") {
@@ -15119,6 +15627,34 @@ class RemoteOrchestrator {
15119
15627
  this.setEngineStateSig(next);
15120
15628
  return;
15121
15629
  }
15630
+ if (name === "task.jobs") {
15631
+ const p = payload;
15632
+ if (typeof p?.taskId !== "string" || p.kind !== "ensureWorktree")
15633
+ return;
15634
+ const current = this.taskJobsAcc();
15635
+ if (p.phase === "running") {
15636
+ const next = new Map(current);
15637
+ next.set(p.taskId, { kind: p.kind });
15638
+ this.setTaskJobsSig(next);
15639
+ return;
15640
+ }
15641
+ if ((p.phase === "done" || p.phase === "error") && current.has(p.taskId)) {
15642
+ const next = new Map(current);
15643
+ next.delete(p.taskId);
15644
+ this.setTaskJobsSig(next);
15645
+ }
15646
+ return;
15647
+ }
15648
+ if (name === "worktree.changes") {
15649
+ const next = parseWorktreeChangesPayload(payload);
15650
+ if (!next)
15651
+ return;
15652
+ const current = this.worktreeChangesAcc();
15653
+ if (current && sameWorktreeChangesMap(current, next))
15654
+ return;
15655
+ this.setWorktreeChangesSig(next);
15656
+ return;
15657
+ }
15122
15658
  if (name === "ui-prefs") {
15123
15659
  const p = payload;
15124
15660
  if (typeof p?.theme !== "string")
@@ -15126,10 +15662,51 @@ class RemoteOrchestrator {
15126
15662
  this.setUiPrefsSig({
15127
15663
  theme: p.theme,
15128
15664
  transparentBackground: p.transparentBackground === true,
15129
- focusAccent: typeof p.focusAccent === "string" ? p.focusAccent : null
15665
+ focusAccent: typeof p.focusAccent === "string" ? p.focusAccent : null,
15666
+ sortMode: p.sortMode === "recent" ? "recent" : "default",
15667
+ keysCollapsed: p.keysCollapsed === true
15130
15668
  });
15131
15669
  return;
15132
15670
  }
15671
+ if (name === "keybindings") {
15672
+ const p = payload;
15673
+ if (typeof p?.rev !== "number")
15674
+ return;
15675
+ this.setKeybindingsRevSig(p.rev);
15676
+ return;
15677
+ }
15678
+ }
15679
+ pruneEngineState(tasks) {
15680
+ const current = this.engineStateAcc();
15681
+ if (current.size === 0)
15682
+ return;
15683
+ const live = new Set(tasks.map((t) => t.id));
15684
+ let next = null;
15685
+ for (const key of current.keys()) {
15686
+ if (live.has(key))
15687
+ continue;
15688
+ if (!next)
15689
+ next = new Map(current);
15690
+ next.delete(key);
15691
+ }
15692
+ if (next)
15693
+ this.setEngineStateSig(next);
15694
+ }
15695
+ pruneTaskJobs(tasks) {
15696
+ const current = this.taskJobsAcc();
15697
+ if (current.size === 0)
15698
+ return;
15699
+ const live = new Set(tasks.map((t) => t.id));
15700
+ let next = null;
15701
+ for (const key of current.keys()) {
15702
+ if (live.has(key))
15703
+ continue;
15704
+ if (!next)
15705
+ next = new Map(current);
15706
+ next.delete(key);
15707
+ }
15708
+ if (next)
15709
+ this.setTaskJobsSig(next);
15133
15710
  }
15134
15711
  }
15135
15712
  function deserializeTask(s) {
@@ -15154,6 +15731,7 @@ var init_remote_orchestrator = __esm(() => {
15154
15731
  init_daemon_process();
15155
15732
  init_protocol();
15156
15733
  init_dev();
15734
+ init_worktree_changes();
15157
15735
  init_version();
15158
15736
  });
15159
15737
 
@@ -15523,10 +16101,6 @@ function resolveTheme(theme, mode = "dark") {
15523
16101
  ...out
15524
16102
  };
15525
16103
  }
15526
- function withAlpha(color, alpha) {
15527
- const [r, g, b] = color.toInts();
15528
- return RGBA.fromInts(r ?? 0, g ?? 0, b ?? 0, alpha);
15529
- }
15530
16104
  var BUNDLED_THEMES, FOCUS_ACCENT_SLOTS, store, setStore, useTheme, ThemeProvider;
15531
16105
  var init_theme2 = __esm(() => {
15532
16106
  init_solid();
@@ -15598,8 +16172,7 @@ var init_theme2 = __esm(() => {
15598
16172
  return {
15599
16173
  ...v,
15600
16174
  background: transparent,
15601
- backgroundPanel: transparent,
15602
- backgroundDialog: withAlpha(v.backgroundDialog, 128)
16175
+ backgroundPanel: transparent
15603
16176
  };
15604
16177
  });
15605
16178
  createEffect(() => {
@@ -15687,7 +16260,7 @@ function dispatchKeyEvent(bindingStack, evt) {
15687
16260
  continue;
15688
16261
  const hit = cfg.bindings.find((b) => candidates.includes(b.key));
15689
16262
  if (hit) {
15690
- hit.cmd(evt);
16263
+ hit.cmd(evt, hit.slot);
15691
16264
  evt.preventDefault();
15692
16265
  return true;
15693
16266
  }
@@ -16098,7 +16671,7 @@ function joinDrill(typedValue, baseExpanded, name) {
16098
16671
  var init_path_helpers = () => {};
16099
16672
 
16100
16673
  // src/tui/component/new-task-dialog/clone.ts
16101
- import { spawn as spawn3 } from "child_process";
16674
+ import { spawn as spawn4 } from "child_process";
16102
16675
  import * as fs5 from "fs";
16103
16676
  import * as path11 from "path";
16104
16677
  function deriveFolderName(url) {
@@ -16177,7 +16750,7 @@ function cloneRepo(url, target, onProgress) {
16177
16750
  return new Promise((resolve8) => {
16178
16751
  let stderrBuf = "";
16179
16752
  try {
16180
- const child = spawn3("git", ["clone", "--progress", url, target], {
16753
+ const child = spawn4("git", ["clone", "--progress", url, target], {
16181
16754
  stdio: ["ignore", "ignore", "pipe"]
16182
16755
  });
16183
16756
  child.stderr?.setEncoding("utf-8");
@@ -17673,6 +18246,19 @@ var init_focus = __esm(() => {
17673
18246
  });
17674
18247
 
17675
18248
  // src/tui/context/keybindings.ts
18249
+ function resetKeymapToDefaults() {
18250
+ for (const row of KobeKeymap) {
18251
+ const def = KEYMAP_DEFAULTS.get(row.id);
18252
+ if (!def)
18253
+ continue;
18254
+ const mutable = row;
18255
+ mutable.keys = [...def.keys];
18256
+ mutable.hint = def.hint ? { ...def.hint } : undefined;
18257
+ }
18258
+ }
18259
+ function bumpKeymapVersion() {
18260
+ setKeymapVersion((v) => v + 1);
18261
+ }
17676
18262
  function findBinding(id) {
17677
18263
  return KobeKeymap.find((b) => b.id === id);
17678
18264
  }
@@ -17690,20 +18276,20 @@ function bindByIds(handlers) {
17690
18276
  console.warn(`[kobe/keybindings] bindByIds: id="${id}" has no chords (or doesn't exist in KobeKeymap)`);
17691
18277
  continue;
17692
18278
  }
17693
- for (const c of chords)
17694
- out.push({ key: c, cmd });
18279
+ chords.forEach((c, slot) => out.push({ key: c, cmd, slot }));
17695
18280
  }
17696
18281
  return out;
17697
18282
  }
17698
- var KobeKeymap;
18283
+ var KobeKeymap, KEYMAP_DEFAULTS, keymapVersion, setKeymapVersion;
17699
18284
  var init_keybindings2 = __esm(() => {
18285
+ init_dev();
17700
18286
  KobeKeymap = [
17701
18287
  {
17702
18288
  id: "help.open",
17703
18289
  scope: "global",
17704
18290
  keys: ["f1"],
17705
18291
  category: "Global",
17706
- description: "Show this help dialog",
18292
+ description: "Show keybindings help",
17707
18293
  hint: { keys: "F1", label: "help", pin: "right" }
17708
18294
  },
17709
18295
  {
@@ -18154,6 +18740,8 @@ var init_keybindings2 = __esm(() => {
18154
18740
  hint: { keys: "ctrl+[/]", label: "tab" }
18155
18741
  }
18156
18742
  ];
18743
+ KEYMAP_DEFAULTS = new Map(KobeKeymap.map((b) => [b.id, { keys: [...b.keys], hint: b.hint ? { ...b.hint } : undefined }]));
18744
+ [keymapVersion, setKeymapVersion] = createSignal(0);
18157
18745
  });
18158
18746
 
18159
18747
  // src/tui/context/keybindings-user.ts
@@ -18201,6 +18789,15 @@ function applyUserKeybindings() {
18201
18789
  function userKeybindingsReport() {
18202
18790
  return cached3 ?? applyUserKeybindings();
18203
18791
  }
18792
+ function reloadUserKeybindings() {
18793
+ cached3 = null;
18794
+ resetKeybindingsFileCache();
18795
+ resetTmuxKeysCache();
18796
+ resetKeymapToDefaults();
18797
+ const report = applyUserKeybindings();
18798
+ bumpKeymapVersion();
18799
+ return report;
18800
+ }
18204
18801
  var cached3 = null;
18205
18802
  var init_keybindings_user = __esm(() => {
18206
18803
  init_keybindings_file();
@@ -18299,9 +18896,9 @@ var pulse_default = "../pulse-n3cq1btw.wav";
18299
18896
  var init_pulse = () => {};
18300
18897
 
18301
18898
  // src/tui/lib/sound.ts
18302
- import { existsSync as existsSync14, mkdirSync as mkdirSync5 } from "fs";
18899
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6 } from "fs";
18303
18900
  import { tmpdir as tmpdir2 } from "os";
18304
- import { basename as basename5, isAbsolute, join as join14, resolve as resolve8 } from "path";
18901
+ import { basename as basename6, isAbsolute, join as join15, resolve as resolve8 } from "path";
18305
18902
  function args(player, file, volume) {
18306
18903
  if (player === "ffplay")
18307
18904
  return [player, "-autoexit", "-nodisp", "-af", `volume=${volume}`, file];
@@ -18324,13 +18921,13 @@ function pickPlayer() {
18324
18921
  return cachedPlayer;
18325
18922
  const path12 = process.env.PATH ?? "";
18326
18923
  const segments = path12.split(":").filter(Boolean);
18327
- cachedPlayer = PLAYERS.find((p) => segments.some((dir) => existsSync14(join14(dir, p)))) ?? null;
18924
+ cachedPlayer = PLAYERS.find((p) => segments.some((dir) => existsSync14(join15(dir, p)))) ?? null;
18328
18925
  return cachedPlayer;
18329
18926
  }
18330
18927
  async function ensureAsset() {
18331
18928
  cachedPath ??= (async () => {
18332
- mkdirSync5(DIR, { recursive: true });
18333
- const dest = join14(DIR, basename5(pulseAsset));
18929
+ mkdirSync6(DIR, { recursive: true });
18930
+ const dest = join15(DIR, basename6(pulseAsset));
18334
18931
  const out = Bun.file(dest);
18335
18932
  if (await out.exists())
18336
18933
  return dest;
@@ -18360,7 +18957,7 @@ var pulseAsset, DIR, PLAYERS, cachedPlayer, cachedPath;
18360
18957
  var init_sound = __esm(() => {
18361
18958
  init_pulse();
18362
18959
  pulseAsset = isAbsolute(pulse_default) ? pulse_default : resolve8(import.meta.dir, pulse_default);
18363
- DIR = join14(tmpdir2(), "kobe-sfx");
18960
+ DIR = join15(tmpdir2(), "kobe-sfx");
18364
18961
  PLAYERS = [
18365
18962
  "ffplay",
18366
18963
  "mpv",
@@ -18584,20 +19181,28 @@ function UiPrefsSync(props) {
18584
19181
  focusAccent: props.boot.focusAccent
18585
19182
  });
18586
19183
  const [prefsOrch, setPrefsOrch] = createSignal(null);
19184
+ let disposed = false;
18587
19185
  onMount(() => {
18588
19186
  (async () => {
19187
+ let remote = null;
18589
19188
  try {
18590
19189
  const client = await connectIfRunning();
18591
19190
  if (!client) {
18592
19191
  logClient("ui-prefs", "no daemon \u2014 keeping boot-time visual prefs");
18593
19192
  return;
18594
19193
  }
18595
- const remote = new RemoteOrchestrator(client);
19194
+ remote = new RemoteOrchestrator(client);
18596
19195
  await remote.init();
18597
- setPrefsOrch(remote);
18598
19196
  } catch (err) {
18599
19197
  logClientError("ui-prefs", err);
19198
+ remote?.dispose();
19199
+ return;
18600
19200
  }
19201
+ if (disposed) {
19202
+ remote.dispose();
19203
+ return;
19204
+ }
19205
+ setPrefsOrch(remote);
18601
19206
  })();
18602
19207
  });
18603
19208
  createEffect(() => {
@@ -18605,7 +19210,22 @@ function UiPrefsSync(props) {
18605
19210
  if (payload)
18606
19211
  applyUiPrefs(target, payload);
18607
19212
  });
18608
- onCleanup(() => prefsOrch()?.dispose());
19213
+ let lastKeybindingsRev = null;
19214
+ createEffect(() => {
19215
+ const rev = prefsOrch()?.keybindingsRevSignal()();
19216
+ if (rev == null)
19217
+ return;
19218
+ if (lastKeybindingsRev === null || rev === lastKeybindingsRev) {
19219
+ lastKeybindingsRev = rev;
19220
+ return;
19221
+ }
19222
+ lastKeybindingsRev = rev;
19223
+ reloadUserKeybindings();
19224
+ });
19225
+ onCleanup(() => {
19226
+ disposed = true;
19227
+ prefsOrch()?.dispose();
19228
+ });
18609
19229
  return null;
18610
19230
  }
18611
19231
  async function bootPaneHost(opts) {
@@ -18830,6 +19450,31 @@ function repoBasename(repo) {
18830
19450
  function flattenIds(rows) {
18831
19451
  return rows.map((r) => r.task.id);
18832
19452
  }
19453
+ function sameSidebarRowTask(a, b) {
19454
+ return a === b || a.id === b.id && a.kind === b.kind && a.title === b.title && a.repo === b.repo && a.branch === b.branch && a.worktreePath === b.worktreePath && a.status === b.status && a.archived === b.archived && a.pinned === b.pinned && a.vendor === b.vendor;
19455
+ }
19456
+ function reconcileSidebarRows(prev, next) {
19457
+ if (prev.length === 0)
19458
+ return next;
19459
+ const prevById = new Map;
19460
+ for (const row of prev)
19461
+ prevById.set(row.task.id, row);
19462
+ let allReused = prev.length === next.length;
19463
+ const out = new Array(next.length);
19464
+ for (let i = 0;i < next.length; i++) {
19465
+ const fresh = next[i];
19466
+ const old = prevById.get(fresh.task.id);
19467
+ if (old && old.flatIndex === fresh.flatIndex && sameSidebarRowTask(old.task, fresh.task)) {
19468
+ out[i] = old;
19469
+ if (allReused && prev[i] !== old)
19470
+ allReused = false;
19471
+ } else {
19472
+ out[i] = fresh;
19473
+ allReused = false;
19474
+ }
19475
+ }
19476
+ return allReused ? prev : out;
19477
+ }
18833
19478
  var init_groups = () => {};
18834
19479
 
18835
19480
  // src/tui/quick-task/host.tsx
@@ -19107,12 +19752,13 @@ function groupBindings(keymap) {
19107
19752
  rows: groups.get(cat)
19108
19753
  }));
19109
19754
  }
19110
- function HelpDialog() {
19755
+ function HelpDialog(props = {}) {
19111
19756
  const dialog = useDialog();
19112
19757
  const {
19113
19758
  theme
19114
19759
  } = useTheme();
19115
19760
  const grouped = () => groupBindings(KobeKeymap);
19761
+ const close = () => props.onClose ? props.onClose() : dialog.clear();
19116
19762
  const [prefixGlyph, setPrefixGlyph] = createSignal("\u2303B");
19117
19763
  onMount(() => {
19118
19764
  runTmuxCapturing(["show-options", "-g", "prefix"]).then(({
@@ -19129,7 +19775,7 @@ function HelpDialog() {
19129
19775
  useBindings(() => ({
19130
19776
  bindings: [{
19131
19777
  key: "?",
19132
- cmd: () => dialog.clear()
19778
+ cmd: close
19133
19779
  }]
19134
19780
  }));
19135
19781
  return (() => {
@@ -19147,7 +19793,7 @@ function HelpDialog() {
19147
19793
  setProp(_el$2, "flexShrink", 0);
19148
19794
  insertNode(_el$3, createTextNode(`kobe \u2014 keybindings`));
19149
19795
  insertNode(_el$5, createTextNode(`esc`));
19150
- setProp(_el$5, "onMouseUp", () => dialog.clear());
19796
+ setProp(_el$5, "onMouseUp", close);
19151
19797
  insertNode(_el$7, _el$8);
19152
19798
  setProp(_el$7, "flexShrink", 1);
19153
19799
  setProp(_el$7, "flexGrow", 1);
@@ -19517,7 +20163,7 @@ var init_dialog_confirm = __esm(() => {
19517
20163
 
19518
20164
  // src/tui/component/settings-dialog/actions.ts
19519
20165
  import { unlinkSync as unlinkSync2 } from "fs";
19520
- import { join as join15 } from "path";
20166
+ import { join as join16 } from "path";
19521
20167
  function hasRestartableDaemon(orchestrator) {
19522
20168
  return orchestrator instanceof RemoteOrchestrator;
19523
20169
  }
@@ -19534,7 +20180,7 @@ async function confirmResetState(dialog, kv, renderer) {
19534
20180
  return;
19535
20181
  kv.clear();
19536
20182
  try {
19537
- unlinkSync2(join15(homeDir(), ".kobe", "tasks.json"));
20183
+ unlinkSync2(join16(homeDir(), ".kobe", "tasks.json"));
19538
20184
  } catch (err) {
19539
20185
  if (err.code !== "ENOENT") {
19540
20186
  console.error("kobe: failed to delete tasks.json during reset:", err);
@@ -21771,9 +22417,9 @@ var init_task_actions = __esm(() => {
21771
22417
  });
21772
22418
 
21773
22419
  // src/tui/lib/worktree-opener.ts
21774
- import { spawn as spawn4 } from "child_process";
22420
+ import { spawn as spawn5 } from "child_process";
21775
22421
  import { existsSync as existsSync15 } from "fs";
21776
- import { basename as basename6, delimiter, isAbsolute as isAbsolute2, join as join16 } from "path";
22422
+ import { basename as basename7, delimiter, isAbsolute as isAbsolute2, join as join17 } from "path";
21777
22423
  function executableOnPath(command, env, exists) {
21778
22424
  if (isAbsolute2(command))
21779
22425
  return exists(command);
@@ -21781,13 +22427,13 @@ function executableOnPath(command, env, exists) {
21781
22427
  for (const dir of pathEnv.split(delimiter)) {
21782
22428
  if (!dir)
21783
22429
  continue;
21784
- if (exists(join16(dir, command)))
22430
+ if (exists(join17(dir, command)))
21785
22431
  return true;
21786
22432
  }
21787
22433
  return false;
21788
22434
  }
21789
22435
  function labelForOverride(command) {
21790
- const name = basename6(command);
22436
+ const name = basename7(command);
21791
22437
  if (name === "code")
21792
22438
  return "VS Code";
21793
22439
  if (name === "cursor")
@@ -21830,7 +22476,7 @@ function buildOpenWorktreeCommand(worktreePath, opener) {
21830
22476
  function openWorktree(worktreePath, opener, deps = {}) {
21831
22477
  if (!worktreePath)
21832
22478
  return false;
21833
- const spawnFn = deps.spawn ?? spawn4;
22479
+ const spawnFn = deps.spawn ?? spawn5;
21834
22480
  const [command, args2] = buildOpenWorktreeCommand(worktreePath, opener);
21835
22481
  try {
21836
22482
  const child = spawnFn(command, args2, { detached: true, stdio: "ignore" });
@@ -21879,15 +22525,6 @@ var init_worktree_opener = __esm(() => {
21879
22525
  });
21880
22526
 
21881
22527
  // src/tui/lib/background-poll.ts
21882
- import { spawn as spawn5 } from "child_process";
21883
- function computeNextAllowedAt(startedAt, finishedAt, timedOut, cfg) {
21884
- if (timedOut)
21885
- return startedAt + cfg.slowRetryMs;
21886
- return finishedAt + Math.max(cfg.minIntervalMs, (finishedAt - startedAt) * 5);
21887
- }
21888
- function shouldPoll(state, now) {
21889
- return !state.inFlight && now >= state.nextAllowedAt;
21890
- }
21891
22528
  function createBackgroundPoller(cfg) {
21892
22529
  const entries = new Map;
21893
22530
  function entryFor(key) {
@@ -21909,58 +22546,17 @@ function createBackgroundPoller(cfg) {
21909
22546
  if (!key)
21910
22547
  return;
21911
22548
  const entry = entryFor(key);
21912
- const startedAt = Date.now();
21913
- if (!shouldPoll(entry, startedAt))
21914
- return;
21915
- entry.inFlight = true;
21916
- const controller = new AbortController;
21917
- const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
21918
- (async () => {
21919
- let value;
21920
- let ok = false;
21921
- try {
21922
- value = await cfg.run(key, controller.signal);
21923
- ok = true;
21924
- } catch {}
21925
- clearTimeout(timer);
21926
- const timedOut = controller.signal.aborted;
21927
- entry.nextAllowedAt = computeNextAllowedAt(startedAt, Date.now(), timedOut, cfg);
21928
- entry.inFlight = false;
21929
- if (ok && !timedOut)
21930
- entry.write(value);
21931
- })();
22549
+ maybeStartScheduledRun(entry, cfg, (signal) => cfg.run(key, signal), (value) => entry.write(value));
21932
22550
  },
21933
22551
  reset() {
21934
22552
  entries.clear();
21935
22553
  }
21936
22554
  };
21937
22555
  }
21938
- function spawnCapture(cmd, args2, opts) {
21939
- return new Promise((resolve9) => {
21940
- let out = "";
21941
- let settled = false;
21942
- const finish = (status) => {
21943
- if (settled)
21944
- return;
21945
- settled = true;
21946
- resolve9({ status, stdout: out });
21947
- };
21948
- const child = spawn5(cmd, args2.slice(), {
21949
- cwd: opts.cwd,
21950
- stdio: ["ignore", "pipe", "ignore"],
21951
- env: opts.env,
21952
- signal: opts.signal,
21953
- killSignal: "SIGKILL"
21954
- });
21955
- child.stdout?.on("data", (chunk) => {
21956
- out += String(chunk);
21957
- });
21958
- child.on("error", () => finish(null));
21959
- child.on("close", (code) => finish(code));
21960
- });
21961
- }
21962
22556
  var init_background_poll = __esm(() => {
21963
22557
  init_dev();
22558
+ init_poll_scheduling();
22559
+ init_poll_scheduling();
21964
22560
  });
21965
22561
 
21966
22562
  // src/tui/panes/sidebar/git-head.ts
@@ -22098,20 +22694,18 @@ function useSidebarBindings(opts) {
22098
22694
  useBindings(() => ({
22099
22695
  enabled: opts.focused() && !searchModeAccessor(),
22100
22696
  bindings: bindByIds({
22101
- "sidebar.nav": (evt) => {
22697
+ "sidebar.nav": (_evt, slot) => {
22698
+ const down = (slot ?? 0) % 2 === 0;
22102
22699
  if (moveModeAccessor()) {
22103
22700
  const id = cursorTaskId();
22104
22701
  if (id === undefined)
22105
22702
  return;
22106
- if (evt.name === "j" || evt.name === "down")
22107
- opts.onMoveRequest?.(id, 1);
22108
- else if (evt.name === "k" || evt.name === "up")
22109
- opts.onMoveRequest?.(id, -1);
22703
+ opts.onMoveRequest?.(id, down ? 1 : -1);
22110
22704
  return;
22111
22705
  }
22112
- if (evt.name === "j" || evt.name === "down")
22706
+ if (down)
22113
22707
  ctrl.moveDown();
22114
- else if (evt.name === "k" || evt.name === "up")
22708
+ else
22115
22709
  ctrl.moveUp();
22116
22710
  },
22117
22711
  "sidebar.select": () => {
@@ -22185,21 +22779,18 @@ function useSidebarBindings(opts) {
22185
22779
  useBindings(() => ({
22186
22780
  enabled: opts.focused(),
22187
22781
  bindings: bindByIds({
22188
- "sidebar.view": (evt) => {
22189
- if (evt.name === "]")
22190
- opts.onViewSwitch?.(1);
22191
- else
22192
- opts.onViewSwitch?.(-1);
22782
+ "sidebar.view": (_evt, slot) => {
22783
+ opts.onViewSwitch?.((slot ?? 0) % 2 === 0 ? -1 : 1);
22193
22784
  }
22194
22785
  })
22195
22786
  }));
22196
22787
  useBindings(() => ({
22197
22788
  enabled: opts.focused() && searchModeAccessor(),
22198
22789
  bindings: bindByIds({
22199
- "sidebar.search.nav": (evt) => {
22200
- if (evt.name === "down")
22790
+ "sidebar.search.nav": (_evt, slot) => {
22791
+ if ((slot ?? 0) % 2 === 0)
22201
22792
  ctrl.moveDown();
22202
- else if (evt.name === "up")
22793
+ else
22203
22794
  ctrl.moveUp();
22204
22795
  },
22205
22796
  "sidebar.search.submit": () => {
@@ -22255,11 +22846,12 @@ function buildSidebarRowView(opts) {
22255
22846
  const activityBadge = activityBadgeFor(activityState);
22256
22847
  const activityLabel = activityLabelFor(activityState);
22257
22848
  const untrackedCustomEngine = isCustomEngineTask(task) && !hasActivity;
22258
- const loading = !untrackedCustomEngine && (activityState === "running" || opts.live || !hasActivity && !isMain && task.status === "in_progress");
22849
+ const materializing = opts.job !== undefined;
22850
+ const loading = materializing || !untrackedCustomEngine && (activityState === "running" || opts.live || !hasActivity && !isMain && task.status === "in_progress");
22259
22851
  const spinner = IN_PROGRESS_SPINNER[opts.spinnerFrame] ?? IN_PROGRESS_SPINNER[0];
22260
- const tone = untrackedCustomEngine ? "textMuted" : activityLabel?.tone ?? (loading ? "primary" : activityBadge?.tone ?? badge.tone);
22852
+ const tone = materializing ? "primary" : untrackedCustomEngine ? "textMuted" : activityLabel?.tone ?? (loading ? "primary" : activityBadge?.tone ?? badge.tone);
22261
22853
  const fallbackSubtitle = untrackedCustomEngine ? NO_TRACKING_SUBTITLE : STATUS_LABEL[task.status];
22262
- const subtitleText = activityLabel ? opts.truncateBranch(activityLabel.text, opts.subtitleBudget) : branch.length > 0 ? opts.truncateBranch(branch, opts.subtitleBudget) : opts.truncateBranch(fallbackSubtitle, opts.subtitleBudget);
22854
+ const subtitleText = materializing ? opts.truncateBranch(MATERIALIZING_SUBTITLE, opts.subtitleBudget) : activityLabel ? opts.truncateBranch(activityLabel.text, opts.subtitleBudget) : branch.length > 0 ? opts.truncateBranch(branch, opts.subtitleBudget) : opts.truncateBranch(fallbackSubtitle, opts.subtitleBudget);
22263
22855
  const restGlyph = untrackedCustomEngine ? NO_TRACKING_GLYPH : activityBadge?.glyph ?? badge.glyph;
22264
22856
  const restProjectGlyph = untrackedCustomEngine ? NO_TRACKING_GLYPH : activityBadge?.glyph ?? "\u2605";
22265
22857
  return {
@@ -22286,7 +22878,7 @@ function activityBadgeFor(state) {
22286
22878
  return null;
22287
22879
  }
22288
22880
  }
22289
- var STATUS_BADGE, STATUS_LABEL, IN_PROGRESS_SPINNER, SPINNER_FRAME_MS = 100, NO_TRACKING_GLYPH = "\xB7", NO_TRACKING_SUBTITLE = "no activity tracking";
22881
+ var STATUS_BADGE, STATUS_LABEL, IN_PROGRESS_SPINNER, SPINNER_FRAME_MS = 100, NO_TRACKING_GLYPH = "\xB7", NO_TRACKING_SUBTITLE = "no activity tracking", MATERIALIZING_SUBTITLE = "materializing";
22290
22882
  var init_row_view = __esm(() => {
22291
22883
  init_vendor();
22292
22884
  init_groups();
@@ -22324,7 +22916,7 @@ var init_worktree_changes_poller = __esm(() => {
22324
22916
  ZERO2 = { added: 0, deleted: 0 };
22325
22917
  poller2 = createBackgroundPoller({
22326
22918
  initial: ZERO2,
22327
- equals: (a, b) => a.added === b.added && a.deleted === b.deleted,
22919
+ equals: sameWorktreeChanges,
22328
22920
  timeoutMs: POLL_TIMEOUT_MS,
22329
22921
  slowRetryMs: SLOW_REPO_RETRY_MS,
22330
22922
  minIntervalMs: MIN_POLL_INTERVAL_MS,
@@ -22406,12 +22998,13 @@ function Sidebar(props) {
22406
22998
  onCleanup(() => r.keyInput.off("keypress", listener2));
22407
22999
  });
22408
23000
  const [branchTick, setBranchTick] = createSignal(0);
22409
- setInterval(() => setBranchTick((n) => n + 1), MAIN_BRANCH_POLL_MS);
23001
+ const branchInterval = setInterval(() => setBranchTick((n) => n + 1), MAIN_BRANCH_POLL_MS);
23002
+ onCleanup(() => clearInterval(branchInterval));
22410
23003
  const [spinnerFrame, setSpinnerFrame] = createSignal(0);
22411
23004
  const spinnerInterval = setInterval(() => setSpinnerFrame((n) => (n + 1) % IN_PROGRESS_SPINNER.length), SPINNER_FRAME_MS);
22412
23005
  onCleanup(() => clearInterval(spinnerInterval));
22413
23006
  const sortMode = () => props.sortMode?.() ?? "default";
22414
- const rows = createMemo(() => buildRows(props.tasks(), view(), searchMode() ? searchQuery() : "", sortMode()));
23007
+ const rows = createMemo((prev) => reconcileSidebarRows(prev, buildRows(props.tasks(), view(), searchMode() ? searchQuery() : "", sortMode())), []);
22415
23008
  const flatIds = createMemo(() => flattenIds(rows()));
22416
23009
  const firstTaskFlatIndex = createMemo(() => {
22417
23010
  const r = rows();
@@ -22760,10 +23353,15 @@ function Sidebar(props) {
22760
23353
  }
22761
23354
  };
22762
23355
  const changes = createMemo(() => {
23356
+ const pushed = pickPushedChanges(props.worktreeChanges?.(), task.worktreePath);
23357
+ if (pushed)
23358
+ return pushed;
22763
23359
  branchTick();
22764
23360
  if (!task.archived)
22765
23361
  pollWorktreeChanges(task.worktreePath);
22766
23362
  return worktreeChanges(task.worktreePath);
23363
+ }, undefined, {
23364
+ equals: sameWorktreeChanges
22767
23365
  });
22768
23366
  const projectBranch = createMemo(() => {
22769
23367
  branchTick();
@@ -22774,6 +23372,7 @@ function Sidebar(props) {
22774
23372
  const rowView = createMemo(() => buildSidebarRowView({
22775
23373
  task,
22776
23374
  activity: props.engineState?.().get(task.id),
23375
+ job: props.taskJobs?.().get(task.id),
22777
23376
  live: isLive(),
22778
23377
  spinnerFrame: spinnerFrame(),
22779
23378
  subtitleBudget: subtitleBudget(),
@@ -23206,6 +23805,7 @@ var init_Sidebar = __esm(() => {
23206
23805
  init_groups();
23207
23806
  init_keys();
23208
23807
  init_row_view();
23808
+ init_worktree_changes();
23209
23809
  init_worktree_changes_poller();
23210
23810
  VIEW_TABS = [{
23211
23811
  view: "active",
@@ -23251,7 +23851,7 @@ function TasksShell(props) {
23251
23851
  });
23252
23852
  }
23253
23853
  const [moveMode, setMoveMode] = createSignal(false);
23254
- const [sortMode, setSortMode] = createSignal("default");
23854
+ const [sortMode, setSortMode] = createSignal(kv.get("activeSortMode") === "recent" ? "recent" : "default");
23255
23855
  const [updateInfo, setUpdateInfo] = createSignal(null);
23256
23856
  const dimensions = useTerminalDimensions();
23257
23857
  let activeConfirmed = !props.initialTaskId;
@@ -23266,6 +23866,11 @@ function TasksShell(props) {
23266
23866
  }
23267
23867
  setSelectedId(active);
23268
23868
  });
23869
+ createEffect(() => {
23870
+ const payload = props.orch?.uiPrefsSignal()();
23871
+ if (payload && payload.sortMode !== sortMode())
23872
+ setSortMode(payload.sortMode);
23873
+ });
23269
23874
  createEffect(() => {
23270
23875
  const info = props.orch?.updateSignal()();
23271
23876
  if (info)
@@ -23333,6 +23938,14 @@ function TasksShell(props) {
23333
23938
  console.error("[kobe tasks] failed to refresh workspace panes:", err);
23334
23939
  }
23335
23940
  }
23941
+ async function openHelp() {
23942
+ const session = await currentSessionName();
23943
+ if (session) {
23944
+ await openHelpTab(session);
23945
+ return;
23946
+ }
23947
+ HelpDialog.show(dialog);
23948
+ }
23336
23949
  async function openUpdate() {
23337
23950
  const info = updateInfo();
23338
23951
  if (!info?.hasUpdate) {
@@ -23404,13 +24017,21 @@ function TasksShell(props) {
23404
24017
  setSelectedId(id);
23405
24018
  await props.reload();
23406
24019
  }
23407
- const keysCollapsed = () => kv.get("tasksPane.keysCollapsed", false) === true;
23408
- const setKeysCollapsed = (next) => kv.set("tasksPane.keysCollapsed", next);
24020
+ const [keysCollapsed, setKeysCollapsedSig] = createSignal(kv.get("tasksPane.keysCollapsed", false) === true);
24021
+ const setKeysCollapsed = (next) => {
24022
+ setKeysCollapsedSig(next);
24023
+ kv.set("tasksPane.keysCollapsed", next);
24024
+ };
24025
+ createEffect(() => {
24026
+ const payload = props.orch?.uiPrefsSignal()();
24027
+ if (payload && payload.keysCollapsed !== keysCollapsed())
24028
+ setKeysCollapsedSig(payload.keysCollapsed);
24029
+ });
23409
24030
  const [searchActive, setSearchActive] = createSignal(false);
23410
24031
  useBindings(() => ({
23411
24032
  enabled: dialog.stack.length === 0 && !searchActive(),
23412
24033
  bindings: bindByIds({
23413
- "help.open": () => HelpDialog.show(dialog),
24034
+ "help.open": () => void openHelp(),
23414
24035
  "task.new": () => void createTask(),
23415
24036
  "settings.open.sidebar": () => void openSettings(),
23416
24037
  "tasks.update": () => void openUpdate(),
@@ -23435,6 +24056,9 @@ function TasksShell(props) {
23435
24056
  async function switchTo(id) {
23436
24057
  const name = tmuxSessionName(id);
23437
24058
  const task = props.tasks().find((t) => t.id === id);
24059
+ const from = await currentSessionName();
24060
+ if (from && from !== name)
24061
+ await captureGlobalLayout(from);
23438
24062
  const exists = await sessionExists(name);
23439
24063
  if (exists) {
23440
24064
  const cwd2 = await getSessionOption(name, "@kobe_worktree") || task?.worktreePath || "";
@@ -23532,6 +24156,17 @@ function TasksShell(props) {
23532
24156
  get engineState() {
23533
24157
  return memo2(() => !!props.orch)() ? props.orch.engineStateSignal() : undefined;
23534
24158
  },
24159
+ get taskJobs() {
24160
+ return memo2(() => !!props.orch)() ? props.orch.taskJobsSignal() : undefined;
24161
+ },
24162
+ get worktreeChanges() {
24163
+ return props.orch ? () => {
24164
+ const orch = props.orch;
24165
+ if (!orch || orch.connectionStateSignal()() !== "online")
24166
+ return null;
24167
+ return orch.worktreeChangesSignal()();
24168
+ } : undefined;
24169
+ },
23535
24170
  onRenameRequest: (id) => void renameTask(id),
23536
24171
  onDeleteRequest: (id) => void deleteTask(id),
23537
24172
  onArchiveRequest: (id) => void archiveTask(id),
@@ -23546,7 +24181,11 @@ function TasksShell(props) {
23546
24181
  onMoveRequest: (id, delta) => void moveTask(id, delta),
23547
24182
  onMoveModeExit: () => setMoveMode(false),
23548
24183
  sortMode,
23549
- onSortModeToggle: () => setSortMode((cur) => cur === "default" ? "recent" : "default"),
24184
+ onSortModeToggle: () => {
24185
+ const next = sortMode() === "default" ? "recent" : "default";
24186
+ setSortMode(next);
24187
+ kv.set("activeSortMode", next);
24188
+ },
23550
24189
  focused: () => dialog.stack.length === 0,
23551
24190
  onSearchActiveChange: setSearchActive
23552
24191
  }));
@@ -23578,6 +24217,7 @@ function ShortcutHints(props) {
23578
24217
  });
23579
24218
  });
23580
24219
  const tmuxHints = () => {
24220
+ keymapVersion();
23581
24221
  const res = resolveUserTmuxKeys();
23582
24222
  const b = res.binds;
23583
24223
  const out = [];
@@ -23985,9 +24625,73 @@ var init_host4 = __esm(() => {
23985
24625
  init_dialog();
23986
24626
  });
23987
24627
 
23988
- // src/tui/update/host.tsx
24628
+ // src/tui/help/host.tsx
23989
24629
  var exports_host5 = {};
23990
24630
  __export(exports_host5, {
24631
+ startHelpHost: () => startHelpHost
24632
+ });
24633
+ function HelpPage() {
24634
+ const dialog = useDialog();
24635
+ const {
24636
+ theme
24637
+ } = useTheme();
24638
+ function exit() {
24639
+ process.exit(0);
24640
+ }
24641
+ useBindings(() => ({
24642
+ enabled: dialog.stack.length === 0,
24643
+ bindings: [{
24644
+ key: "escape",
24645
+ cmd: exit
24646
+ }, {
24647
+ key: "q",
24648
+ cmd: exit
24649
+ }, {
24650
+ key: "f1",
24651
+ cmd: exit
24652
+ }, {
24653
+ key: "ctrl+c",
24654
+ cmd: exit
24655
+ }]
24656
+ }));
24657
+ return (() => {
24658
+ var _el$ = createElement("box");
24659
+ setProp(_el$, "flexGrow", 1);
24660
+ setProp(_el$, "paddingTop", 1);
24661
+ insert(_el$, createComponent2(HelpDialog, {
24662
+ onClose: exit
24663
+ }));
24664
+ effect((_$p) => setProp(_el$, "backgroundColor", theme.background, _$p));
24665
+ return _el$;
24666
+ })();
24667
+ }
24668
+ async function startHelpHost() {
24669
+ await bootPaneHost({
24670
+ providers: {
24671
+ kv: false,
24672
+ focus: false
24673
+ },
24674
+ setup: () => ({
24675
+ root: () => createComponent2(HelpPage, {})
24676
+ })
24677
+ });
24678
+ }
24679
+ var init_host5 = __esm(() => {
24680
+ init_solid();
24681
+ init_solid();
24682
+ init_solid();
24683
+ init_solid();
24684
+ init_solid();
24685
+ init_help_dialog();
24686
+ init_theme2();
24687
+ init_host_boot();
24688
+ init_keymap();
24689
+ init_dialog();
24690
+ });
24691
+
24692
+ // src/tui/update/host.tsx
24693
+ var exports_host6 = {};
24694
+ __export(exports_host6, {
23991
24695
  startUpdateHost: () => startUpdateHost
23992
24696
  });
23993
24697
  import { spawn as spawn6, spawnSync as spawnSync11 } from "child_process";
@@ -24342,7 +25046,7 @@ async function startUpdateHost() {
24342
25046
  })
24343
25047
  });
24344
25048
  }
24345
- var init_host5 = __esm(() => {
25049
+ var init_host6 = __esm(() => {
24346
25050
  init_solid();
24347
25051
  init_solid();
24348
25052
  init_solid();
@@ -24666,26 +25370,24 @@ function useFileTreeBindings(opts) {
24666
25370
  useBindings(() => ({
24667
25371
  enabled: opts.focused(),
24668
25372
  bindings: bindByIds({
24669
- "files.nav": (evt) => {
24670
- if (evt.name === "j" || evt.name === "down")
25373
+ "files.nav": (_evt, slot) => {
25374
+ if ((slot ?? 0) % 2 === 0)
24671
25375
  opts.moveDown();
24672
- else if (evt.name === "k" || evt.name === "up")
25376
+ else
24673
25377
  opts.moveUp();
24674
25378
  },
24675
- "files.hierarchy": (evt) => {
24676
- if (evt.name === "l" || evt.name === "right")
24677
- opts.expandOrDescend();
24678
- else if (evt.name === "h" || evt.name === "left")
25379
+ "files.hierarchy": (_evt, slot) => {
25380
+ if ((slot ?? 0) % 2 === 0)
24679
25381
  opts.collapseOrParent();
25382
+ else
25383
+ opts.expandOrDescend();
24680
25384
  },
24681
- "files.tab": (evt) => {
25385
+ "files.tab": (_evt, slot) => {
24682
25386
  const cur = opts.currentTab();
24683
25387
  const idx = TAB_ORDER.indexOf(cur);
24684
25388
  if (idx < 0)
24685
25389
  return;
24686
- const delta = evt.name === "[" ? -1 : evt.name === "]" ? 1 : 0;
24687
- if (delta === 0)
24688
- return;
25390
+ const delta = (slot ?? 0) % 2 === 0 ? -1 : 1;
24689
25391
  const next = TAB_ORDER[(idx + delta + TAB_ORDER.length) % TAB_ORDER.length];
24690
25392
  if (next)
24691
25393
  opts.setTab(next);
@@ -24753,8 +25455,109 @@ function spawnDetached(cmd, args2, onError) {
24753
25455
  }
24754
25456
  var init_open_external = () => {};
24755
25457
 
25458
+ // src/tui/panes/filetree/rows.ts
25459
+ function flattenTree(node, expanded, depth, out) {
25460
+ for (const child of node.children) {
25461
+ if (child.isDir) {
25462
+ const isOpen = expanded.has(child.path);
25463
+ out.push({
25464
+ kind: "dir",
25465
+ path: child.path,
25466
+ name: child.name,
25467
+ depth,
25468
+ expanded: isOpen,
25469
+ hasChildren: child.children.length > 0
25470
+ });
25471
+ if (isOpen)
25472
+ flattenTree(child, expanded, depth + 1, out);
25473
+ } else {
25474
+ out.push({ kind: "file", path: child.path, name: child.name, depth });
25475
+ }
25476
+ }
25477
+ }
25478
+ function statusRows(entries) {
25479
+ return entries.map((e) => ({
25480
+ kind: "status",
25481
+ path: e.path,
25482
+ status: e.status,
25483
+ added: e.added,
25484
+ deleted: e.deleted
25485
+ }));
25486
+ }
25487
+ function rowKey(row) {
25488
+ return `${row.kind}\x00${row.path}`;
25489
+ }
25490
+ function rowEquals(a, b) {
25491
+ if (a.kind !== b.kind || a.path !== b.path)
25492
+ return false;
25493
+ switch (a.kind) {
25494
+ case "file": {
25495
+ const o = b;
25496
+ return a.name === o.name && a.depth === o.depth;
25497
+ }
25498
+ case "dir": {
25499
+ const o = b;
25500
+ return a.name === o.name && a.depth === o.depth && a.expanded === o.expanded && a.hasChildren === o.hasChildren;
25501
+ }
25502
+ case "status": {
25503
+ const o = b;
25504
+ return a.status === o.status && a.added === o.added && a.deleted === o.deleted;
25505
+ }
25506
+ }
25507
+ }
25508
+ function reconcileRows(prev, next) {
25509
+ if (prev.length === 0)
25510
+ return next;
25511
+ const prevByKey = new Map;
25512
+ for (const row of prev)
25513
+ prevByKey.set(rowKey(row), row);
25514
+ let allReused = prev.length === next.length;
25515
+ const out = new Array(next.length);
25516
+ for (let i = 0;i < next.length; i++) {
25517
+ const fresh = next[i];
25518
+ const old = prevByKey.get(rowKey(fresh));
25519
+ if (old && rowEquals(old, fresh)) {
25520
+ out[i] = old;
25521
+ if (allReused && prev[i] !== old)
25522
+ allReused = false;
25523
+ } else {
25524
+ out[i] = fresh;
25525
+ allReused = false;
25526
+ }
25527
+ }
25528
+ return allReused ? prev : out;
25529
+ }
25530
+ function sameFileList(a, b) {
25531
+ if (a === b)
25532
+ return true;
25533
+ if (a == null || b == null)
25534
+ return false;
25535
+ if (a.length !== b.length)
25536
+ return false;
25537
+ for (let i = 0;i < a.length; i++) {
25538
+ if (a[i] !== b[i])
25539
+ return false;
25540
+ }
25541
+ return true;
25542
+ }
25543
+ function sameStatusEntries(a, b) {
25544
+ if (a === b)
25545
+ return true;
25546
+ if (a == null || b == null)
25547
+ return false;
25548
+ if (a.length !== b.length)
25549
+ return false;
25550
+ for (let i = 0;i < a.length; i++) {
25551
+ const x = a[i];
25552
+ const y = b[i];
25553
+ if (x.path !== y.path || x.status !== y.status || x.added !== y.added || x.deleted !== y.deleted)
25554
+ return false;
25555
+ }
25556
+ return true;
25557
+ }
25558
+
24756
25559
  // src/tui/panes/filetree/FileTree.tsx
24757
- import { watch as watch2 } from "fs";
25560
+ import { watch as watch3 } from "fs";
24758
25561
  import { TextAttributes as TextAttributes14 } from "@opentui/core";
24759
25562
  function statusToken(s) {
24760
25563
  switch (s) {
@@ -24801,8 +25604,12 @@ function FileTree(props) {
24801
25604
  const [tab, setTab] = createSignal("all");
24802
25605
  const [cursorIndex, setCursorIndex] = createSignal(0);
24803
25606
  const [refreshTick, setRefreshTick] = createSignal(0);
24804
- const [allFiles, setAllFiles] = createSignal(null);
24805
- const [changes, setChanges] = createSignal(null);
25607
+ const [allFiles, setAllFiles] = createSignal(null, {
25608
+ equals: sameFileList
25609
+ });
25610
+ const [changes, setChanges] = createSignal(null, {
25611
+ equals: sameStatusEntries
25612
+ });
24806
25613
  const [error, setError] = createSignal(null);
24807
25614
  const [expandedDirs, setExpandedDirs] = createSignal(new Set);
24808
25615
  let fetchSeq = 0;
@@ -24849,7 +25656,7 @@ function FileTree(props) {
24849
25656
  let debounceTimer = null;
24850
25657
  let watcher = null;
24851
25658
  try {
24852
- watcher = watch2(path12, {
25659
+ watcher = watch3(path12, {
24853
25660
  recursive: true
24854
25661
  }, (_event, filename) => {
24855
25662
  if (filename == null)
@@ -24898,53 +25705,19 @@ function FileTree(props) {
24898
25705
  return null;
24899
25706
  return buildTree(files);
24900
25707
  });
24901
- function flattenTree(node, expanded, depth, out) {
24902
- for (const child of node.children) {
24903
- if (child.isDir) {
24904
- const isOpen = expanded.has(child.path);
24905
- out.push({
24906
- kind: "dir",
24907
- path: child.path,
24908
- name: child.name,
24909
- depth,
24910
- expanded: isOpen,
24911
- hasChildren: child.children.length > 0
24912
- });
24913
- if (isOpen)
24914
- flattenTree(child, expanded, depth + 1, out);
24915
- } else {
24916
- out.push({
24917
- kind: "file",
24918
- path: child.path,
24919
- name: child.name,
24920
- depth
24921
- });
24922
- }
24923
- }
24924
- }
24925
- const rows = createMemo(() => {
25708
+ const rows = createMemo((prev) => {
25709
+ const next = [];
24926
25710
  if (tab() === "all") {
24927
25711
  const root = tree();
24928
- if (root == null)
24929
- return [];
24930
- const out = [];
24931
- flattenTree(root, expandedDirs(), 0, out);
24932
- return out;
24933
- }
24934
- if (tab() === "changes") {
25712
+ if (root != null)
25713
+ flattenTree(root, expandedDirs(), 0, next);
25714
+ } else if (tab() === "changes") {
24935
25715
  const list2 = changes();
24936
- if (list2 == null)
24937
- return [];
24938
- return list2.map((e) => ({
24939
- kind: "status",
24940
- path: e.path,
24941
- status: e.status,
24942
- added: e.added,
24943
- deleted: e.deleted
24944
- }));
25716
+ if (list2 != null)
25717
+ next.push(...statusRows(list2));
24945
25718
  }
24946
- return [];
24947
- });
25719
+ return reconcileRows(prev ?? [], next);
25720
+ }, []);
24948
25721
  const statWidths = createMemo(() => {
24949
25722
  let added = 0;
24950
25723
  let deleted = 0;
@@ -25631,8 +26404,8 @@ var init_pr_prompt = __esm(() => {
25631
26404
  });
25632
26405
 
25633
26406
  // src/tui/ops/host.tsx
25634
- var exports_host6 = {};
25635
- __export(exports_host6, {
26407
+ var exports_host7 = {};
26408
+ __export(exports_host7, {
25636
26409
  startOpsPreview: () => startOpsPreview,
25637
26410
  startOpsHost: () => startOpsHost
25638
26411
  });
@@ -25738,7 +26511,7 @@ function OpsShell(props) {
25738
26511
  relPath: rel,
25739
26512
  cliInvocation: kobeCliInvocation()
25740
26513
  }),
25741
- name: basename7(rel)
26514
+ name: basename8(rel)
25742
26515
  });
25743
26516
  }
25744
26517
  function openFile(rel) {
@@ -25781,7 +26554,7 @@ function OpsShell(props) {
25781
26554
  return _el$;
25782
26555
  })();
25783
26556
  }
25784
- function basename7(p) {
26557
+ function basename8(p) {
25785
26558
  const i = p.lastIndexOf("/");
25786
26559
  return i >= 0 ? p.slice(i + 1) : p;
25787
26560
  }
@@ -26026,7 +26799,7 @@ async function startOpsPreview(args2) {
26026
26799
  });
26027
26800
  }
26028
26801
  var ACTIVITY_POLL_MS = 2500, TURN_STATUS_POLL_MS = 1500, STABLE_POLLS_FOR_DONE = 2, CHAT_TAB_STATE_OPTION2 = "@kobe_tab_state";
26029
- var init_host6 = __esm(() => {
26802
+ var init_host7 = __esm(() => {
26030
26803
  init_solid();
26031
26804
  init_solid();
26032
26805
  init_solid();
@@ -26124,6 +26897,7 @@ async function startDirectTmux() {
26124
26897
  if (!task) {
26125
26898
  const home = await ensureFallbackSession();
26126
26899
  await applyTmuxPaneBorderTheme();
26900
+ await prepareWindowForAttach(home);
26127
26901
  if (await attachTmux(attachArgv(home)) === null) {
26128
26902
  console.error("kobe: failed to attach to the kobe-home session");
26129
26903
  process.exitCode = 1;
@@ -26151,6 +26925,7 @@ async function startDirectTmux() {
26151
26925
  return;
26152
26926
  }
26153
26927
  await applyTmuxPaneBorderTheme();
26928
+ await prepareWindowForAttach(name);
26154
26929
  const exitCode = await attachTmux(attachArgv(name));
26155
26930
  if (exitCode === null) {
26156
26931
  console.error(`kobe: failed to attach to tmux session ${name}`);
@@ -26576,6 +27351,17 @@ async function main() {
26576
27351
  await selectTasksPane2(session);
26577
27352
  return;
26578
27353
  }
27354
+ if (subcommand === "heal-layout") {
27355
+ const flags = parseOpsFlags(rest);
27356
+ const session = flags.session;
27357
+ if (!session) {
27358
+ console.error("kobe heal-layout: --session <name> is required");
27359
+ process.exit(2);
27360
+ }
27361
+ const { healSessionLayout: healSessionLayout2 } = await Promise.resolve().then(() => (init_tmux(), exports_tmux));
27362
+ await healSessionLayout2(session);
27363
+ return;
27364
+ }
26579
27365
  if (subcommand === "quick-task") {
26580
27366
  const flags = parseOpsFlags(rest);
26581
27367
  const { startQuickTaskHost: startQuickTaskHost2 } = await Promise.resolve().then(() => (init_host2(), exports_host2));
@@ -26593,6 +27379,11 @@ async function main() {
26593
27379
  await startSettingsHost2();
26594
27380
  return;
26595
27381
  }
27382
+ if (subcommand === "help-page") {
27383
+ const { startHelpHost: startHelpHost2 } = await Promise.resolve().then(() => (init_host5(), exports_host5));
27384
+ await startHelpHost2();
27385
+ return;
27386
+ }
26596
27387
  if (subcommand === "new-task") {
26597
27388
  const flags = parseOpsFlags(rest);
26598
27389
  const { startNewTaskHost: startNewTaskHost2 } = await Promise.resolve().then(() => (init_host(), exports_host));
@@ -26600,7 +27391,7 @@ async function main() {
26600
27391
  return;
26601
27392
  }
26602
27393
  if (subcommand === "update-page") {
26603
- const { startUpdateHost: startUpdateHost2 } = await Promise.resolve().then(() => (init_host5(), exports_host5));
27394
+ const { startUpdateHost: startUpdateHost2 } = await Promise.resolve().then(() => (init_host6(), exports_host6));
26604
27395
  await startUpdateHost2();
26605
27396
  return;
26606
27397
  }
@@ -26611,11 +27402,11 @@ async function main() {
26611
27402
  process.exit(2);
26612
27403
  }
26613
27404
  if (flags.preview) {
26614
- const { startOpsPreview: startOpsPreview2 } = await Promise.resolve().then(() => (init_host6(), exports_host6));
27405
+ const { startOpsPreview: startOpsPreview2 } = await Promise.resolve().then(() => (init_host7(), exports_host7));
26615
27406
  await startOpsPreview2({ worktree: flags.worktree, relPath: flags.preview });
26616
27407
  return;
26617
27408
  }
26618
- const { startOpsHost: startOpsHost2 } = await Promise.resolve().then(() => (init_host6(), exports_host6));
27409
+ const { startOpsHost: startOpsHost2 } = await Promise.resolve().then(() => (init_host7(), exports_host7));
26619
27410
  await startOpsHost2({
26620
27411
  taskId: flags.taskId ?? "",
26621
27412
  worktree: flags.worktree,