@sma1lboy/kobe 0.7.18 → 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 +517 -177
  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.18",
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",
@@ -3919,7 +3919,9 @@ var init_protocol = __esm(() => {
3919
3919
  "update",
3920
3920
  "engine-state",
3921
3921
  "ui-prefs",
3922
- "keybindings"
3922
+ "keybindings",
3923
+ "task.jobs",
3924
+ "worktree.changes"
3923
3925
  ];
3924
3926
  });
3925
3927
 
@@ -4067,6 +4069,7 @@ class KobeDaemonClient {
4067
4069
  this.disposed = true;
4068
4070
  this.socket?.end();
4069
4071
  this.socket = null;
4072
+ this.failPending();
4070
4073
  }
4071
4074
  forceDisconnect() {
4072
4075
  const socket = this.socket;
@@ -4074,6 +4077,15 @@ class KobeDaemonClient {
4074
4077
  return;
4075
4078
  this.socket = null;
4076
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();
4077
4089
  }
4078
4090
  on(name, handler) {
4079
4091
  let set = this.handlers.get(name);
@@ -4148,9 +4160,7 @@ class KobeDaemonClient {
4148
4160
  if (this.socket !== which)
4149
4161
  return;
4150
4162
  this.socket = null;
4151
- for (const pending of this.pending.values())
4152
- pending.reject(new Error("daemon connection closed"));
4153
- this.pending.clear();
4163
+ this.failPending();
4154
4164
  this.emitLifecycle("close");
4155
4165
  }
4156
4166
  emitLifecycle(name) {
@@ -5941,7 +5951,7 @@ function parseEvents(raw, fallbackSessionId) {
5941
5951
  if (!text)
5942
5952
  continue;
5943
5953
  if (!firstUserMessage)
5944
- firstUserMessage = text.slice(0, PREVIEW_CHAR_CAP);
5954
+ firstUserMessage = Buffer.from(text.slice(0, PREVIEW_CHAR_CAP), "utf8").toString("utf8");
5945
5955
  messages.push({ role: "user", blocks: [{ type: "text", text }], timestamp, sessionId });
5946
5956
  continue;
5947
5957
  }
@@ -6277,7 +6287,8 @@ function titleFromMessages(messages) {
6277
6287
  if (!firstUser)
6278
6288
  return "";
6279
6289
  const text = firstUser.blocks.filter((b) => b.type === "text").map((b) => b.text).join(" ");
6280
- return deriveTitleFromPrompt(text);
6290
+ const title = deriveTitleFromPrompt(text);
6291
+ return title.length > 0 ? Buffer.from(title, "utf8").toString("utf8") : title;
6281
6292
  }
6282
6293
  async function deriveTitleFromSession(worktree, vendor = DEFAULT_TASK_VENDOR) {
6283
6294
  if (!worktree)
@@ -7071,8 +7082,20 @@ function createDaemonHandlerRegistry() {
7071
7082
  name: "task.ensureWorktree",
7072
7083
  async handle(payload, ctx) {
7073
7084
  const taskId = requireString(payload, "taskId");
7074
- const path11 = await ctx.orch.ensureWorktree(taskId);
7075
- 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
+ }
7076
7099
  }
7077
7100
  },
7078
7101
  {
@@ -7333,6 +7356,232 @@ var init_ui_prefs_watcher = __esm(() => {
7333
7356
  FOCUS_ACCENT_SLOT_NAMES = ["primary", "success", "info"];
7334
7357
  });
7335
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
+
7336
7585
  // ../kobe-daemon/src/daemon/server.ts
7337
7586
  import { mkdir as mkdir6, readFile as readFile8, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
7338
7587
  import { createServer } from "net";
@@ -7436,6 +7685,7 @@ async function startDaemonServer(orch, options = {}) {
7436
7685
  path: defaultKeybindingsPath(options.homeDir),
7437
7686
  debounceMs: options.keybindingsDebounceMs ?? DEFAULT_KEYBINDINGS_DEBOUNCE_MS
7438
7687
  });
7688
+ const stopWorktreeChangesCollector = startWorktreeChangesCollector(orch, bus, options.worktreeChangesTickMs ?? DEFAULT_WORKTREE_CHANGES_TICK_MS);
7439
7689
  const serverApi = {
7440
7690
  socketPath,
7441
7691
  pidPath,
@@ -7450,6 +7700,7 @@ async function startDaemonServer(orch, options = {}) {
7450
7700
  stopAutoTitlePoller();
7451
7701
  stopUiPrefsWatcher();
7452
7702
  stopKeybindingsWatcher();
7703
+ stopWorktreeChangesCollector();
7453
7704
  activity.close();
7454
7705
  broadcast(clients, { type: "event", name: "daemon.stopping", payload: {} });
7455
7706
  for (const client of Array.from(clients)) {
@@ -7573,6 +7824,7 @@ var init_server = __esm(() => {
7573
7824
  init_paths2();
7574
7825
  init_protocol();
7575
7826
  init_ui_prefs_watcher();
7827
+ init_worktree_changes_collector();
7576
7828
  init_handlers();
7577
7829
  DEFAULT_UPDATE_POLL_MS = 6 * 60 * 60 * 1000;
7578
7830
  });
@@ -7641,7 +7893,7 @@ __export(exports_daemon_process, {
7641
7893
  connectOrStartDaemon: () => connectOrStartDaemon,
7642
7894
  connectIfRunning: () => connectIfRunning
7643
7895
  });
7644
- import { spawn as spawn2 } from "child_process";
7896
+ import { spawn as spawn3 } from "child_process";
7645
7897
  import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync4, openSync } from "fs";
7646
7898
  import { dirname as dirname8, resolve as resolve2 } from "path";
7647
7899
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -7655,7 +7907,7 @@ function spawnDetachedDaemon(command, args, env, logPath) {
7655
7907
  } catch {
7656
7908
  stdio = "ignore";
7657
7909
  }
7658
- const child = spawn2(command, [...args], { detached: true, stdio, env });
7910
+ const child = spawn3(command, [...args], { detached: true, stdio, env });
7659
7911
  child.unref();
7660
7912
  if (logFd !== undefined) {
7661
7913
  try {
@@ -7938,7 +8190,7 @@ var init_interactive_command = __esm(() => {
7938
8190
  });
7939
8191
 
7940
8192
  // src/lib/feedback.ts
7941
- import { spawnSync as spawnSync7 } from "child_process";
8193
+ import { spawnSync as spawnSync8 } from "child_process";
7942
8194
  function parseRepoSlug(slug) {
7943
8195
  const [owner, name] = slug.split("/");
7944
8196
  if (!owner || !name)
@@ -7993,7 +8245,7 @@ function submitFeedback(input, deps = {}) {
7993
8245
  throw new Error("package repository is not a GitHub repository");
7994
8246
  const { owner, name } = parseRepoSlug(slug);
7995
8247
  const categorySlug = input.categorySlug?.trim() || DEFAULT_FEEDBACK_CATEGORY_SLUG;
7996
- const io = { spawn: deps.spawn ?? spawnSync7 };
8248
+ const io = { spawn: deps.spawn ?? spawnSync8 };
7997
8249
  const categoryData = runGhGraphql(DISCUSSION_CATEGORY_QUERY, { owner, name }, io);
7998
8250
  const repository = categoryData.repository;
7999
8251
  const repositoryId = repository?.id;
@@ -8132,6 +8384,13 @@ var init_keybindings_file = __esm(() => {
8132
8384
  });
8133
8385
 
8134
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
+ }
8135
8394
  function normalizeChord(raw, opts) {
8136
8395
  const trimmed = raw.trim().toLowerCase();
8137
8396
  if (!trimmed)
@@ -8271,6 +8530,14 @@ function applyKeymapOverrides(keymap, entries) {
8271
8530
  warnings.push(`${entry.id}: not customizable \u2014 the key is handled outside the keymap (doc-only row)`);
8272
8531
  continue;
8273
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
+ }
8274
8541
  const keys = entry.keys.filter((chord) => {
8275
8542
  if (chord.length === 1 && NO_BARE_LETTER_SCOPES.has(row.scope)) {
8276
8543
  warnings.push(`${entry.id}: "${chord}" dropped \u2014 a bare character on a ${row.scope}-scope binding would steal typed input (add a modifier)`);
@@ -8282,6 +8549,10 @@ function applyKeymapOverrides(keymap, entries) {
8282
8549
  warnings.push(`${entry.id}: no chords survived validation \u2014 keeping the default`);
8283
8550
  continue;
8284
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
+ }
8285
8556
  const defaultKeys = row.keys;
8286
8557
  const mutable = row;
8287
8558
  mutable.keys = keys;
@@ -8312,21 +8583,23 @@ function applyKeymapOverrides(keymap, entries) {
8312
8583
  }
8313
8584
  return { applied, warnings };
8314
8585
  }
8315
- 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;
8316
8587
  var init_keymap_overrides = __esm(() => {
8317
8588
  FIXED_BINDING_IDS = {
8318
- "focus.numeric": "pane focus is positional (h/j/k/l \u2192 pane) and mirrors the tmux-layer ctrl+hjkl bindings",
8319
- "sidebar.nav": "handler maps j/down vs k/up by key name",
8320
- "sidebar.goto": "gg vs Shift+G is discriminated inside the handler",
8321
- "sidebar.pin": "Shift+P is discriminated inside the handler",
8322
- "sidebar.localMerge": "Shift+M is discriminated inside the handler",
8323
- "sidebar.view": "[ vs ] direction is read from the key name",
8324
- "sidebar.search.nav": "handler maps down vs up by key name",
8325
- "files.nav": "handler maps j/down vs k/up by key name",
8326
- "files.hierarchy": "handler maps h/left vs l/right by key name",
8327
- "files.tab": "[ vs ] direction is read from the key name",
8328
- "chat.question.nav": "handler maps j/down vs k/up by key name",
8329
- "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")
8330
8603
  };
8331
8604
  NO_BARE_LETTER_SCOPES = new Set(["global", "workspace", "terminal"]);
8332
8605
  MOD_ALIASES = {
@@ -10897,51 +11170,6 @@ var init_repo_init = __esm(() => {
10897
11170
  INIT_PROMPT_REL = join7(".kobe", "init-prompt.md");
10898
11171
  });
10899
11172
 
10900
- // src/tui/panes/sidebar/worktree-changes.ts
10901
- var exports_worktree_changes = {};
10902
- __export(exports_worktree_changes, {
10903
- readWorktreeChanges: () => readWorktreeChanges,
10904
- parsePorcelain: () => parsePorcelain2
10905
- });
10906
- import { spawnSync as spawnSync8 } from "child_process";
10907
- function readWorktreeChanges(worktreePath) {
10908
- if (!worktreePath)
10909
- return ZERO;
10910
- try {
10911
- const out = spawnSync8("git", ["status", "--porcelain=v1"], {
10912
- cwd: worktreePath,
10913
- encoding: "utf8",
10914
- stdio: ["ignore", "pipe", "pipe"],
10915
- env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" }
10916
- });
10917
- if (out.status !== 0 || !out.stdout)
10918
- return ZERO;
10919
- return parsePorcelain2(out.stdout);
10920
- } catch {
10921
- return ZERO;
10922
- }
10923
- }
10924
- function parsePorcelain2(text) {
10925
- let added = 0;
10926
- let deleted = 0;
10927
- for (const line of text.split(`
10928
- `)) {
10929
- if (!line || line.startsWith("##"))
10930
- continue;
10931
- const x = line.charAt(0);
10932
- const y = line.charAt(1);
10933
- if (x === "D" || y === "D")
10934
- deleted += 1;
10935
- else
10936
- added += 1;
10937
- }
10938
- return { added, deleted };
10939
- }
10940
- var ZERO;
10941
- var init_worktree_changes = __esm(() => {
10942
- ZERO = { added: 0, deleted: 0 };
10943
- });
10944
-
10945
11173
  // src/cli/api-cmd.ts
10946
11174
  var exports_api_cmd = {};
10947
11175
  __export(exports_api_cmd, {
@@ -13035,16 +13263,21 @@ class DaemonLink {
13035
13263
  const socketPath = allowSpawn ? await ensureDaemonReachable() : defaultDaemonSocketPath();
13036
13264
  const client = new KobeDaemonClient(socketPath);
13037
13265
  await client.connect();
13038
- const hello = await client.request("hello", {
13039
- protocolVersion: DAEMON_PROTOCOL_VERSION,
13040
- minProtocolVersion: MIN_COMPATIBLE_PROTOCOL_VERSION
13041
- });
13042
- if (hello.tasks)
13043
- this.tasks = hello.tasks;
13044
- this.engineStates = {};
13045
- client.on("*", (frame) => this.onFrame(frame.name, frame.payload));
13046
- client.onLifecycle("close", () => this.onDrop(client));
13047
- 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
+ }
13048
13281
  this.client = client;
13049
13282
  this.setConnected(true);
13050
13283
  }
@@ -15101,6 +15334,30 @@ var init_solid = __esm(() => {
15101
15334
  });
15102
15335
 
15103
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
+
15104
15361
  class RemoteOrchestrator {
15105
15362
  client;
15106
15363
  tasksAcc;
@@ -15113,6 +15370,10 @@ class RemoteOrchestrator {
15113
15370
  setDaemonVersionSig;
15114
15371
  engineStateAcc;
15115
15372
  setEngineStateSig;
15373
+ taskJobsAcc;
15374
+ setTaskJobsSig;
15375
+ worktreeChangesAcc;
15376
+ setWorktreeChangesSig;
15116
15377
  uiPrefsAcc;
15117
15378
  setUiPrefsSig;
15118
15379
  keybindingsRevAcc;
@@ -15129,6 +15390,8 @@ class RemoteOrchestrator {
15129
15390
  const [update, setUpdate] = createSignal(null);
15130
15391
  const [daemonVersion, setDaemonVersion] = createSignal(null);
15131
15392
  const [engineState, setEngineState] = createSignal(new Map);
15393
+ const [taskJobs, setTaskJobs] = createSignal(new Map);
15394
+ const [worktreeChanges, setWorktreeChanges] = createSignal(null);
15132
15395
  const [uiPrefs, setUiPrefs] = createSignal(null);
15133
15396
  const [keybindingsRev, setKeybindingsRev] = createSignal(null);
15134
15397
  const [connectionState, setConnectionState] = createSignal("online");
@@ -15142,6 +15405,10 @@ class RemoteOrchestrator {
15142
15405
  this.setDaemonVersionSig = (next) => setDaemonVersion(() => next);
15143
15406
  this.engineStateAcc = engineState;
15144
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);
15145
15412
  this.uiPrefsAcc = uiPrefs;
15146
15413
  this.setUiPrefsSig = (next) => setUiPrefs(() => next);
15147
15414
  this.keybindingsRevAcc = keybindingsRev;
@@ -15202,6 +15469,12 @@ class RemoteOrchestrator {
15202
15469
  if (hello.tasks)
15203
15470
  this.setTasks(hello.tasks.map(deserializeTask));
15204
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
+ }
15205
15478
  this.setConnectionState("online");
15206
15479
  logClient("orch", `subscribed as ${this.role} (${this.tasksAcc().length} tasks)`);
15207
15480
  }
@@ -15234,6 +15507,12 @@ class RemoteOrchestrator {
15234
15507
  engineStateSignal() {
15235
15508
  return this.engineStateAcc;
15236
15509
  }
15510
+ taskJobsSignal() {
15511
+ return this.taskJobsAcc;
15512
+ }
15513
+ worktreeChangesSignal() {
15514
+ return this.worktreeChangesAcc;
15515
+ }
15237
15516
  uiPrefsSignal() {
15238
15517
  return this.uiPrefsAcc;
15239
15518
  }
@@ -15319,8 +15598,11 @@ class RemoteOrchestrator {
15319
15598
  handleEvent(name, payload) {
15320
15599
  if (name === "task.snapshot") {
15321
15600
  const value = payload?.tasks;
15322
- if (Array.isArray(value))
15601
+ if (Array.isArray(value)) {
15323
15602
  this.setTasks(value.map(deserializeTask));
15603
+ this.pruneEngineState(value);
15604
+ this.pruneTaskJobs(value);
15605
+ }
15324
15606
  return;
15325
15607
  }
15326
15608
  if (name === "active-task") {
@@ -15345,6 +15627,34 @@ class RemoteOrchestrator {
15345
15627
  this.setEngineStateSig(next);
15346
15628
  return;
15347
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
+ }
15348
15658
  if (name === "ui-prefs") {
15349
15659
  const p = payload;
15350
15660
  if (typeof p?.theme !== "string")
@@ -15366,6 +15676,38 @@ class RemoteOrchestrator {
15366
15676
  return;
15367
15677
  }
15368
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);
15710
+ }
15369
15711
  }
15370
15712
  function deserializeTask(s) {
15371
15713
  return {
@@ -15389,6 +15731,7 @@ var init_remote_orchestrator = __esm(() => {
15389
15731
  init_daemon_process();
15390
15732
  init_protocol();
15391
15733
  init_dev();
15734
+ init_worktree_changes();
15392
15735
  init_version();
15393
15736
  });
15394
15737
 
@@ -15917,7 +16260,7 @@ function dispatchKeyEvent(bindingStack, evt) {
15917
16260
  continue;
15918
16261
  const hit = cfg.bindings.find((b) => candidates.includes(b.key));
15919
16262
  if (hit) {
15920
- hit.cmd(evt);
16263
+ hit.cmd(evt, hit.slot);
15921
16264
  evt.preventDefault();
15922
16265
  return true;
15923
16266
  }
@@ -16328,7 +16671,7 @@ function joinDrill(typedValue, baseExpanded, name) {
16328
16671
  var init_path_helpers = () => {};
16329
16672
 
16330
16673
  // src/tui/component/new-task-dialog/clone.ts
16331
- import { spawn as spawn3 } from "child_process";
16674
+ import { spawn as spawn4 } from "child_process";
16332
16675
  import * as fs5 from "fs";
16333
16676
  import * as path11 from "path";
16334
16677
  function deriveFolderName(url) {
@@ -16407,7 +16750,7 @@ function cloneRepo(url, target, onProgress) {
16407
16750
  return new Promise((resolve8) => {
16408
16751
  let stderrBuf = "";
16409
16752
  try {
16410
- const child = spawn3("git", ["clone", "--progress", url, target], {
16753
+ const child = spawn4("git", ["clone", "--progress", url, target], {
16411
16754
  stdio: ["ignore", "ignore", "pipe"]
16412
16755
  });
16413
16756
  child.stderr?.setEncoding("utf-8");
@@ -17933,8 +18276,7 @@ function bindByIds(handlers) {
17933
18276
  console.warn(`[kobe/keybindings] bindByIds: id="${id}" has no chords (or doesn't exist in KobeKeymap)`);
17934
18277
  continue;
17935
18278
  }
17936
- for (const c of chords)
17937
- out.push({ key: c, cmd });
18279
+ chords.forEach((c, slot) => out.push({ key: c, cmd, slot }));
17938
18280
  }
17939
18281
  return out;
17940
18282
  }
@@ -18839,20 +19181,28 @@ function UiPrefsSync(props) {
18839
19181
  focusAccent: props.boot.focusAccent
18840
19182
  });
18841
19183
  const [prefsOrch, setPrefsOrch] = createSignal(null);
19184
+ let disposed = false;
18842
19185
  onMount(() => {
18843
19186
  (async () => {
19187
+ let remote = null;
18844
19188
  try {
18845
19189
  const client = await connectIfRunning();
18846
19190
  if (!client) {
18847
19191
  logClient("ui-prefs", "no daemon \u2014 keeping boot-time visual prefs");
18848
19192
  return;
18849
19193
  }
18850
- const remote = new RemoteOrchestrator(client);
19194
+ remote = new RemoteOrchestrator(client);
18851
19195
  await remote.init();
18852
- setPrefsOrch(remote);
18853
19196
  } catch (err) {
18854
19197
  logClientError("ui-prefs", err);
19198
+ remote?.dispose();
19199
+ return;
19200
+ }
19201
+ if (disposed) {
19202
+ remote.dispose();
19203
+ return;
18855
19204
  }
19205
+ setPrefsOrch(remote);
18856
19206
  })();
18857
19207
  });
18858
19208
  createEffect(() => {
@@ -18872,7 +19222,10 @@ function UiPrefsSync(props) {
18872
19222
  lastKeybindingsRev = rev;
18873
19223
  reloadUserKeybindings();
18874
19224
  });
18875
- onCleanup(() => prefsOrch()?.dispose());
19225
+ onCleanup(() => {
19226
+ disposed = true;
19227
+ prefsOrch()?.dispose();
19228
+ });
18876
19229
  return null;
18877
19230
  }
18878
19231
  async function bootPaneHost(opts) {
@@ -19097,6 +19450,31 @@ function repoBasename(repo) {
19097
19450
  function flattenIds(rows) {
19098
19451
  return rows.map((r) => r.task.id);
19099
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
+ }
19100
19478
  var init_groups = () => {};
19101
19479
 
19102
19480
  // src/tui/quick-task/host.tsx
@@ -22039,7 +22417,7 @@ var init_task_actions = __esm(() => {
22039
22417
  });
22040
22418
 
22041
22419
  // src/tui/lib/worktree-opener.ts
22042
- import { spawn as spawn4 } from "child_process";
22420
+ import { spawn as spawn5 } from "child_process";
22043
22421
  import { existsSync as existsSync15 } from "fs";
22044
22422
  import { basename as basename7, delimiter, isAbsolute as isAbsolute2, join as join17 } from "path";
22045
22423
  function executableOnPath(command, env, exists) {
@@ -22098,7 +22476,7 @@ function buildOpenWorktreeCommand(worktreePath, opener) {
22098
22476
  function openWorktree(worktreePath, opener, deps = {}) {
22099
22477
  if (!worktreePath)
22100
22478
  return false;
22101
- const spawnFn = deps.spawn ?? spawn4;
22479
+ const spawnFn = deps.spawn ?? spawn5;
22102
22480
  const [command, args2] = buildOpenWorktreeCommand(worktreePath, opener);
22103
22481
  try {
22104
22482
  const child = spawnFn(command, args2, { detached: true, stdio: "ignore" });
@@ -22147,15 +22525,6 @@ var init_worktree_opener = __esm(() => {
22147
22525
  });
22148
22526
 
22149
22527
  // src/tui/lib/background-poll.ts
22150
- import { spawn as spawn5 } from "child_process";
22151
- function computeNextAllowedAt(startedAt, finishedAt, timedOut, cfg) {
22152
- if (timedOut)
22153
- return startedAt + cfg.slowRetryMs;
22154
- return finishedAt + Math.max(cfg.minIntervalMs, (finishedAt - startedAt) * 5);
22155
- }
22156
- function shouldPoll(state, now) {
22157
- return !state.inFlight && now >= state.nextAllowedAt;
22158
- }
22159
22528
  function createBackgroundPoller(cfg) {
22160
22529
  const entries = new Map;
22161
22530
  function entryFor(key) {
@@ -22177,58 +22546,17 @@ function createBackgroundPoller(cfg) {
22177
22546
  if (!key)
22178
22547
  return;
22179
22548
  const entry = entryFor(key);
22180
- const startedAt = Date.now();
22181
- if (!shouldPoll(entry, startedAt))
22182
- return;
22183
- entry.inFlight = true;
22184
- const controller = new AbortController;
22185
- const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
22186
- (async () => {
22187
- let value;
22188
- let ok = false;
22189
- try {
22190
- value = await cfg.run(key, controller.signal);
22191
- ok = true;
22192
- } catch {}
22193
- clearTimeout(timer);
22194
- const timedOut = controller.signal.aborted;
22195
- entry.nextAllowedAt = computeNextAllowedAt(startedAt, Date.now(), timedOut, cfg);
22196
- entry.inFlight = false;
22197
- if (ok && !timedOut)
22198
- entry.write(value);
22199
- })();
22549
+ maybeStartScheduledRun(entry, cfg, (signal) => cfg.run(key, signal), (value) => entry.write(value));
22200
22550
  },
22201
22551
  reset() {
22202
22552
  entries.clear();
22203
22553
  }
22204
22554
  };
22205
22555
  }
22206
- function spawnCapture(cmd, args2, opts) {
22207
- return new Promise((resolve9) => {
22208
- let out = "";
22209
- let settled = false;
22210
- const finish = (status) => {
22211
- if (settled)
22212
- return;
22213
- settled = true;
22214
- resolve9({ status, stdout: out });
22215
- };
22216
- const child = spawn5(cmd, args2.slice(), {
22217
- cwd: opts.cwd,
22218
- stdio: ["ignore", "pipe", "ignore"],
22219
- env: opts.env,
22220
- signal: opts.signal,
22221
- killSignal: "SIGKILL"
22222
- });
22223
- child.stdout?.on("data", (chunk) => {
22224
- out += String(chunk);
22225
- });
22226
- child.on("error", () => finish(null));
22227
- child.on("close", (code) => finish(code));
22228
- });
22229
- }
22230
22556
  var init_background_poll = __esm(() => {
22231
22557
  init_dev();
22558
+ init_poll_scheduling();
22559
+ init_poll_scheduling();
22232
22560
  });
22233
22561
 
22234
22562
  // src/tui/panes/sidebar/git-head.ts
@@ -22366,20 +22694,18 @@ function useSidebarBindings(opts) {
22366
22694
  useBindings(() => ({
22367
22695
  enabled: opts.focused() && !searchModeAccessor(),
22368
22696
  bindings: bindByIds({
22369
- "sidebar.nav": (evt) => {
22697
+ "sidebar.nav": (_evt, slot) => {
22698
+ const down = (slot ?? 0) % 2 === 0;
22370
22699
  if (moveModeAccessor()) {
22371
22700
  const id = cursorTaskId();
22372
22701
  if (id === undefined)
22373
22702
  return;
22374
- if (evt.name === "j" || evt.name === "down")
22375
- opts.onMoveRequest?.(id, 1);
22376
- else if (evt.name === "k" || evt.name === "up")
22377
- opts.onMoveRequest?.(id, -1);
22703
+ opts.onMoveRequest?.(id, down ? 1 : -1);
22378
22704
  return;
22379
22705
  }
22380
- if (evt.name === "j" || evt.name === "down")
22706
+ if (down)
22381
22707
  ctrl.moveDown();
22382
- else if (evt.name === "k" || evt.name === "up")
22708
+ else
22383
22709
  ctrl.moveUp();
22384
22710
  },
22385
22711
  "sidebar.select": () => {
@@ -22453,21 +22779,18 @@ function useSidebarBindings(opts) {
22453
22779
  useBindings(() => ({
22454
22780
  enabled: opts.focused(),
22455
22781
  bindings: bindByIds({
22456
- "sidebar.view": (evt) => {
22457
- if (evt.name === "]")
22458
- opts.onViewSwitch?.(1);
22459
- else
22460
- opts.onViewSwitch?.(-1);
22782
+ "sidebar.view": (_evt, slot) => {
22783
+ opts.onViewSwitch?.((slot ?? 0) % 2 === 0 ? -1 : 1);
22461
22784
  }
22462
22785
  })
22463
22786
  }));
22464
22787
  useBindings(() => ({
22465
22788
  enabled: opts.focused() && searchModeAccessor(),
22466
22789
  bindings: bindByIds({
22467
- "sidebar.search.nav": (evt) => {
22468
- if (evt.name === "down")
22790
+ "sidebar.search.nav": (_evt, slot) => {
22791
+ if ((slot ?? 0) % 2 === 0)
22469
22792
  ctrl.moveDown();
22470
- else if (evt.name === "up")
22793
+ else
22471
22794
  ctrl.moveUp();
22472
22795
  },
22473
22796
  "sidebar.search.submit": () => {
@@ -22523,11 +22846,12 @@ function buildSidebarRowView(opts) {
22523
22846
  const activityBadge = activityBadgeFor(activityState);
22524
22847
  const activityLabel = activityLabelFor(activityState);
22525
22848
  const untrackedCustomEngine = isCustomEngineTask(task) && !hasActivity;
22526
- 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");
22527
22851
  const spinner = IN_PROGRESS_SPINNER[opts.spinnerFrame] ?? IN_PROGRESS_SPINNER[0];
22528
- 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);
22529
22853
  const fallbackSubtitle = untrackedCustomEngine ? NO_TRACKING_SUBTITLE : STATUS_LABEL[task.status];
22530
- 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);
22531
22855
  const restGlyph = untrackedCustomEngine ? NO_TRACKING_GLYPH : activityBadge?.glyph ?? badge.glyph;
22532
22856
  const restProjectGlyph = untrackedCustomEngine ? NO_TRACKING_GLYPH : activityBadge?.glyph ?? "\u2605";
22533
22857
  return {
@@ -22554,7 +22878,7 @@ function activityBadgeFor(state) {
22554
22878
  return null;
22555
22879
  }
22556
22880
  }
22557
- 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";
22558
22882
  var init_row_view = __esm(() => {
22559
22883
  init_vendor();
22560
22884
  init_groups();
@@ -22592,7 +22916,7 @@ var init_worktree_changes_poller = __esm(() => {
22592
22916
  ZERO2 = { added: 0, deleted: 0 };
22593
22917
  poller2 = createBackgroundPoller({
22594
22918
  initial: ZERO2,
22595
- equals: (a, b) => a.added === b.added && a.deleted === b.deleted,
22919
+ equals: sameWorktreeChanges,
22596
22920
  timeoutMs: POLL_TIMEOUT_MS,
22597
22921
  slowRetryMs: SLOW_REPO_RETRY_MS,
22598
22922
  minIntervalMs: MIN_POLL_INTERVAL_MS,
@@ -22680,7 +23004,7 @@ function Sidebar(props) {
22680
23004
  const spinnerInterval = setInterval(() => setSpinnerFrame((n) => (n + 1) % IN_PROGRESS_SPINNER.length), SPINNER_FRAME_MS);
22681
23005
  onCleanup(() => clearInterval(spinnerInterval));
22682
23006
  const sortMode = () => props.sortMode?.() ?? "default";
22683
- const rows = createMemo(() => buildRows(props.tasks(), view(), searchMode() ? searchQuery() : "", sortMode()));
23007
+ const rows = createMemo((prev) => reconcileSidebarRows(prev, buildRows(props.tasks(), view(), searchMode() ? searchQuery() : "", sortMode())), []);
22684
23008
  const flatIds = createMemo(() => flattenIds(rows()));
22685
23009
  const firstTaskFlatIndex = createMemo(() => {
22686
23010
  const r = rows();
@@ -23029,10 +23353,15 @@ function Sidebar(props) {
23029
23353
  }
23030
23354
  };
23031
23355
  const changes = createMemo(() => {
23356
+ const pushed = pickPushedChanges(props.worktreeChanges?.(), task.worktreePath);
23357
+ if (pushed)
23358
+ return pushed;
23032
23359
  branchTick();
23033
23360
  if (!task.archived)
23034
23361
  pollWorktreeChanges(task.worktreePath);
23035
23362
  return worktreeChanges(task.worktreePath);
23363
+ }, undefined, {
23364
+ equals: sameWorktreeChanges
23036
23365
  });
23037
23366
  const projectBranch = createMemo(() => {
23038
23367
  branchTick();
@@ -23043,6 +23372,7 @@ function Sidebar(props) {
23043
23372
  const rowView = createMemo(() => buildSidebarRowView({
23044
23373
  task,
23045
23374
  activity: props.engineState?.().get(task.id),
23375
+ job: props.taskJobs?.().get(task.id),
23046
23376
  live: isLive(),
23047
23377
  spinnerFrame: spinnerFrame(),
23048
23378
  subtitleBudget: subtitleBudget(),
@@ -23475,6 +23805,7 @@ var init_Sidebar = __esm(() => {
23475
23805
  init_groups();
23476
23806
  init_keys();
23477
23807
  init_row_view();
23808
+ init_worktree_changes();
23478
23809
  init_worktree_changes_poller();
23479
23810
  VIEW_TABS = [{
23480
23811
  view: "active",
@@ -23825,6 +24156,17 @@ function TasksShell(props) {
23825
24156
  get engineState() {
23826
24157
  return memo2(() => !!props.orch)() ? props.orch.engineStateSignal() : undefined;
23827
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
+ },
23828
24170
  onRenameRequest: (id) => void renameTask(id),
23829
24171
  onDeleteRequest: (id) => void deleteTask(id),
23830
24172
  onArchiveRequest: (id) => void archiveTask(id),
@@ -25028,26 +25370,24 @@ function useFileTreeBindings(opts) {
25028
25370
  useBindings(() => ({
25029
25371
  enabled: opts.focused(),
25030
25372
  bindings: bindByIds({
25031
- "files.nav": (evt) => {
25032
- if (evt.name === "j" || evt.name === "down")
25373
+ "files.nav": (_evt, slot) => {
25374
+ if ((slot ?? 0) % 2 === 0)
25033
25375
  opts.moveDown();
25034
- else if (evt.name === "k" || evt.name === "up")
25376
+ else
25035
25377
  opts.moveUp();
25036
25378
  },
25037
- "files.hierarchy": (evt) => {
25038
- if (evt.name === "l" || evt.name === "right")
25039
- opts.expandOrDescend();
25040
- else if (evt.name === "h" || evt.name === "left")
25379
+ "files.hierarchy": (_evt, slot) => {
25380
+ if ((slot ?? 0) % 2 === 0)
25041
25381
  opts.collapseOrParent();
25382
+ else
25383
+ opts.expandOrDescend();
25042
25384
  },
25043
- "files.tab": (evt) => {
25385
+ "files.tab": (_evt, slot) => {
25044
25386
  const cur = opts.currentTab();
25045
25387
  const idx = TAB_ORDER.indexOf(cur);
25046
25388
  if (idx < 0)
25047
25389
  return;
25048
- const delta = evt.name === "[" ? -1 : evt.name === "]" ? 1 : 0;
25049
- if (delta === 0)
25050
- return;
25390
+ const delta = (slot ?? 0) % 2 === 0 ? -1 : 1;
25051
25391
  const next = TAB_ORDER[(idx + delta + TAB_ORDER.length) % TAB_ORDER.length];
25052
25392
  if (next)
25053
25393
  opts.setTab(next);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@sma1lboy/kobe",
4
- "version": "0.7.18",
4
+ "version": "0.7.19",
5
5
  "description": "TUI orchestrator for Claude Code (codename)",
6
6
  "type": "module",
7
7
  "packageManager": "bun@1.3.13",