@sma1lboy/kobe 0.7.17 → 0.7.18

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 +601 -150
  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.18",
94
94
  description: "TUI orchestrator for Claude Code (codename)",
95
95
  type: "module",
96
96
  packageManager: "bun@1.3.13",
@@ -3918,7 +3918,8 @@ var init_protocol = __esm(() => {
3918
3918
  "active-task",
3919
3919
  "update",
3920
3920
  "engine-state",
3921
- "ui-prefs"
3921
+ "ui-prefs",
3922
+ "keybindings"
3922
3923
  ];
3923
3924
  });
3924
3925
 
@@ -5131,6 +5132,16 @@ function kobeCliInvocation() {
5131
5132
  var init_invocation = () => {};
5132
5133
 
5133
5134
  // src/tmux/session-layout.ts
5135
+ function clampTasksPaneWidth(width) {
5136
+ if (!Number.isFinite(width))
5137
+ return TASKS_PANE_WIDTH;
5138
+ return Math.max(TASKS_PANE_WIDTH_MIN, Math.min(TASKS_PANE_WIDTH_MAX, Math.round(width)));
5139
+ }
5140
+ function clampPanePercent(percent) {
5141
+ if (!Number.isFinite(percent))
5142
+ return null;
5143
+ return Math.max(PANE_PERCENT_MIN, Math.min(PANE_PERCENT_MAX, Math.round(percent)));
5144
+ }
5134
5145
  function shellQuote(s) {
5135
5146
  return `'${s.replace(/'/g, "'\\''")}'`;
5136
5147
  }
@@ -5201,7 +5212,7 @@ function opsPaneCommand(args) {
5201
5212
  }
5202
5213
  return fallbackOpsScript(args.cwd);
5203
5214
  }
5204
- var TASKS_PANE_WIDTH = 32, CLAUDE_PANE_PERCENT = 60, OPS_PANE_PERCENT = 50;
5215
+ 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
5216
 
5206
5217
  // src/engine/claude-code-local/hook-adapter.ts
5207
5218
  import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
@@ -6317,8 +6328,10 @@ __export(exports_client, {
6317
6328
  paneIdByRole: () => paneIdByRole,
6318
6329
  newWindow: () => newWindow,
6319
6330
  killSession: () => killSession,
6331
+ globalTasksPaneWidth: () => globalTasksPaneWidth,
6320
6332
  getSessionOptions: () => getSessionOptions,
6321
6333
  getSessionOption: () => getSessionOption,
6334
+ getServerOption: () => getServerOption,
6322
6335
  ensureFallbackSession: () => ensureFallbackSession,
6323
6336
  currentSessionName: () => currentSessionName,
6324
6337
  claudePaneIdStrict: () => claudePaneIdStrict,
@@ -6452,6 +6465,15 @@ async function getSessionOptions(session, options) {
6452
6465
  }
6453
6466
  return values;
6454
6467
  }
6468
+ async function getServerOption(option) {
6469
+ const { code, stdout } = await runTmuxCapturing(["show-options", "-sqv", option]);
6470
+ return code === 0 ? stdout.trim() : "";
6471
+ }
6472
+ async function globalTasksPaneWidth() {
6473
+ const raw = await getServerOption(TASKS_WIDTH_OPTION);
6474
+ const n = Number.parseInt(raw, 10);
6475
+ return Number.isFinite(n) && n > 0 ? clampTasksPaneWidth(n) : TASKS_PANE_WIDTH;
6476
+ }
6455
6477
  async function tagPaneRole(paneId, role) {
6456
6478
  await runTmux(["set-option", "-p", "-t", paneId, PANE_ROLE_OPTION, role]);
6457
6479
  }
@@ -6551,6 +6573,7 @@ async function ensureFallbackSession() {
6551
6573
  ]);
6552
6574
  const mainPane = r0.stdout.trim();
6553
6575
  if (mainPane) {
6576
+ const tasksWidth = await globalTasksPaneWidth();
6554
6577
  const r1 = await runTmuxCapturing([
6555
6578
  "split-window",
6556
6579
  "-h",
@@ -6558,7 +6581,7 @@ async function ensureFallbackSession() {
6558
6581
  "-t",
6559
6582
  mainPane,
6560
6583
  "-l",
6561
- `${TASKS_PANE_WIDTH}`,
6584
+ `${tasksWidth}`,
6562
6585
  "-c",
6563
6586
  SAFE_SPAWN_CWD,
6564
6587
  "-P",
@@ -7181,12 +7204,66 @@ var init_handlers = __esm(() => {
7181
7204
  init_protocol();
7182
7205
  });
7183
7206
 
7184
- // ../kobe-daemon/src/daemon/ui-prefs-watcher.ts
7185
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, watch } from "fs";
7207
+ // ../kobe-daemon/src/daemon/keybindings-watcher.ts
7208
+ import { mkdirSync as mkdirSync2, watch } from "fs";
7186
7209
  import { homedir as homedir13 } from "os";
7187
7210
  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");
7211
+ function defaultKeybindingsPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir13()) {
7212
+ return join4(homeDir2, ".kobe", "settings", "keybindings.yaml");
7213
+ }
7214
+ function startKeybindingsWatcher(bus, options = {}) {
7215
+ const debounceMs = options.debounceMs ?? DEFAULT_KEYBINDINGS_DEBOUNCE_MS;
7216
+ if (debounceMs <= 0)
7217
+ return () => {};
7218
+ const filePath = options.path ?? defaultKeybindingsPath();
7219
+ const dir = dirname5(filePath);
7220
+ const baseYaml = basename3(filePath);
7221
+ const baseYml = baseYaml.replace(/\.yaml$/, ".yml");
7222
+ let rev = 0;
7223
+ bus.publish("keybindings", { rev });
7224
+ let timer = null;
7225
+ const bump = () => {
7226
+ timer = null;
7227
+ try {
7228
+ rev += 1;
7229
+ bus.publish("keybindings", { rev });
7230
+ } catch (err) {
7231
+ logDaemonError("keybindings-watcher", err);
7232
+ }
7233
+ };
7234
+ let watcher = null;
7235
+ try {
7236
+ mkdirSync2(dir, { recursive: true });
7237
+ watcher = watch(dir, (_event, filename) => {
7238
+ if (filename !== null && filename !== baseYaml && filename !== baseYml)
7239
+ return;
7240
+ if (timer)
7241
+ clearTimeout(timer);
7242
+ timer = setTimeout(bump, debounceMs);
7243
+ timer.unref?.();
7244
+ });
7245
+ watcher.on("error", (err) => logDaemonError("keybindings-watcher", err));
7246
+ } catch (err) {
7247
+ logDaemonError("keybindings-watcher", err);
7248
+ }
7249
+ return () => {
7250
+ if (timer) {
7251
+ clearTimeout(timer);
7252
+ timer = null;
7253
+ }
7254
+ watcher?.close();
7255
+ watcher = null;
7256
+ };
7257
+ }
7258
+ var DEFAULT_KEYBINDINGS_DEBOUNCE_MS = 200;
7259
+ var init_keybindings_watcher = () => {};
7260
+
7261
+ // ../kobe-daemon/src/daemon/ui-prefs-watcher.ts
7262
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, watch as watch2 } from "fs";
7263
+ import { homedir as homedir14 } from "os";
7264
+ import { basename as basename4, dirname as dirname6, join as join5 } from "path";
7265
+ function defaultUiPrefsStatePath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir14()) {
7266
+ return join5(homeDir2, ".config", "kobe", "state.json");
7190
7267
  }
7191
7268
  function readUiPrefsFromStateFile(statePath2) {
7192
7269
  let parsed = {};
@@ -7198,18 +7275,20 @@ function readUiPrefsFromStateFile(statePath2) {
7198
7275
  const theme = typeof parsed.activeTheme === "string" && parsed.activeTheme.length > 0 ? parsed.activeTheme : "claude";
7199
7276
  const transparentBackground = parsed.transparentBackground === true;
7200
7277
  const focusAccent = typeof parsed.focusAccent === "string" && FOCUS_ACCENT_SLOT_NAMES.includes(parsed.focusAccent) ? parsed.focusAccent : null;
7201
- return { theme, transparentBackground, focusAccent };
7278
+ const sortMode = parsed.activeSortMode === "recent" ? "recent" : "default";
7279
+ const keysCollapsed = parsed["tasksPane.keysCollapsed"] === true;
7280
+ return { theme, transparentBackground, focusAccent, sortMode, keysCollapsed };
7202
7281
  }
7203
7282
  function samePrefs(a, b) {
7204
- return a.theme === b.theme && a.transparentBackground === b.transparentBackground && a.focusAccent === b.focusAccent;
7283
+ return a.theme === b.theme && a.transparentBackground === b.transparentBackground && a.focusAccent === b.focusAccent && a.sortMode === b.sortMode && a.keysCollapsed === b.keysCollapsed;
7205
7284
  }
7206
7285
  function startUiPrefsWatcher(bus, options = {}) {
7207
7286
  const debounceMs = options.debounceMs ?? DEFAULT_UI_PREFS_DEBOUNCE_MS;
7208
7287
  if (debounceMs <= 0)
7209
7288
  return () => {};
7210
7289
  const statePath2 = options.statePath ?? defaultUiPrefsStatePath();
7211
- const stateDir = dirname5(statePath2);
7212
- const stateFile = basename3(statePath2);
7290
+ const stateDir = dirname6(statePath2);
7291
+ const stateFile = basename4(statePath2);
7213
7292
  let last = readUiPrefsFromStateFile(statePath2);
7214
7293
  bus.publish("ui-prefs", last);
7215
7294
  let timer = null;
@@ -7227,8 +7306,8 @@ function startUiPrefsWatcher(bus, options = {}) {
7227
7306
  };
7228
7307
  let watcher = null;
7229
7308
  try {
7230
- mkdirSync2(stateDir, { recursive: true });
7231
- watcher = watch(stateDir, (_event, filename) => {
7309
+ mkdirSync3(stateDir, { recursive: true });
7310
+ watcher = watch2(stateDir, (_event, filename) => {
7232
7311
  if (filename !== null && filename !== stateFile)
7233
7312
  return;
7234
7313
  if (timer)
@@ -7257,7 +7336,7 @@ var init_ui_prefs_watcher = __esm(() => {
7257
7336
  // ../kobe-daemon/src/daemon/server.ts
7258
7337
  import { mkdir as mkdir6, readFile as readFile8, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
7259
7338
  import { createServer } from "net";
7260
- import { dirname as dirname6 } from "path";
7339
+ import { dirname as dirname7 } from "path";
7261
7340
  function resolveIdleGraceMs() {
7262
7341
  const raw = process.env.KOBE_DAEMON_IDLE_GRACE_MS;
7263
7342
  if (raw === undefined)
@@ -7306,8 +7385,8 @@ async function startDaemonServer(orch, options = {}) {
7306
7385
  broadcast(clients, { type: "event", name: event.channel, payload: event.payload });
7307
7386
  });
7308
7387
  const activity = new DaemonActivityRegistry(bus);
7309
- await mkdir6(dirname6(socketPath), { recursive: true });
7310
- await mkdir6(dirname6(pidPath), { recursive: true });
7388
+ await mkdir6(dirname7(socketPath), { recursive: true });
7389
+ await mkdir6(dirname7(pidPath), { recursive: true });
7311
7390
  await unlink4(socketPath).catch(() => {});
7312
7391
  const server = createServer((socket) => {
7313
7392
  const client = {
@@ -7353,6 +7432,10 @@ async function startDaemonServer(orch, options = {}) {
7353
7432
  statePath: defaultUiPrefsStatePath(options.homeDir),
7354
7433
  debounceMs: options.uiPrefsDebounceMs ?? DEFAULT_UI_PREFS_DEBOUNCE_MS
7355
7434
  });
7435
+ const stopKeybindingsWatcher = startKeybindingsWatcher(bus, {
7436
+ path: defaultKeybindingsPath(options.homeDir),
7437
+ debounceMs: options.keybindingsDebounceMs ?? DEFAULT_KEYBINDINGS_DEBOUNCE_MS
7438
+ });
7356
7439
  const serverApi = {
7357
7440
  socketPath,
7358
7441
  pidPath,
@@ -7366,6 +7449,7 @@ async function startDaemonServer(orch, options = {}) {
7366
7449
  clearInterval(updateTimer);
7367
7450
  stopAutoTitlePoller();
7368
7451
  stopUiPrefsWatcher();
7452
+ stopKeybindingsWatcher();
7369
7453
  activity.close();
7370
7454
  broadcast(clients, { type: "event", name: "daemon.stopping", payload: {} });
7371
7455
  for (const client of Array.from(clients)) {
@@ -7485,6 +7569,7 @@ var init_server = __esm(() => {
7485
7569
  init_activity_registry();
7486
7570
  init_auto_title_poller();
7487
7571
  init_handlers();
7572
+ init_keybindings_watcher();
7488
7573
  init_paths2();
7489
7574
  init_protocol();
7490
7575
  init_ui_prefs_watcher();
@@ -7557,14 +7642,14 @@ __export(exports_daemon_process, {
7557
7642
  connectIfRunning: () => connectIfRunning
7558
7643
  });
7559
7644
  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";
7645
+ import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync4, openSync } from "fs";
7646
+ import { dirname as dirname8, resolve as resolve2 } from "path";
7562
7647
  import { fileURLToPath as fileURLToPath2 } from "url";
7563
7648
  function spawnDetachedDaemon(command, args, env, logPath) {
7564
7649
  let stdio = "ignore";
7565
7650
  let logFd;
7566
7651
  try {
7567
- mkdirSync3(dirname7(logPath), { recursive: true });
7652
+ mkdirSync4(dirname8(logPath), { recursive: true });
7568
7653
  logFd = openSync(logPath, "a");
7569
7654
  stdio = ["ignore", logFd, logFd];
7570
7655
  } catch {
@@ -7631,7 +7716,7 @@ function resolveKobeSpawn(subcommand) {
7631
7716
  if (here.startsWith("/$bunfs") || here.startsWith("B:\\~BUN")) {
7632
7717
  return [process.execPath, ...subcommand];
7633
7718
  }
7634
- const dir = dirname7(here);
7719
+ const dir = dirname8(here);
7635
7720
  const candidates = [
7636
7721
  resolve2(dir, "../cli/index.ts"),
7637
7722
  resolve2(dir, "../../../kobe/src/cli/index.ts"),
@@ -7730,13 +7815,13 @@ async function runRepoSubcommand(args) {
7730
7815
  }
7731
7816
  const { getRepoInitOverride: getRepoInitOverride2, setRepoInitOverride: setRepoInitOverride2, resolveRepoRoot: resolveRepoRoot2 } = await Promise.resolve().then(() => (init_repos(), exports_repos));
7732
7817
  const { existsSync: existsSync6 } = await import("fs");
7733
- const { join: join5 } = await import("path");
7818
+ const { join: join6 } = await import("path");
7734
7819
  if (verb === "show") {
7735
7820
  const [pathArg] = rest.filter((a) => !a.startsWith("-"));
7736
7821
  const repo = resolveRepoRoot2(resolve3(process.cwd(), pathArg ?? "."));
7737
7822
  const override = getRepoInitOverride2(repo);
7738
- const hasFileScript = existsSync6(join5(repo, ".kobe", "init.sh"));
7739
- const hasFilePrompt = existsSync6(join5(repo, ".kobe", "init-prompt.md"));
7823
+ const hasFileScript = existsSync6(join6(repo, ".kobe", "init.sh"));
7824
+ const hasFilePrompt = existsSync6(join6(repo, ".kobe", "init-prompt.md"));
7740
7825
  console.log(`repo: ${repo}`);
7741
7826
  console.log(` .kobe/init.sh: ${hasFileScript ? "present (wins)" : "absent"}`);
7742
7827
  console.log(` .kobe/init-prompt.md: ${hasFilePrompt ? "present (wins)" : "absent"}`);
@@ -8009,6 +8094,9 @@ var init_daemon_session = __esm(() => {
8009
8094
 
8010
8095
  // src/state/keybindings-file.ts
8011
8096
  import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
8097
+ function resetKeybindingsFileCache() {
8098
+ cached = null;
8099
+ }
8012
8100
  function resolveConfigFile() {
8013
8101
  const canonical = keybindingsConfigPath();
8014
8102
  if (existsSync6(canonical))
@@ -8399,6 +8487,9 @@ function resolveTmuxKeyEntries(entries) {
8399
8487
  function tmuxChordOptsFor(id) {
8400
8488
  return id.startsWith("tmux.") ? { allowShiftCharacter: true } : {};
8401
8489
  }
8490
+ function resetTmuxKeysCache() {
8491
+ cached2 = null;
8492
+ }
8402
8493
  function resolveUserTmuxKeys() {
8403
8494
  if (cached2)
8404
8495
  return cached2;
@@ -9948,9 +10039,9 @@ var init_schema = () => {};
9948
10039
 
9949
10040
  // src/tui/context/theme/loader.ts
9950
10041
  import { readFileSync as readFileSync6, readdirSync } from "fs";
9951
- import { join as join5 } from "path";
10042
+ import { join as join6 } from "path";
9952
10043
  function userThemesDir() {
9953
- return join5(kobeStateDir(), "themes");
10044
+ return join6(kobeStateDir(), "themes");
9954
10045
  }
9955
10046
  function loadUserThemes() {
9956
10047
  const dir = userThemesDir();
@@ -9964,7 +10055,7 @@ function loadUserThemes() {
9964
10055
  for (const file of entries) {
9965
10056
  if (!file.endsWith(".json"))
9966
10057
  continue;
9967
- const path11 = join5(dir, file);
10058
+ const path11 = join6(dir, file);
9968
10059
  let parsed;
9969
10060
  try {
9970
10061
  const text = readFileSync6(path11, "utf8");
@@ -10166,11 +10257,106 @@ async function relaunchEngineInAllWindows(session, cwd, command, remoteKey) {
10166
10257
  return true;
10167
10258
  }
10168
10259
  async function healTaskPaneWidths(session) {
10169
- const rows = await listKobePanes(session);
10170
- if (!rows)
10260
+ const target = await globalTasksPaneWidth();
10261
+ const { code, stdout } = await runTmuxCapturing([
10262
+ "list-panes",
10263
+ "-s",
10264
+ "-t",
10265
+ `=${session}`,
10266
+ "-F",
10267
+ "#{pane_id}\t#{@kobe_role}\t#{pane_width}"
10268
+ ]);
10269
+ if (code !== 0)
10270
+ return;
10271
+ const mismatched = stdout.split(`
10272
+ `).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);
10273
+ if (mismatched.length === 0)
10171
10274
  return;
10172
- const taskPanes = paneIdsByRole(rows, "tasks");
10173
- await runTmuxSequence(taskPanes.map((pane) => ["resize-pane", "-t", pane, "-x", `${TASKS_PANE_WIDTH}`]));
10275
+ await runTmuxSequence(mismatched.map((pane) => ["resize-pane", "-t", pane, "-x", `${target}`]));
10276
+ }
10277
+ async function rightColumnPercents() {
10278
+ const [width, height] = await Promise.all([
10279
+ getServerOption(RIGHT_COLUMN_WIDTH_OPTION),
10280
+ getServerOption(OPS_HEIGHT_OPTION)
10281
+ ]);
10282
+ return {
10283
+ widthPct: clampPanePercent(Number.parseInt(width, 10)),
10284
+ heightPct: clampPanePercent(Number.parseInt(height, 10))
10285
+ };
10286
+ }
10287
+ function rightColumnResizeArgs(geom) {
10288
+ const args = [];
10289
+ if (geom.widthPct !== null)
10290
+ args.push("-x", `${geom.widthPct}%`);
10291
+ if (geom.heightPct !== null)
10292
+ args.push("-y", `${geom.heightPct}%`);
10293
+ return args;
10294
+ }
10295
+ async function globalRightColumnResizeArgs() {
10296
+ return rightColumnResizeArgs(await rightColumnPercents());
10297
+ }
10298
+ async function healRightColumn(session) {
10299
+ const args = await globalRightColumnResizeArgs();
10300
+ if (args.length === 0)
10301
+ return;
10302
+ const { code, stdout } = await runTmuxCapturing([
10303
+ "list-panes",
10304
+ "-s",
10305
+ "-t",
10306
+ `=${session}`,
10307
+ "-F",
10308
+ "#{pane_id}\t#{@kobe_role}"
10309
+ ]);
10310
+ if (code !== 0)
10311
+ return;
10312
+ const opsPanes = stdout.split(`
10313
+ `).map((line) => line.split("\t")).filter(([, role]) => role?.trim() === "ops").map(([id]) => id?.trim()).filter((id) => !!id);
10314
+ if (opsPanes.length === 0)
10315
+ return;
10316
+ await runTmuxSequence(opsPanes.map((pane) => ["resize-pane", "-t", pane, ...args]));
10317
+ }
10318
+ async function healSessionLayout(session) {
10319
+ if (!await sessionExists(session))
10320
+ return;
10321
+ await healTaskPaneWidths(session);
10322
+ await healRightColumn(session);
10323
+ }
10324
+ async function captureGlobalLayout(session) {
10325
+ const { code, stdout } = await runTmuxCapturing([
10326
+ "list-panes",
10327
+ "-t",
10328
+ `=${session}`,
10329
+ "-F",
10330
+ "#{@kobe_role}\t#{pane_width}\t#{pane_height}\t#{window_width}\t#{window_height}"
10331
+ ]);
10332
+ if (code !== 0)
10333
+ return;
10334
+ const rows = stdout.split(`
10335
+ `).map((line) => line.split("\t")).filter((cols) => (cols[0]?.trim() ?? "") !== "");
10336
+ if (rows.length === 0)
10337
+ return;
10338
+ const winW = Number.parseInt(rows[0][3]?.trim() ?? "", 10);
10339
+ const winH = Number.parseInt(rows[0][4]?.trim() ?? "", 10);
10340
+ const sets = [];
10341
+ const tasks = rows.find(([role]) => role?.trim() === "tasks");
10342
+ if (tasks) {
10343
+ const width = Number.parseInt(tasks[1]?.trim() ?? "", 10);
10344
+ if (Number.isFinite(width) && width > 0)
10345
+ sets.push(["set-option", "-s", TASKS_WIDTH_OPTION, `${clampTasksPaneWidth(width)}`]);
10346
+ }
10347
+ const ops = rows.find(([role]) => role?.trim() === "ops");
10348
+ if (ops && Number.isFinite(winW) && winW > 0 && Number.isFinite(winH) && winH > 0) {
10349
+ const opsW = Number.parseInt(ops[1]?.trim() ?? "", 10);
10350
+ const opsH = Number.parseInt(ops[2]?.trim() ?? "", 10);
10351
+ const widthPct = Number.isFinite(opsW) ? clampPanePercent(100 * opsW / winW) : null;
10352
+ const heightPct = Number.isFinite(opsH) ? clampPanePercent(100 * opsH / winH) : null;
10353
+ if (widthPct !== null)
10354
+ sets.push(["set-option", "-s", RIGHT_COLUMN_WIDTH_OPTION, `${widthPct}`]);
10355
+ if (heightPct !== null)
10356
+ sets.push(["set-option", "-s", OPS_HEIGHT_OPTION, `${heightPct}`]);
10357
+ }
10358
+ if (sets.length > 0)
10359
+ await runTmuxSequence(sets);
10174
10360
  }
10175
10361
  async function healKobePaneVersions(session, cwd, taskId, vendor) {
10176
10362
  const rows = await listKobePanes(session);
@@ -10253,6 +10439,7 @@ function kobeStatusRight(keys) {
10253
10439
  }
10254
10440
  async function buildPanesAround(claudePane, args) {
10255
10441
  const envPrefix = inheritedEnvPrefix();
10442
+ const tasksWidth = await globalTasksPaneWidth();
10256
10443
  const opsCmd = keepAlive(args.opsCommand ?? envPrefix + opsPaneCommand({
10257
10444
  cwd: args.cwd,
10258
10445
  taskId: args.taskId,
@@ -10270,7 +10457,7 @@ async function buildPanesAround(claudePane, args) {
10270
10457
  "-t",
10271
10458
  claudePane,
10272
10459
  "-l",
10273
- `${TASKS_PANE_WIDTH}`,
10460
+ `${tasksWidth}`,
10274
10461
  "-c",
10275
10462
  localSpawnCwd(args.cwd),
10276
10463
  "-P",
@@ -10303,6 +10490,11 @@ async function buildPanesAround(claudePane, args) {
10303
10490
  ...ids.ops ? [["set-option", "-p", "-t", ids.ops, "@kobe_role", "ops"]] : [],
10304
10491
  ...ids.ops ? [["set-option", "-p", "-t", ids.ops, PANE_VERSION_OPTION, CURRENT_VERSION]] : []
10305
10492
  ]);
10493
+ if (ids.ops) {
10494
+ const rcArgs = await globalRightColumnResizeArgs();
10495
+ if (rcArgs.length > 0)
10496
+ await runTmux(["resize-pane", "-t", ids.ops, ...rcArgs]);
10497
+ }
10306
10498
  }
10307
10499
  async function newChatTab(session, vendorOverride) {
10308
10500
  if (!await sessionExists(session))
@@ -10350,6 +10542,16 @@ async function openSettingsTab(session) {
10350
10542
  const command = `${envPrefix}${inv.map(shellQuote).join(" ")} settings`;
10351
10543
  await newWindow(session, { cwd: localSpawnCwd(cwd), command, name: "settings" });
10352
10544
  }
10545
+ async function openHelpTab(session) {
10546
+ if (!await sessionExists(session))
10547
+ return;
10548
+ const sessionOptions = await getSessionOptions(session, ["@kobe_worktree"]);
10549
+ const cwd = sessionOptions["@kobe_worktree"] || process.cwd();
10550
+ const inv = kobeCliInvocation();
10551
+ const envPrefix = inheritedEnvPrefix();
10552
+ const command = `${envPrefix}${inv.map(shellQuote).join(" ")} help-page`;
10553
+ await newWindow(session, { cwd: localSpawnCwd(cwd), command, name: "help" });
10554
+ }
10353
10555
  async function openNewTaskTab(session, defaultRepo) {
10354
10556
  if (!await sessionExists(session))
10355
10557
  return;
@@ -10422,18 +10624,22 @@ __export(exports_tmux, {
10422
10624
  selectTasksPane: () => selectTasksPane,
10423
10625
  refreshKobeWorkspacePanes: () => refreshKobeWorkspacePanes,
10424
10626
  quickCreate: () => quickCreate,
10627
+ prepareWindowForAttach: () => prepareWindowForAttach,
10425
10628
  openUpdateTab: () => openUpdateTab,
10426
10629
  openSettingsTab: () => openSettingsTab,
10427
10630
  openNewTaskTab: () => openNewTaskTab,
10631
+ openHelpTab: () => openHelpTab,
10428
10632
  newChatTab: () => newChatTab,
10429
10633
  kobeStatusRight: () => kobeStatusRight,
10430
10634
  killSession: () => killSession,
10635
+ healSessionLayout: () => healSessionLayout,
10431
10636
  ensureSession: () => ensureSession,
10432
10637
  currentSessionName: () => currentSessionName,
10433
10638
  chatTabSwitchBindings: () => chatTabSwitchBindings,
10434
10639
  chatTabRenameBinding: () => chatTabRenameBinding,
10435
10640
  chatTabCloseBinding: () => chatTabCloseBinding,
10436
10641
  chatTabChooseEngineBindings: () => chatTabChooseEngineBindings,
10642
+ captureGlobalLayout: () => captureGlobalLayout,
10437
10643
  attachArgv: () => attachArgv,
10438
10644
  PANE_VERSION_OPTION: () => PANE_VERSION_OPTION,
10439
10645
  CHAT_TAB_STATUS_FORMAT: () => CHAT_TAB_STATUS_FORMAT,
@@ -10450,6 +10656,13 @@ function positiveInt(value) {
10450
10656
  const n = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value, 10) : Number.NaN;
10451
10657
  return Number.isInteger(n) && n > 0 ? n : undefined;
10452
10658
  }
10659
+ async function prepareWindowForAttach(session) {
10660
+ const sizeArgs = tmuxInitialSizeArgs();
10661
+ if (sizeArgs.length > 0)
10662
+ await runTmux(["resize-window", "-t", `=${session}`, ...sizeArgs]);
10663
+ await healTaskPaneWidths(session);
10664
+ await healRightColumn(session);
10665
+ }
10453
10666
  async function ensureSession(opts) {
10454
10667
  const inflight = ensureSessionLocks.get(opts.name);
10455
10668
  if (inflight)
@@ -10483,6 +10696,7 @@ async function ensureSessionImpl(opts) {
10483
10696
  const remoteKey = remoteKeyForRepo(opts.repo);
10484
10697
  if (action.kind === "reuse") {
10485
10698
  await healTaskPaneWidths(opts.name);
10699
+ await healRightColumn(opts.name);
10486
10700
  await healKobePaneVersions(opts.name, opts.cwd, opts.taskId, opts.vendor);
10487
10701
  return true;
10488
10702
  }
@@ -10491,6 +10705,7 @@ async function ensureSessionImpl(opts) {
10491
10705
  if (opts.vendor)
10492
10706
  await setSessionOption(opts.name, "@kobe_vendor", opts.vendor);
10493
10707
  await healTaskPaneWidths(opts.name);
10708
+ await healRightColumn(opts.name);
10494
10709
  await healKobePaneVersions(opts.name, opts.cwd, opts.taskId, opts.vendor);
10495
10710
  return true;
10496
10711
  }
@@ -10544,6 +10759,8 @@ async function ensureSessionImpl(opts) {
10544
10759
  const chooseEngineTmuxCommand = `run-shell ${shellQuote(chooseEngineCommand)}`;
10545
10760
  const focusTasksCommand = `${envStr}${invStr} focus-tasks --session '#{session_name}'`;
10546
10761
  const focusTasksTmuxCommand = `run-shell ${shellQuote(focusTasksCommand)}`;
10762
+ const healLayoutCommand = `${envStr}${invStr} heal-layout --session '#{session_name}'`;
10763
+ const healLayoutTmuxCommand = `run-shell -b ${shellQuote(healLayoutCommand)}`;
10547
10764
  const userKeys = resolveUserTmuxKeys();
10548
10765
  const unbinds = [];
10549
10766
  if (userKeys.overridden.has(TMUX_FOCUS_ID)) {
@@ -10585,6 +10802,7 @@ async function ensureSessionImpl(opts) {
10585
10802
  })
10586
10803
  ],
10587
10804
  ["set-option", "-g", "mouse", "on"],
10805
+ ["set-hook", "-g", "window-resized", healLayoutTmuxCommand],
10588
10806
  ...unbinds,
10589
10807
  ...b["tmux.detach"] ? [
10590
10808
  [
@@ -10648,12 +10866,12 @@ __export(exports_repo_init, {
10648
10866
  resolveRepoInit: () => resolveRepoInit
10649
10867
  });
10650
10868
  import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
10651
- import { join as join6 } from "path";
10869
+ import { join as join7 } from "path";
10652
10870
  function repoFileScript(worktreePath) {
10653
- return existsSync7(join6(worktreePath, INIT_SCRIPT_REL)) ? `sh ${INIT_SCRIPT_REL}` : undefined;
10871
+ return existsSync7(join7(worktreePath, INIT_SCRIPT_REL)) ? `sh ${INIT_SCRIPT_REL}` : undefined;
10654
10872
  }
10655
10873
  function repoFilePrompt(worktreePath) {
10656
- const p = join6(worktreePath, INIT_PROMPT_REL);
10874
+ const p = join7(worktreePath, INIT_PROMPT_REL);
10657
10875
  if (!existsSync7(p))
10658
10876
  return;
10659
10877
  try {
@@ -10675,8 +10893,8 @@ function resolveRepoInit(repoRoot, worktreePath) {
10675
10893
  var INIT_SCRIPT_REL, INIT_PROMPT_REL;
10676
10894
  var init_repo_init = __esm(() => {
10677
10895
  init_repos();
10678
- INIT_SCRIPT_REL = join6(".kobe", "init.sh");
10679
- INIT_PROMPT_REL = join6(".kobe", "init-prompt.md");
10896
+ INIT_SCRIPT_REL = join7(".kobe", "init.sh");
10897
+ INIT_PROMPT_REL = join7(".kobe", "init-prompt.md");
10680
10898
  });
10681
10899
 
10682
10900
  // src/tui/panes/sidebar/worktree-changes.ts
@@ -11717,8 +11935,8 @@ var exports_theme = {};
11717
11935
  __export(exports_theme, {
11718
11936
  runThemeSubcommand: () => runThemeSubcommand
11719
11937
  });
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";
11938
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync9, readdirSync as readdirSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
11939
+ import { basename as basename5, join as join8, resolve as resolve5 } from "path";
11722
11940
  function fail3(message) {
11723
11941
  process.stderr.write(`kobe theme: ${message}
11724
11942
  `);
@@ -11749,7 +11967,7 @@ function listThemes() {
11749
11967
  } else {
11750
11968
  for (const f of userFiles) {
11751
11969
  const name = f.slice(0, -".json".length);
11752
- const path11 = join7(dir, f);
11970
+ const path11 = join8(dir, f);
11753
11971
  const overridesBundled = BUNDLED_NAMES.includes(name) ? " (overrides built-in)" : "";
11754
11972
  lines.push(` ${name}${overridesBundled} ${path11}`);
11755
11973
  }
@@ -11771,7 +11989,7 @@ async function readSource(source) {
11771
11989
  }
11772
11990
  const text2 = await res.text();
11773
11991
  const cleanPath = source.split(/[?#]/)[0] ?? source;
11774
- const file2 = basename4(cleanPath) || "theme.json";
11992
+ const file2 = basename5(cleanPath) || "theme.json";
11775
11993
  const defaultName2 = file2.endsWith(".json") ? file2.slice(0, -".json".length) : file2;
11776
11994
  return { text: text2, defaultName: defaultName2 };
11777
11995
  }
@@ -11782,7 +12000,7 @@ async function readSource(source) {
11782
12000
  } catch (err) {
11783
12001
  fail3(`failed to read ${abs}: ${err instanceof Error ? err.message : String(err)}`);
11784
12002
  }
11785
- const file = basename4(abs);
12003
+ const file = basename5(abs);
11786
12004
  const defaultName = file.endsWith(".json") ? file.slice(0, -".json".length) : file;
11787
12005
  return { text, defaultName };
11788
12006
  }
@@ -11838,8 +12056,8 @@ async function addTheme(args) {
11838
12056
  fail3(`invalid theme name "${name}" (use letters, digits, '.', '_', '-')`);
11839
12057
  }
11840
12058
  const dir = userThemesDir();
11841
- mkdirSync4(dir, { recursive: true });
11842
- const dest = join7(dir, `${name}.json`);
12059
+ mkdirSync5(dir, { recursive: true });
12060
+ const dest = join8(dir, `${name}.json`);
11843
12061
  if (existsSync8(dest) && !opts.force) {
11844
12062
  fail3(`${dest} already exists (pass --force to overwrite)`);
11845
12063
  }
@@ -11857,7 +12075,7 @@ function removeTheme(args) {
11857
12075
  if (BUNDLED_NAMES.includes(name)) {
11858
12076
  fail3(`"${name}" is a built-in theme and cannot be removed`);
11859
12077
  }
11860
- const dest = join7(userThemesDir(), `${name}.json`);
12078
+ const dest = join8(userThemesDir(), `${name}.json`);
11861
12079
  if (!existsSync8(dest)) {
11862
12080
  fail3(`no user theme named "${name}" (looked for ${dest})`);
11863
12081
  }
@@ -12018,9 +12236,9 @@ var init_feedback_cmd = __esm(() => {
12018
12236
  });
12019
12237
 
12020
12238
  // src/core/index.ts
12021
- import { homedir as homedir14 } from "os";
12239
+ import { homedir as homedir15 } from "os";
12022
12240
  async function createKobeCore(options = {}) {
12023
- const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir14();
12241
+ const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir15();
12024
12242
  const store = new TaskIndexStore({ homeDir: homeDir2 });
12025
12243
  await store.load();
12026
12244
  const worktrees = new GitWorktreeManager;
@@ -12138,8 +12356,8 @@ var init_daemon_cmd = __esm(() => {
12138
12356
 
12139
12357
  // src/lib/skill-install.ts
12140
12358
  import { existsSync as existsSync9, readFileSync as readFileSync11 } from "fs";
12141
- import { homedir as homedir15 } from "os";
12142
- import { join as join8 } from "path";
12359
+ import { homedir as homedir16 } from "os";
12360
+ import { join as join9 } from "path";
12143
12361
  function npxSkillsArgv(opts = {}) {
12144
12362
  return ["skills", "add", SKILL_SOURCE_SLUG, "--skill", "kobe", "--agent", opts.agent ?? DEFAULT_SKILL_AGENT];
12145
12363
  }
@@ -12147,9 +12365,9 @@ function npxSkillsCommand(opts = {}) {
12147
12365
  return `npx ${npxSkillsArgv(opts).join(" ")}`;
12148
12366
  }
12149
12367
  function kobeSkillPaths(opts = {}) {
12150
- const home = opts.home ?? homedir15();
12368
+ const home = opts.home ?? homedir16();
12151
12369
  const cwd = opts.cwd ?? process.cwd();
12152
- return [join8(home, SKILL_REL_PATH), join8(cwd, SKILL_REL_PATH)];
12370
+ return [join9(home, SKILL_REL_PATH), join9(cwd, SKILL_REL_PATH)];
12153
12371
  }
12154
12372
  function parseSkillVersion(content) {
12155
12373
  const m = content.match(/kobe-skill-version:\s*(\d+)/);
@@ -12210,7 +12428,7 @@ __export(exports_maintenance, {
12210
12428
  });
12211
12429
  import { existsSync as existsSync10, readFileSync as readFileSync12, statSync as statSync5 } from "fs";
12212
12430
  import { unlink as unlink6 } from "fs/promises";
12213
- import { join as join9 } from "path";
12431
+ import { join as join10 } from "path";
12214
12432
  import { createInterface as createInterface2 } from "readline";
12215
12433
  function isProcessAlive2(pid) {
12216
12434
  try {
@@ -12307,7 +12525,7 @@ async function runDoctorSubcommand(argv = []) {
12307
12525
  const socketPath = defaultDaemonSocketPath();
12308
12526
  const pidPath = defaultDaemonPidPath();
12309
12527
  const logPath = defaultDaemonLogPath();
12310
- const tasksPath = join9(kobeStateDir(), "tasks.json");
12528
+ const tasksPath = join10(kobeStateDir(), "tasks.json");
12311
12529
  const statePath2 = kvStatePath();
12312
12530
  const out = ["kobe doctor", ` home: ${homeDir()}`, ` socket: ${socketPath}`, ""];
12313
12531
  const status = await probeDaemonStatus(socketPath);
@@ -12426,7 +12644,7 @@ async function runResetSubcommand(argv) {
12426
12644
  const yes = argv.includes("--yes") || argv.includes("-y");
12427
12645
  const socketPath = defaultDaemonSocketPath();
12428
12646
  const pidPath = defaultDaemonPidPath();
12429
- const tasksPath = join9(kobeStateDir(), "tasks.json");
12647
+ const tasksPath = join10(kobeStateDir(), "tasks.json");
12430
12648
  const statePath2 = kvStatePath();
12431
12649
  console.log("kobe reset will:");
12432
12650
  console.log(" \u2022 stop the kobe daemon (graceful \u2192 SIGTERM \u2192 SIGKILL)");
@@ -12696,15 +12914,15 @@ var GIT_TIMEOUT_MS = 15000;
12696
12914
 
12697
12915
  // src/web/notes.ts
12698
12916
  import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
12699
- import { join as join10 } from "path";
12917
+ import { join as join11 } from "path";
12700
12918
  function notesDir() {
12701
- return join10(kobeStateDir(), "notes");
12919
+ return join11(kobeStateDir(), "notes");
12702
12920
  }
12703
12921
  function isSafeTaskId(taskId) {
12704
12922
  return typeof taskId === "string" && taskId.length > 0 && /^[A-Za-z0-9_-]+$/.test(taskId);
12705
12923
  }
12706
12924
  function noteFilePath(taskId) {
12707
- return join10(notesDir(), `${taskId}.md`);
12925
+ return join11(notesDir(), `${taskId}.md`);
12708
12926
  }
12709
12927
  async function handleGet(url) {
12710
12928
  const taskId = url.searchParams.get("taskId");
@@ -12950,7 +13168,7 @@ var init_session = __esm(() => {
12950
13168
 
12951
13169
  // ../kobe-web/server/bridge.ts
12952
13170
  import { existsSync as existsSync11 } from "fs";
12953
- import { join as join11, normalize as normalize2 } from "path";
13171
+ import { join as join12, normalize as normalize2 } from "path";
12954
13172
  function sseResponse(register) {
12955
13173
  let unregister = null;
12956
13174
  let heartbeat = null;
@@ -13024,10 +13242,10 @@ async function specResponse(url, link, spec) {
13024
13242
  }
13025
13243
  async function staticResponse(pathname, staticDir) {
13026
13244
  const rel = pathname === "/" ? "/index.html" : pathname;
13027
- const resolved = normalize2(join11(staticDir, rel));
13245
+ const resolved = normalize2(join12(staticDir, rel));
13028
13246
  if (!resolved.startsWith(staticDir))
13029
13247
  return new Response("forbidden", { status: 403 });
13030
- const file = Bun.file(existsSync11(resolved) ? resolved : join11(staticDir, "index.html"));
13248
+ const file = Bun.file(existsSync11(resolved) ? resolved : join12(staticDir, "index.html"));
13031
13249
  if (!await file.exists()) {
13032
13250
  return new Response("kobe web assets not built \u2014 run `bun --filter kobe-web build`", { status: 503 });
13033
13251
  }
@@ -13424,8 +13642,8 @@ __export(exports_hook_cmd, {
13424
13642
  parseWorktreeAddPath: () => parseWorktreeAddPath,
13425
13643
  ensureGlobalKobeHooks: () => ensureGlobalKobeHooks
13426
13644
  });
13427
- import { homedir as homedir16 } from "os";
13428
- import { join as join12, resolve as resolve7 } from "path";
13645
+ import { homedir as homedir17 } from "os";
13646
+ import { join as join13, resolve as resolve7 } from "path";
13429
13647
  async function readStdinPayload() {
13430
13648
  try {
13431
13649
  const text = await Promise.race([
@@ -13545,7 +13763,7 @@ function activityHookAdapters() {
13545
13763
  return ALL_VENDORS.map((v) => createEngineHookAdapter(v)).filter((a) => a.supportsHooks());
13546
13764
  }
13547
13765
  function globalSettingsPath() {
13548
- return join12(homedir16(), ".claude", "settings.json");
13766
+ return join13(homedir17(), ".claude", "settings.json");
13549
13767
  }
13550
13768
  function persistedSyncPath(stored) {
13551
13769
  if (!stored || stored === "off")
@@ -13553,7 +13771,7 @@ function persistedSyncPath(stored) {
13553
13771
  if (stored === "global")
13554
13772
  return globalSettingsPath();
13555
13773
  if (stored.startsWith("repo:"))
13556
- return join12(resolve7(stored.slice(5)), ".claude", "settings.json");
13774
+ return join13(resolve7(stored.slice(5)), ".claude", "settings.json");
13557
13775
  return stored;
13558
13776
  }
13559
13777
  async function ensureGlobalKobeHooks() {
@@ -14897,6 +15115,8 @@ class RemoteOrchestrator {
14897
15115
  setEngineStateSig;
14898
15116
  uiPrefsAcc;
14899
15117
  setUiPrefsSig;
15118
+ keybindingsRevAcc;
15119
+ setKeybindingsRevSig;
14900
15120
  connectionStateAcc;
14901
15121
  setConnectionState;
14902
15122
  ensureReachable;
@@ -14910,6 +15130,7 @@ class RemoteOrchestrator {
14910
15130
  const [daemonVersion, setDaemonVersion] = createSignal(null);
14911
15131
  const [engineState, setEngineState] = createSignal(new Map);
14912
15132
  const [uiPrefs, setUiPrefs] = createSignal(null);
15133
+ const [keybindingsRev, setKeybindingsRev] = createSignal(null);
14913
15134
  const [connectionState, setConnectionState] = createSignal("online");
14914
15135
  this.tasksAcc = tasks;
14915
15136
  this.setTasks = (next) => setTasks(() => next);
@@ -14923,6 +15144,8 @@ class RemoteOrchestrator {
14923
15144
  this.setEngineStateSig = (next) => setEngineState(() => next);
14924
15145
  this.uiPrefsAcc = uiPrefs;
14925
15146
  this.setUiPrefsSig = (next) => setUiPrefs(() => next);
15147
+ this.keybindingsRevAcc = keybindingsRev;
15148
+ this.setKeybindingsRevSig = (next) => setKeybindingsRev(() => next);
14926
15149
  this.connectionStateAcc = connectionState;
14927
15150
  this.setConnectionState = (next) => setConnectionState(() => next);
14928
15151
  this.ensureReachable = options.ensureReachable ?? ensureDaemonReachable;
@@ -15014,6 +15237,9 @@ class RemoteOrchestrator {
15014
15237
  uiPrefsSignal() {
15015
15238
  return this.uiPrefsAcc;
15016
15239
  }
15240
+ keybindingsRevSignal() {
15241
+ return this.keybindingsRevAcc;
15242
+ }
15017
15243
  listTasks() {
15018
15244
  return this.tasksAcc();
15019
15245
  }
@@ -15126,10 +15352,19 @@ class RemoteOrchestrator {
15126
15352
  this.setUiPrefsSig({
15127
15353
  theme: p.theme,
15128
15354
  transparentBackground: p.transparentBackground === true,
15129
- focusAccent: typeof p.focusAccent === "string" ? p.focusAccent : null
15355
+ focusAccent: typeof p.focusAccent === "string" ? p.focusAccent : null,
15356
+ sortMode: p.sortMode === "recent" ? "recent" : "default",
15357
+ keysCollapsed: p.keysCollapsed === true
15130
15358
  });
15131
15359
  return;
15132
15360
  }
15361
+ if (name === "keybindings") {
15362
+ const p = payload;
15363
+ if (typeof p?.rev !== "number")
15364
+ return;
15365
+ this.setKeybindingsRevSig(p.rev);
15366
+ return;
15367
+ }
15133
15368
  }
15134
15369
  }
15135
15370
  function deserializeTask(s) {
@@ -15523,10 +15758,6 @@ function resolveTheme(theme, mode = "dark") {
15523
15758
  ...out
15524
15759
  };
15525
15760
  }
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
15761
  var BUNDLED_THEMES, FOCUS_ACCENT_SLOTS, store, setStore, useTheme, ThemeProvider;
15531
15762
  var init_theme2 = __esm(() => {
15532
15763
  init_solid();
@@ -15598,8 +15829,7 @@ var init_theme2 = __esm(() => {
15598
15829
  return {
15599
15830
  ...v,
15600
15831
  background: transparent,
15601
- backgroundPanel: transparent,
15602
- backgroundDialog: withAlpha(v.backgroundDialog, 128)
15832
+ backgroundPanel: transparent
15603
15833
  };
15604
15834
  });
15605
15835
  createEffect(() => {
@@ -17673,6 +17903,19 @@ var init_focus = __esm(() => {
17673
17903
  });
17674
17904
 
17675
17905
  // src/tui/context/keybindings.ts
17906
+ function resetKeymapToDefaults() {
17907
+ for (const row of KobeKeymap) {
17908
+ const def = KEYMAP_DEFAULTS.get(row.id);
17909
+ if (!def)
17910
+ continue;
17911
+ const mutable = row;
17912
+ mutable.keys = [...def.keys];
17913
+ mutable.hint = def.hint ? { ...def.hint } : undefined;
17914
+ }
17915
+ }
17916
+ function bumpKeymapVersion() {
17917
+ setKeymapVersion((v) => v + 1);
17918
+ }
17676
17919
  function findBinding(id) {
17677
17920
  return KobeKeymap.find((b) => b.id === id);
17678
17921
  }
@@ -17695,15 +17938,16 @@ function bindByIds(handlers) {
17695
17938
  }
17696
17939
  return out;
17697
17940
  }
17698
- var KobeKeymap;
17941
+ var KobeKeymap, KEYMAP_DEFAULTS, keymapVersion, setKeymapVersion;
17699
17942
  var init_keybindings2 = __esm(() => {
17943
+ init_dev();
17700
17944
  KobeKeymap = [
17701
17945
  {
17702
17946
  id: "help.open",
17703
17947
  scope: "global",
17704
17948
  keys: ["f1"],
17705
17949
  category: "Global",
17706
- description: "Show this help dialog",
17950
+ description: "Show keybindings help",
17707
17951
  hint: { keys: "F1", label: "help", pin: "right" }
17708
17952
  },
17709
17953
  {
@@ -18154,6 +18398,8 @@ var init_keybindings2 = __esm(() => {
18154
18398
  hint: { keys: "ctrl+[/]", label: "tab" }
18155
18399
  }
18156
18400
  ];
18401
+ KEYMAP_DEFAULTS = new Map(KobeKeymap.map((b) => [b.id, { keys: [...b.keys], hint: b.hint ? { ...b.hint } : undefined }]));
18402
+ [keymapVersion, setKeymapVersion] = createSignal(0);
18157
18403
  });
18158
18404
 
18159
18405
  // src/tui/context/keybindings-user.ts
@@ -18201,6 +18447,15 @@ function applyUserKeybindings() {
18201
18447
  function userKeybindingsReport() {
18202
18448
  return cached3 ?? applyUserKeybindings();
18203
18449
  }
18450
+ function reloadUserKeybindings() {
18451
+ cached3 = null;
18452
+ resetKeybindingsFileCache();
18453
+ resetTmuxKeysCache();
18454
+ resetKeymapToDefaults();
18455
+ const report = applyUserKeybindings();
18456
+ bumpKeymapVersion();
18457
+ return report;
18458
+ }
18204
18459
  var cached3 = null;
18205
18460
  var init_keybindings_user = __esm(() => {
18206
18461
  init_keybindings_file();
@@ -18299,9 +18554,9 @@ var pulse_default = "../pulse-n3cq1btw.wav";
18299
18554
  var init_pulse = () => {};
18300
18555
 
18301
18556
  // src/tui/lib/sound.ts
18302
- import { existsSync as existsSync14, mkdirSync as mkdirSync5 } from "fs";
18557
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6 } from "fs";
18303
18558
  import { tmpdir as tmpdir2 } from "os";
18304
- import { basename as basename5, isAbsolute, join as join14, resolve as resolve8 } from "path";
18559
+ import { basename as basename6, isAbsolute, join as join15, resolve as resolve8 } from "path";
18305
18560
  function args(player, file, volume) {
18306
18561
  if (player === "ffplay")
18307
18562
  return [player, "-autoexit", "-nodisp", "-af", `volume=${volume}`, file];
@@ -18324,13 +18579,13 @@ function pickPlayer() {
18324
18579
  return cachedPlayer;
18325
18580
  const path12 = process.env.PATH ?? "";
18326
18581
  const segments = path12.split(":").filter(Boolean);
18327
- cachedPlayer = PLAYERS.find((p) => segments.some((dir) => existsSync14(join14(dir, p)))) ?? null;
18582
+ cachedPlayer = PLAYERS.find((p) => segments.some((dir) => existsSync14(join15(dir, p)))) ?? null;
18328
18583
  return cachedPlayer;
18329
18584
  }
18330
18585
  async function ensureAsset() {
18331
18586
  cachedPath ??= (async () => {
18332
- mkdirSync5(DIR, { recursive: true });
18333
- const dest = join14(DIR, basename5(pulseAsset));
18587
+ mkdirSync6(DIR, { recursive: true });
18588
+ const dest = join15(DIR, basename6(pulseAsset));
18334
18589
  const out = Bun.file(dest);
18335
18590
  if (await out.exists())
18336
18591
  return dest;
@@ -18360,7 +18615,7 @@ var pulseAsset, DIR, PLAYERS, cachedPlayer, cachedPath;
18360
18615
  var init_sound = __esm(() => {
18361
18616
  init_pulse();
18362
18617
  pulseAsset = isAbsolute(pulse_default) ? pulse_default : resolve8(import.meta.dir, pulse_default);
18363
- DIR = join14(tmpdir2(), "kobe-sfx");
18618
+ DIR = join15(tmpdir2(), "kobe-sfx");
18364
18619
  PLAYERS = [
18365
18620
  "ffplay",
18366
18621
  "mpv",
@@ -18605,6 +18860,18 @@ function UiPrefsSync(props) {
18605
18860
  if (payload)
18606
18861
  applyUiPrefs(target, payload);
18607
18862
  });
18863
+ let lastKeybindingsRev = null;
18864
+ createEffect(() => {
18865
+ const rev = prefsOrch()?.keybindingsRevSignal()();
18866
+ if (rev == null)
18867
+ return;
18868
+ if (lastKeybindingsRev === null || rev === lastKeybindingsRev) {
18869
+ lastKeybindingsRev = rev;
18870
+ return;
18871
+ }
18872
+ lastKeybindingsRev = rev;
18873
+ reloadUserKeybindings();
18874
+ });
18608
18875
  onCleanup(() => prefsOrch()?.dispose());
18609
18876
  return null;
18610
18877
  }
@@ -19107,12 +19374,13 @@ function groupBindings(keymap) {
19107
19374
  rows: groups.get(cat)
19108
19375
  }));
19109
19376
  }
19110
- function HelpDialog() {
19377
+ function HelpDialog(props = {}) {
19111
19378
  const dialog = useDialog();
19112
19379
  const {
19113
19380
  theme
19114
19381
  } = useTheme();
19115
19382
  const grouped = () => groupBindings(KobeKeymap);
19383
+ const close = () => props.onClose ? props.onClose() : dialog.clear();
19116
19384
  const [prefixGlyph, setPrefixGlyph] = createSignal("\u2303B");
19117
19385
  onMount(() => {
19118
19386
  runTmuxCapturing(["show-options", "-g", "prefix"]).then(({
@@ -19129,7 +19397,7 @@ function HelpDialog() {
19129
19397
  useBindings(() => ({
19130
19398
  bindings: [{
19131
19399
  key: "?",
19132
- cmd: () => dialog.clear()
19400
+ cmd: close
19133
19401
  }]
19134
19402
  }));
19135
19403
  return (() => {
@@ -19147,7 +19415,7 @@ function HelpDialog() {
19147
19415
  setProp(_el$2, "flexShrink", 0);
19148
19416
  insertNode(_el$3, createTextNode(`kobe \u2014 keybindings`));
19149
19417
  insertNode(_el$5, createTextNode(`esc`));
19150
- setProp(_el$5, "onMouseUp", () => dialog.clear());
19418
+ setProp(_el$5, "onMouseUp", close);
19151
19419
  insertNode(_el$7, _el$8);
19152
19420
  setProp(_el$7, "flexShrink", 1);
19153
19421
  setProp(_el$7, "flexGrow", 1);
@@ -19517,7 +19785,7 @@ var init_dialog_confirm = __esm(() => {
19517
19785
 
19518
19786
  // src/tui/component/settings-dialog/actions.ts
19519
19787
  import { unlinkSync as unlinkSync2 } from "fs";
19520
- import { join as join15 } from "path";
19788
+ import { join as join16 } from "path";
19521
19789
  function hasRestartableDaemon(orchestrator) {
19522
19790
  return orchestrator instanceof RemoteOrchestrator;
19523
19791
  }
@@ -19534,7 +19802,7 @@ async function confirmResetState(dialog, kv, renderer) {
19534
19802
  return;
19535
19803
  kv.clear();
19536
19804
  try {
19537
- unlinkSync2(join15(homeDir(), ".kobe", "tasks.json"));
19805
+ unlinkSync2(join16(homeDir(), ".kobe", "tasks.json"));
19538
19806
  } catch (err) {
19539
19807
  if (err.code !== "ENOENT") {
19540
19808
  console.error("kobe: failed to delete tasks.json during reset:", err);
@@ -21773,7 +22041,7 @@ var init_task_actions = __esm(() => {
21773
22041
  // src/tui/lib/worktree-opener.ts
21774
22042
  import { spawn as spawn4 } from "child_process";
21775
22043
  import { existsSync as existsSync15 } from "fs";
21776
- import { basename as basename6, delimiter, isAbsolute as isAbsolute2, join as join16 } from "path";
22044
+ import { basename as basename7, delimiter, isAbsolute as isAbsolute2, join as join17 } from "path";
21777
22045
  function executableOnPath(command, env, exists) {
21778
22046
  if (isAbsolute2(command))
21779
22047
  return exists(command);
@@ -21781,13 +22049,13 @@ function executableOnPath(command, env, exists) {
21781
22049
  for (const dir of pathEnv.split(delimiter)) {
21782
22050
  if (!dir)
21783
22051
  continue;
21784
- if (exists(join16(dir, command)))
22052
+ if (exists(join17(dir, command)))
21785
22053
  return true;
21786
22054
  }
21787
22055
  return false;
21788
22056
  }
21789
22057
  function labelForOverride(command) {
21790
- const name = basename6(command);
22058
+ const name = basename7(command);
21791
22059
  if (name === "code")
21792
22060
  return "VS Code";
21793
22061
  if (name === "cursor")
@@ -22406,7 +22674,8 @@ function Sidebar(props) {
22406
22674
  onCleanup(() => r.keyInput.off("keypress", listener2));
22407
22675
  });
22408
22676
  const [branchTick, setBranchTick] = createSignal(0);
22409
- setInterval(() => setBranchTick((n) => n + 1), MAIN_BRANCH_POLL_MS);
22677
+ const branchInterval = setInterval(() => setBranchTick((n) => n + 1), MAIN_BRANCH_POLL_MS);
22678
+ onCleanup(() => clearInterval(branchInterval));
22410
22679
  const [spinnerFrame, setSpinnerFrame] = createSignal(0);
22411
22680
  const spinnerInterval = setInterval(() => setSpinnerFrame((n) => (n + 1) % IN_PROGRESS_SPINNER.length), SPINNER_FRAME_MS);
22412
22681
  onCleanup(() => clearInterval(spinnerInterval));
@@ -23251,7 +23520,7 @@ function TasksShell(props) {
23251
23520
  });
23252
23521
  }
23253
23522
  const [moveMode, setMoveMode] = createSignal(false);
23254
- const [sortMode, setSortMode] = createSignal("default");
23523
+ const [sortMode, setSortMode] = createSignal(kv.get("activeSortMode") === "recent" ? "recent" : "default");
23255
23524
  const [updateInfo, setUpdateInfo] = createSignal(null);
23256
23525
  const dimensions = useTerminalDimensions();
23257
23526
  let activeConfirmed = !props.initialTaskId;
@@ -23266,6 +23535,11 @@ function TasksShell(props) {
23266
23535
  }
23267
23536
  setSelectedId(active);
23268
23537
  });
23538
+ createEffect(() => {
23539
+ const payload = props.orch?.uiPrefsSignal()();
23540
+ if (payload && payload.sortMode !== sortMode())
23541
+ setSortMode(payload.sortMode);
23542
+ });
23269
23543
  createEffect(() => {
23270
23544
  const info = props.orch?.updateSignal()();
23271
23545
  if (info)
@@ -23333,6 +23607,14 @@ function TasksShell(props) {
23333
23607
  console.error("[kobe tasks] failed to refresh workspace panes:", err);
23334
23608
  }
23335
23609
  }
23610
+ async function openHelp() {
23611
+ const session = await currentSessionName();
23612
+ if (session) {
23613
+ await openHelpTab(session);
23614
+ return;
23615
+ }
23616
+ HelpDialog.show(dialog);
23617
+ }
23336
23618
  async function openUpdate() {
23337
23619
  const info = updateInfo();
23338
23620
  if (!info?.hasUpdate) {
@@ -23404,13 +23686,21 @@ function TasksShell(props) {
23404
23686
  setSelectedId(id);
23405
23687
  await props.reload();
23406
23688
  }
23407
- const keysCollapsed = () => kv.get("tasksPane.keysCollapsed", false) === true;
23408
- const setKeysCollapsed = (next) => kv.set("tasksPane.keysCollapsed", next);
23689
+ const [keysCollapsed, setKeysCollapsedSig] = createSignal(kv.get("tasksPane.keysCollapsed", false) === true);
23690
+ const setKeysCollapsed = (next) => {
23691
+ setKeysCollapsedSig(next);
23692
+ kv.set("tasksPane.keysCollapsed", next);
23693
+ };
23694
+ createEffect(() => {
23695
+ const payload = props.orch?.uiPrefsSignal()();
23696
+ if (payload && payload.keysCollapsed !== keysCollapsed())
23697
+ setKeysCollapsedSig(payload.keysCollapsed);
23698
+ });
23409
23699
  const [searchActive, setSearchActive] = createSignal(false);
23410
23700
  useBindings(() => ({
23411
23701
  enabled: dialog.stack.length === 0 && !searchActive(),
23412
23702
  bindings: bindByIds({
23413
- "help.open": () => HelpDialog.show(dialog),
23703
+ "help.open": () => void openHelp(),
23414
23704
  "task.new": () => void createTask(),
23415
23705
  "settings.open.sidebar": () => void openSettings(),
23416
23706
  "tasks.update": () => void openUpdate(),
@@ -23435,6 +23725,9 @@ function TasksShell(props) {
23435
23725
  async function switchTo(id) {
23436
23726
  const name = tmuxSessionName(id);
23437
23727
  const task = props.tasks().find((t) => t.id === id);
23728
+ const from = await currentSessionName();
23729
+ if (from && from !== name)
23730
+ await captureGlobalLayout(from);
23438
23731
  const exists = await sessionExists(name);
23439
23732
  if (exists) {
23440
23733
  const cwd2 = await getSessionOption(name, "@kobe_worktree") || task?.worktreePath || "";
@@ -23546,7 +23839,11 @@ function TasksShell(props) {
23546
23839
  onMoveRequest: (id, delta) => void moveTask(id, delta),
23547
23840
  onMoveModeExit: () => setMoveMode(false),
23548
23841
  sortMode,
23549
- onSortModeToggle: () => setSortMode((cur) => cur === "default" ? "recent" : "default"),
23842
+ onSortModeToggle: () => {
23843
+ const next = sortMode() === "default" ? "recent" : "default";
23844
+ setSortMode(next);
23845
+ kv.set("activeSortMode", next);
23846
+ },
23550
23847
  focused: () => dialog.stack.length === 0,
23551
23848
  onSearchActiveChange: setSearchActive
23552
23849
  }));
@@ -23578,6 +23875,7 @@ function ShortcutHints(props) {
23578
23875
  });
23579
23876
  });
23580
23877
  const tmuxHints = () => {
23878
+ keymapVersion();
23581
23879
  const res = resolveUserTmuxKeys();
23582
23880
  const b = res.binds;
23583
23881
  const out = [];
@@ -23985,9 +24283,73 @@ var init_host4 = __esm(() => {
23985
24283
  init_dialog();
23986
24284
  });
23987
24285
 
23988
- // src/tui/update/host.tsx
24286
+ // src/tui/help/host.tsx
23989
24287
  var exports_host5 = {};
23990
24288
  __export(exports_host5, {
24289
+ startHelpHost: () => startHelpHost
24290
+ });
24291
+ function HelpPage() {
24292
+ const dialog = useDialog();
24293
+ const {
24294
+ theme
24295
+ } = useTheme();
24296
+ function exit() {
24297
+ process.exit(0);
24298
+ }
24299
+ useBindings(() => ({
24300
+ enabled: dialog.stack.length === 0,
24301
+ bindings: [{
24302
+ key: "escape",
24303
+ cmd: exit
24304
+ }, {
24305
+ key: "q",
24306
+ cmd: exit
24307
+ }, {
24308
+ key: "f1",
24309
+ cmd: exit
24310
+ }, {
24311
+ key: "ctrl+c",
24312
+ cmd: exit
24313
+ }]
24314
+ }));
24315
+ return (() => {
24316
+ var _el$ = createElement("box");
24317
+ setProp(_el$, "flexGrow", 1);
24318
+ setProp(_el$, "paddingTop", 1);
24319
+ insert(_el$, createComponent2(HelpDialog, {
24320
+ onClose: exit
24321
+ }));
24322
+ effect((_$p) => setProp(_el$, "backgroundColor", theme.background, _$p));
24323
+ return _el$;
24324
+ })();
24325
+ }
24326
+ async function startHelpHost() {
24327
+ await bootPaneHost({
24328
+ providers: {
24329
+ kv: false,
24330
+ focus: false
24331
+ },
24332
+ setup: () => ({
24333
+ root: () => createComponent2(HelpPage, {})
24334
+ })
24335
+ });
24336
+ }
24337
+ var init_host5 = __esm(() => {
24338
+ init_solid();
24339
+ init_solid();
24340
+ init_solid();
24341
+ init_solid();
24342
+ init_solid();
24343
+ init_help_dialog();
24344
+ init_theme2();
24345
+ init_host_boot();
24346
+ init_keymap();
24347
+ init_dialog();
24348
+ });
24349
+
24350
+ // src/tui/update/host.tsx
24351
+ var exports_host6 = {};
24352
+ __export(exports_host6, {
23991
24353
  startUpdateHost: () => startUpdateHost
23992
24354
  });
23993
24355
  import { spawn as spawn6, spawnSync as spawnSync11 } from "child_process";
@@ -24342,7 +24704,7 @@ async function startUpdateHost() {
24342
24704
  })
24343
24705
  });
24344
24706
  }
24345
- var init_host5 = __esm(() => {
24707
+ var init_host6 = __esm(() => {
24346
24708
  init_solid();
24347
24709
  init_solid();
24348
24710
  init_solid();
@@ -24753,8 +25115,109 @@ function spawnDetached(cmd, args2, onError) {
24753
25115
  }
24754
25116
  var init_open_external = () => {};
24755
25117
 
25118
+ // src/tui/panes/filetree/rows.ts
25119
+ function flattenTree(node, expanded, depth, out) {
25120
+ for (const child of node.children) {
25121
+ if (child.isDir) {
25122
+ const isOpen = expanded.has(child.path);
25123
+ out.push({
25124
+ kind: "dir",
25125
+ path: child.path,
25126
+ name: child.name,
25127
+ depth,
25128
+ expanded: isOpen,
25129
+ hasChildren: child.children.length > 0
25130
+ });
25131
+ if (isOpen)
25132
+ flattenTree(child, expanded, depth + 1, out);
25133
+ } else {
25134
+ out.push({ kind: "file", path: child.path, name: child.name, depth });
25135
+ }
25136
+ }
25137
+ }
25138
+ function statusRows(entries) {
25139
+ return entries.map((e) => ({
25140
+ kind: "status",
25141
+ path: e.path,
25142
+ status: e.status,
25143
+ added: e.added,
25144
+ deleted: e.deleted
25145
+ }));
25146
+ }
25147
+ function rowKey(row) {
25148
+ return `${row.kind}\x00${row.path}`;
25149
+ }
25150
+ function rowEquals(a, b) {
25151
+ if (a.kind !== b.kind || a.path !== b.path)
25152
+ return false;
25153
+ switch (a.kind) {
25154
+ case "file": {
25155
+ const o = b;
25156
+ return a.name === o.name && a.depth === o.depth;
25157
+ }
25158
+ case "dir": {
25159
+ const o = b;
25160
+ return a.name === o.name && a.depth === o.depth && a.expanded === o.expanded && a.hasChildren === o.hasChildren;
25161
+ }
25162
+ case "status": {
25163
+ const o = b;
25164
+ return a.status === o.status && a.added === o.added && a.deleted === o.deleted;
25165
+ }
25166
+ }
25167
+ }
25168
+ function reconcileRows(prev, next) {
25169
+ if (prev.length === 0)
25170
+ return next;
25171
+ const prevByKey = new Map;
25172
+ for (const row of prev)
25173
+ prevByKey.set(rowKey(row), row);
25174
+ let allReused = prev.length === next.length;
25175
+ const out = new Array(next.length);
25176
+ for (let i = 0;i < next.length; i++) {
25177
+ const fresh = next[i];
25178
+ const old = prevByKey.get(rowKey(fresh));
25179
+ if (old && rowEquals(old, fresh)) {
25180
+ out[i] = old;
25181
+ if (allReused && prev[i] !== old)
25182
+ allReused = false;
25183
+ } else {
25184
+ out[i] = fresh;
25185
+ allReused = false;
25186
+ }
25187
+ }
25188
+ return allReused ? prev : out;
25189
+ }
25190
+ function sameFileList(a, b) {
25191
+ if (a === b)
25192
+ return true;
25193
+ if (a == null || b == null)
25194
+ return false;
25195
+ if (a.length !== b.length)
25196
+ return false;
25197
+ for (let i = 0;i < a.length; i++) {
25198
+ if (a[i] !== b[i])
25199
+ return false;
25200
+ }
25201
+ return true;
25202
+ }
25203
+ function sameStatusEntries(a, b) {
25204
+ if (a === b)
25205
+ return true;
25206
+ if (a == null || b == null)
25207
+ return false;
25208
+ if (a.length !== b.length)
25209
+ return false;
25210
+ for (let i = 0;i < a.length; i++) {
25211
+ const x = a[i];
25212
+ const y = b[i];
25213
+ if (x.path !== y.path || x.status !== y.status || x.added !== y.added || x.deleted !== y.deleted)
25214
+ return false;
25215
+ }
25216
+ return true;
25217
+ }
25218
+
24756
25219
  // src/tui/panes/filetree/FileTree.tsx
24757
- import { watch as watch2 } from "fs";
25220
+ import { watch as watch3 } from "fs";
24758
25221
  import { TextAttributes as TextAttributes14 } from "@opentui/core";
24759
25222
  function statusToken(s) {
24760
25223
  switch (s) {
@@ -24801,8 +25264,12 @@ function FileTree(props) {
24801
25264
  const [tab, setTab] = createSignal("all");
24802
25265
  const [cursorIndex, setCursorIndex] = createSignal(0);
24803
25266
  const [refreshTick, setRefreshTick] = createSignal(0);
24804
- const [allFiles, setAllFiles] = createSignal(null);
24805
- const [changes, setChanges] = createSignal(null);
25267
+ const [allFiles, setAllFiles] = createSignal(null, {
25268
+ equals: sameFileList
25269
+ });
25270
+ const [changes, setChanges] = createSignal(null, {
25271
+ equals: sameStatusEntries
25272
+ });
24806
25273
  const [error, setError] = createSignal(null);
24807
25274
  const [expandedDirs, setExpandedDirs] = createSignal(new Set);
24808
25275
  let fetchSeq = 0;
@@ -24849,7 +25316,7 @@ function FileTree(props) {
24849
25316
  let debounceTimer = null;
24850
25317
  let watcher = null;
24851
25318
  try {
24852
- watcher = watch2(path12, {
25319
+ watcher = watch3(path12, {
24853
25320
  recursive: true
24854
25321
  }, (_event, filename) => {
24855
25322
  if (filename == null)
@@ -24898,53 +25365,19 @@ function FileTree(props) {
24898
25365
  return null;
24899
25366
  return buildTree(files);
24900
25367
  });
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(() => {
25368
+ const rows = createMemo((prev) => {
25369
+ const next = [];
24926
25370
  if (tab() === "all") {
24927
25371
  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") {
25372
+ if (root != null)
25373
+ flattenTree(root, expandedDirs(), 0, next);
25374
+ } else if (tab() === "changes") {
24935
25375
  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
- }));
25376
+ if (list2 != null)
25377
+ next.push(...statusRows(list2));
24945
25378
  }
24946
- return [];
24947
- });
25379
+ return reconcileRows(prev ?? [], next);
25380
+ }, []);
24948
25381
  const statWidths = createMemo(() => {
24949
25382
  let added = 0;
24950
25383
  let deleted = 0;
@@ -25631,8 +26064,8 @@ var init_pr_prompt = __esm(() => {
25631
26064
  });
25632
26065
 
25633
26066
  // src/tui/ops/host.tsx
25634
- var exports_host6 = {};
25635
- __export(exports_host6, {
26067
+ var exports_host7 = {};
26068
+ __export(exports_host7, {
25636
26069
  startOpsPreview: () => startOpsPreview,
25637
26070
  startOpsHost: () => startOpsHost
25638
26071
  });
@@ -25738,7 +26171,7 @@ function OpsShell(props) {
25738
26171
  relPath: rel,
25739
26172
  cliInvocation: kobeCliInvocation()
25740
26173
  }),
25741
- name: basename7(rel)
26174
+ name: basename8(rel)
25742
26175
  });
25743
26176
  }
25744
26177
  function openFile(rel) {
@@ -25781,7 +26214,7 @@ function OpsShell(props) {
25781
26214
  return _el$;
25782
26215
  })();
25783
26216
  }
25784
- function basename7(p) {
26217
+ function basename8(p) {
25785
26218
  const i = p.lastIndexOf("/");
25786
26219
  return i >= 0 ? p.slice(i + 1) : p;
25787
26220
  }
@@ -26026,7 +26459,7 @@ async function startOpsPreview(args2) {
26026
26459
  });
26027
26460
  }
26028
26461
  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(() => {
26462
+ var init_host7 = __esm(() => {
26030
26463
  init_solid();
26031
26464
  init_solid();
26032
26465
  init_solid();
@@ -26124,6 +26557,7 @@ async function startDirectTmux() {
26124
26557
  if (!task) {
26125
26558
  const home = await ensureFallbackSession();
26126
26559
  await applyTmuxPaneBorderTheme();
26560
+ await prepareWindowForAttach(home);
26127
26561
  if (await attachTmux(attachArgv(home)) === null) {
26128
26562
  console.error("kobe: failed to attach to the kobe-home session");
26129
26563
  process.exitCode = 1;
@@ -26151,6 +26585,7 @@ async function startDirectTmux() {
26151
26585
  return;
26152
26586
  }
26153
26587
  await applyTmuxPaneBorderTheme();
26588
+ await prepareWindowForAttach(name);
26154
26589
  const exitCode = await attachTmux(attachArgv(name));
26155
26590
  if (exitCode === null) {
26156
26591
  console.error(`kobe: failed to attach to tmux session ${name}`);
@@ -26576,6 +27011,17 @@ async function main() {
26576
27011
  await selectTasksPane2(session);
26577
27012
  return;
26578
27013
  }
27014
+ if (subcommand === "heal-layout") {
27015
+ const flags = parseOpsFlags(rest);
27016
+ const session = flags.session;
27017
+ if (!session) {
27018
+ console.error("kobe heal-layout: --session <name> is required");
27019
+ process.exit(2);
27020
+ }
27021
+ const { healSessionLayout: healSessionLayout2 } = await Promise.resolve().then(() => (init_tmux(), exports_tmux));
27022
+ await healSessionLayout2(session);
27023
+ return;
27024
+ }
26579
27025
  if (subcommand === "quick-task") {
26580
27026
  const flags = parseOpsFlags(rest);
26581
27027
  const { startQuickTaskHost: startQuickTaskHost2 } = await Promise.resolve().then(() => (init_host2(), exports_host2));
@@ -26593,6 +27039,11 @@ async function main() {
26593
27039
  await startSettingsHost2();
26594
27040
  return;
26595
27041
  }
27042
+ if (subcommand === "help-page") {
27043
+ const { startHelpHost: startHelpHost2 } = await Promise.resolve().then(() => (init_host5(), exports_host5));
27044
+ await startHelpHost2();
27045
+ return;
27046
+ }
26596
27047
  if (subcommand === "new-task") {
26597
27048
  const flags = parseOpsFlags(rest);
26598
27049
  const { startNewTaskHost: startNewTaskHost2 } = await Promise.resolve().then(() => (init_host(), exports_host));
@@ -26600,7 +27051,7 @@ async function main() {
26600
27051
  return;
26601
27052
  }
26602
27053
  if (subcommand === "update-page") {
26603
- const { startUpdateHost: startUpdateHost2 } = await Promise.resolve().then(() => (init_host5(), exports_host5));
27054
+ const { startUpdateHost: startUpdateHost2 } = await Promise.resolve().then(() => (init_host6(), exports_host6));
26604
27055
  await startUpdateHost2();
26605
27056
  return;
26606
27057
  }
@@ -26611,11 +27062,11 @@ async function main() {
26611
27062
  process.exit(2);
26612
27063
  }
26613
27064
  if (flags.preview) {
26614
- const { startOpsPreview: startOpsPreview2 } = await Promise.resolve().then(() => (init_host6(), exports_host6));
27065
+ const { startOpsPreview: startOpsPreview2 } = await Promise.resolve().then(() => (init_host7(), exports_host7));
26615
27066
  await startOpsPreview2({ worktree: flags.worktree, relPath: flags.preview });
26616
27067
  return;
26617
27068
  }
26618
- const { startOpsHost: startOpsHost2 } = await Promise.resolve().then(() => (init_host6(), exports_host6));
27069
+ const { startOpsHost: startOpsHost2 } = await Promise.resolve().then(() => (init_host7(), exports_host7));
26619
27070
  await startOpsHost2({
26620
27071
  taskId: flags.taskId ?? "",
26621
27072
  worktree: flags.worktree,