@sma1lboy/kobe 0.7.18 → 0.7.20

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 +831 -406
  2. package/package.json +2 -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.20",
94
94
  description: "TUI orchestrator for Claude Code (codename)",
95
95
  type: "module",
96
96
  packageManager: "bun@1.3.13",
@@ -123,6 +123,7 @@ var init_package = __esm(() => {
123
123
  "test:fast": "vitest run --passWithNoTests",
124
124
  "test:socket": "KOBE_INCLUDE_SOCKET=1 vitest run test/daemon --pool forks --minWorkers=1 --maxWorkers=1 --passWithNoTests",
125
125
  "test:behavior": "KOBE_INCLUDE_BEHAVIOR=1 vitest run test/behavior --passWithNoTests",
126
+ bench: "vitest bench --run",
126
127
  lint: "biome check .",
127
128
  knip: "knip-bun",
128
129
  postinstall: "bun run scripts/check-preview-deps.ts || true",
@@ -3919,7 +3920,9 @@ var init_protocol = __esm(() => {
3919
3920
  "update",
3920
3921
  "engine-state",
3921
3922
  "ui-prefs",
3922
- "keybindings"
3923
+ "keybindings",
3924
+ "task.jobs",
3925
+ "worktree.changes"
3923
3926
  ];
3924
3927
  });
3925
3928
 
@@ -4067,6 +4070,7 @@ class KobeDaemonClient {
4067
4070
  this.disposed = true;
4068
4071
  this.socket?.end();
4069
4072
  this.socket = null;
4073
+ this.failPending();
4070
4074
  }
4071
4075
  forceDisconnect() {
4072
4076
  const socket = this.socket;
@@ -4074,6 +4078,15 @@ class KobeDaemonClient {
4074
4078
  return;
4075
4079
  this.socket = null;
4076
4080
  socket.destroy();
4081
+ this.failPending();
4082
+ }
4083
+ failPending() {
4084
+ if (this.pending.size === 0)
4085
+ return;
4086
+ const err = new Error("daemon connection closed");
4087
+ for (const pending of this.pending.values())
4088
+ pending.reject(err);
4089
+ this.pending.clear();
4077
4090
  }
4078
4091
  on(name, handler) {
4079
4092
  let set = this.handlers.get(name);
@@ -4148,9 +4161,7 @@ class KobeDaemonClient {
4148
4161
  if (this.socket !== which)
4149
4162
  return;
4150
4163
  this.socket = null;
4151
- for (const pending of this.pending.values())
4152
- pending.reject(new Error("daemon connection closed"));
4153
- this.pending.clear();
4164
+ this.failPending();
4154
4165
  this.emitLifecycle("close");
4155
4166
  }
4156
4167
  emitLifecycle(name) {
@@ -5059,66 +5070,6 @@ var init_history = __esm(() => {
5059
5070
  };
5060
5071
  });
5061
5072
 
5062
- // src/engine/claude-code-local/cost.ts
5063
- import { readFile as readFile3 } from "fs/promises";
5064
- async function summarizeClaudeWorktreeCost(worktree) {
5065
- const files = await listSessionFilesForWorktree(worktree);
5066
- const base = {
5067
- sessionCount: files.length,
5068
- inputTokens: 0,
5069
- outputTokens: 0,
5070
- cacheReadTokens: 0,
5071
- cacheCreateTokens: 0,
5072
- lastActivityMs: files[0]?.mtimeMs ?? null
5073
- };
5074
- if (files.length === 0)
5075
- return base;
5076
- let input = 0;
5077
- let output = 0;
5078
- let cacheRead = 0;
5079
- let cacheCreate = 0;
5080
- for (const file of files) {
5081
- let raw;
5082
- try {
5083
- raw = await readFile3(file.path, "utf8");
5084
- } catch {
5085
- continue;
5086
- }
5087
- for (const line of raw.split(`
5088
- `)) {
5089
- if (line.length === 0)
5090
- continue;
5091
- let parsed;
5092
- try {
5093
- parsed = JSON.parse(line);
5094
- } catch {
5095
- continue;
5096
- }
5097
- const usage = parsed.message?.usage;
5098
- if (!usage)
5099
- continue;
5100
- if (typeof usage.input_tokens === "number")
5101
- input += usage.input_tokens;
5102
- if (typeof usage.output_tokens === "number")
5103
- output += usage.output_tokens;
5104
- if (typeof usage.cache_read_input_tokens === "number")
5105
- cacheRead += usage.cache_read_input_tokens;
5106
- if (typeof usage.cache_creation_input_tokens === "number")
5107
- cacheCreate += usage.cache_creation_input_tokens;
5108
- }
5109
- }
5110
- return {
5111
- ...base,
5112
- inputTokens: input,
5113
- outputTokens: output,
5114
- cacheReadTokens: cacheRead,
5115
- cacheCreateTokens: cacheCreate
5116
- };
5117
- }
5118
- var init_cost = __esm(() => {
5119
- init_history();
5120
- });
5121
-
5122
5073
  // src/cli/invocation.ts
5123
5074
  import { fileURLToPath } from "url";
5124
5075
  function kobeCliInvocation() {
@@ -5215,7 +5166,7 @@ function opsPaneCommand(args) {
5215
5166
  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;
5216
5167
 
5217
5168
  // src/engine/claude-code-local/hook-adapter.ts
5218
- import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
5169
+ import { mkdir as mkdir5, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
5219
5170
  import { dirname as dirname4 } from "path";
5220
5171
  function failureFromErrorType(errorType) {
5221
5172
  if (typeof errorType !== "string")
@@ -5231,7 +5182,7 @@ function isObject2(v) {
5231
5182
  }
5232
5183
  async function readJsonObject(path8) {
5233
5184
  try {
5234
- const parsed = JSON.parse(await readFile4(path8, "utf8"));
5185
+ const parsed = JSON.parse(await readFile3(path8, "utf8"));
5235
5186
  return isObject2(parsed) ? parsed : {};
5236
5187
  } catch {
5237
5188
  return {};
@@ -5445,7 +5396,7 @@ function validPositive(v) {
5445
5396
  }
5446
5397
 
5447
5398
  // src/engine/codex-local/history.ts
5448
- import { readFile as readFile5, readdir as readdir2, stat as stat2, unlink as unlink3 } from "fs/promises";
5399
+ import { readFile as readFile4, readdir as readdir2, stat as stat2, unlink as unlink3 } from "fs/promises";
5449
5400
  import { homedir as homedir9 } from "os";
5450
5401
  import path8 from "path";
5451
5402
  async function listRolloutFiles(deps = defaultDeps7) {
@@ -5492,6 +5443,26 @@ function rolloutCwd(raw) {
5492
5443
  }
5493
5444
  return "";
5494
5445
  }
5446
+ async function rolloutCwdForFile(file, deps) {
5447
+ let cache = rolloutCwdCaches.get(deps);
5448
+ if (!cache) {
5449
+ cache = new Map;
5450
+ rolloutCwdCaches.set(deps, cache);
5451
+ }
5452
+ const hit = cache.get(file);
5453
+ if (hit !== undefined)
5454
+ return hit;
5455
+ let raw;
5456
+ try {
5457
+ raw = await deps.readFile(file);
5458
+ } catch {
5459
+ return null;
5460
+ }
5461
+ const cwd = rolloutCwd(raw);
5462
+ if (cwd)
5463
+ cache.set(file, cwd);
5464
+ return cwd;
5465
+ }
5495
5466
  async function listSessionIdsForWorktree(worktree, deps = defaultDeps7) {
5496
5467
  if (!worktree)
5497
5468
  return [];
@@ -5502,13 +5473,7 @@ async function listSessionIdsForWorktree(worktree, deps = defaultDeps7) {
5502
5473
  if (scanned >= MAX_WORKTREE_SCAN)
5503
5474
  break;
5504
5475
  scanned++;
5505
- let raw;
5506
- try {
5507
- raw = await deps.readFile(file);
5508
- } catch {
5509
- continue;
5510
- }
5511
- if (rolloutCwd(raw) !== worktree)
5476
+ if (await rolloutCwdForFile(file, deps) !== worktree)
5512
5477
  continue;
5513
5478
  const id = path8.basename(file).match(UUID_AT_END)?.[1];
5514
5479
  if (id)
@@ -5516,30 +5481,25 @@ async function listSessionIdsForWorktree(worktree, deps = defaultDeps7) {
5516
5481
  }
5517
5482
  return matches.reverse();
5518
5483
  }
5519
- async function latestTranscriptMtimeForWorktree2(worktree, deps = defaultDeps7) {
5484
+ async function findLatestRolloutForWorktree(worktree, deps = defaultDeps7) {
5520
5485
  if (!worktree)
5521
- return 0;
5486
+ return null;
5522
5487
  const files = await listRolloutFiles(deps);
5523
5488
  let scanned = 0;
5524
5489
  for (const file of files) {
5525
5490
  if (scanned >= MAX_MTIME_SCAN)
5526
5491
  break;
5527
5492
  scanned++;
5528
- let raw;
5529
- try {
5530
- raw = await deps.readFile(file);
5531
- } catch {
5532
- continue;
5533
- }
5534
- if (rolloutCwd(raw) !== worktree)
5493
+ if (await rolloutCwdForFile(file, deps) !== worktree)
5535
5494
  continue;
5536
5495
  try {
5537
- return (await deps.stat(file)).mtimeMs;
5538
- } catch {
5539
- return 0;
5540
- }
5496
+ return { path: file, mtimeMs: (await deps.stat(file)).mtimeMs };
5497
+ } catch {}
5541
5498
  }
5542
- return 0;
5499
+ return null;
5500
+ }
5501
+ async function latestTranscriptMtimeForWorktree2(worktree, deps = defaultDeps7) {
5502
+ return (await findLatestRolloutForWorktree(worktree, deps))?.mtimeMs ?? 0;
5543
5503
  }
5544
5504
  async function readHistory2(sessionId, deps = defaultDeps7) {
5545
5505
  return (await readHistoryWithMetrics(sessionId, deps)).messages;
@@ -5772,7 +5732,7 @@ function parseTimestampMs(value) {
5772
5732
  function isObject4(v) {
5773
5733
  return typeof v === "object" && v !== null && !Array.isArray(v);
5774
5734
  }
5775
- var defaultDeps7, UUID_AT_END, MAX_WORKTREE_SCAN = 200, MAX_MTIME_SCAN = 12;
5735
+ var defaultDeps7, UUID_AT_END, rolloutCwdCaches, MAX_WORKTREE_SCAN = 200, MAX_MTIME_SCAN = 12;
5776
5736
  var init_history2 = __esm(() => {
5777
5737
  init_synthetic();
5778
5738
  defaultDeps7 = {
@@ -5787,11 +5747,12 @@ var init_history2 = __esm(() => {
5787
5747
  }
5788
5748
  },
5789
5749
  async readFile(p) {
5790
- return await readFile5(p, "utf8");
5750
+ return await readFile4(p, "utf8");
5791
5751
  },
5792
5752
  stat: stat2
5793
5753
  };
5794
5754
  UUID_AT_END = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i;
5755
+ rolloutCwdCaches = new WeakMap;
5795
5756
  });
5796
5757
 
5797
5758
  // src/engine/copilot-local/usage.ts
@@ -5829,7 +5790,7 @@ function numberOr2(value, fallback) {
5829
5790
  }
5830
5791
 
5831
5792
  // src/engine/copilot-local/history.ts
5832
- import { readFile as readFile6, readdir as readdir3, rm, stat as stat3 } from "fs/promises";
5793
+ import { readFile as readFile5, readdir as readdir3, rm, stat as stat3 } from "fs/promises";
5833
5794
  import { homedir as homedir10 } from "os";
5834
5795
  import path9 from "path";
5835
5796
  async function listSessionDirs(deps = defaultDeps8) {
@@ -5941,7 +5902,7 @@ function parseEvents(raw, fallbackSessionId) {
5941
5902
  if (!text)
5942
5903
  continue;
5943
5904
  if (!firstUserMessage)
5944
- firstUserMessage = text.slice(0, PREVIEW_CHAR_CAP);
5905
+ firstUserMessage = Buffer.from(text.slice(0, PREVIEW_CHAR_CAP), "utf8").toString("utf8");
5945
5906
  messages.push({ role: "user", blocks: [{ type: "text", text }], timestamp, sessionId });
5946
5907
  continue;
5947
5908
  }
@@ -6009,7 +5970,7 @@ var init_history3 = __esm(() => {
6009
5970
  }
6010
5971
  },
6011
5972
  async readFile(p) {
6012
- return await readFile6(p, "utf8");
5973
+ return await readFile5(p, "utf8");
6013
5974
  },
6014
5975
  stat: stat3,
6015
5976
  async rm(p) {
@@ -6048,7 +6009,7 @@ var init_hook_adapter2 = __esm(() => {
6048
6009
  });
6049
6010
 
6050
6011
  // src/engine/turn-detector.ts
6051
- import { readFile as readFile7 } from "fs/promises";
6012
+ import { readFile as readFile6 } from "fs/promises";
6052
6013
 
6053
6014
  class EngineTurnDetector {
6054
6015
  supportsCompletionMarkers() {
@@ -6121,42 +6082,71 @@ function timestampFromRecord(record, fallback) {
6121
6082
  function isObject7(v) {
6122
6083
  return typeof v === "object" && v !== null && !Array.isArray(v);
6123
6084
  }
6124
- var ClaudeTurnDetector, CodexTurnDetector, UnknownTurnDetector;
6085
+ var defaultClaudeDeps, ClaudeTurnDetector, defaultCodexDeps, CodexTurnDetector, UnknownTurnDetector;
6125
6086
  var init_turn_detector = __esm(() => {
6126
6087
  init_history();
6127
6088
  init_history2();
6128
6089
  init_registry();
6090
+ defaultClaudeDeps = {
6091
+ listSessionFiles: (worktree) => listSessionFilesForWorktree(worktree),
6092
+ readFile: (path10) => readFile6(path10, "utf8")
6093
+ };
6129
6094
  ClaudeTurnDetector = class ClaudeTurnDetector extends EngineTurnDetector {
6095
+ deps;
6130
6096
  vendor = "claude";
6097
+ cache = new Map;
6098
+ constructor(deps = defaultClaudeDeps) {
6099
+ super();
6100
+ this.deps = deps;
6101
+ }
6131
6102
  async latestCompletion(worktree) {
6132
- const files = await listSessionFilesForWorktree(worktree);
6103
+ const files = await this.deps.listSessionFiles(worktree);
6133
6104
  let latest = null;
6105
+ const next = new Map;
6134
6106
  for (const file of files.slice(0, 4)) {
6135
- const raw = await readFile7(file.path, "utf8").catch(() => "");
6136
- const marker = latestClaudeCompletionMarkerFromJsonl(raw, file.path, file.mtimeMs);
6107
+ const hit = this.cache.get(file.path);
6108
+ let marker;
6109
+ if (hit && file.mtimeMs > 0 && hit.mtimeMs === file.mtimeMs) {
6110
+ marker = hit.marker;
6111
+ } else {
6112
+ const raw = await this.deps.readFile(file.path).catch(() => "");
6113
+ marker = latestClaudeCompletionMarkerFromJsonl(raw, file.path, file.mtimeMs);
6114
+ }
6115
+ next.set(file.path, { mtimeMs: file.mtimeMs, marker });
6137
6116
  if (marker && (!latest || marker.timestampMs > latest.timestampMs))
6138
6117
  latest = marker;
6139
6118
  }
6119
+ this.cache = next;
6140
6120
  return latest;
6141
6121
  }
6142
6122
  };
6123
+ defaultCodexDeps = {
6124
+ findLatestRollout: (worktree) => findLatestRolloutForWorktree(worktree),
6125
+ readFile: (path10) => readFile6(path10, "utf8")
6126
+ };
6143
6127
  CodexTurnDetector = class CodexTurnDetector extends EngineTurnDetector {
6128
+ deps;
6144
6129
  vendor = "codex";
6130
+ cache = null;
6131
+ constructor(deps = defaultCodexDeps) {
6132
+ super();
6133
+ this.deps = deps;
6134
+ }
6145
6135
  async latestCompletion(worktree) {
6146
6136
  if (!worktree)
6147
6137
  return null;
6148
- const files = await listRolloutFiles();
6149
- let scanned = 0;
6150
- for (const file of files) {
6151
- if (scanned >= 12)
6152
- break;
6153
- scanned++;
6154
- const raw = await readFile7(file, "utf8").catch(() => "");
6155
- if (!raw || rolloutCwd(raw) !== worktree)
6156
- continue;
6157
- return latestCodexCompletionMarkerFromJsonl(raw, file);
6138
+ const found = await this.deps.findLatestRollout(worktree);
6139
+ if (!found)
6140
+ return null;
6141
+ if (this.cache && this.cache.path === found.path && found.mtimeMs > 0 && this.cache.mtimeMs === found.mtimeMs) {
6142
+ return this.cache.marker;
6158
6143
  }
6159
- return null;
6144
+ const raw = await this.deps.readFile(found.path).catch(() => "");
6145
+ if (!raw)
6146
+ return null;
6147
+ const marker = latestCodexCompletionMarkerFromJsonl(raw, found.path);
6148
+ this.cache = { path: found.path, mtimeMs: found.mtimeMs, marker };
6149
+ return marker;
6160
6150
  }
6161
6151
  };
6162
6152
  UnknownTurnDetector = class UnknownTurnDetector extends EngineTurnDetector {
@@ -6182,7 +6172,6 @@ function customEngineEntry(vendor) {
6182
6172
  displayName: vendor,
6183
6173
  defaultCommand: [vendor],
6184
6174
  history: EMPTY_HISTORY,
6185
- summarizeCost: null,
6186
6175
  detectAccount: async () => ({
6187
6176
  binary: { found: false, error: "custom engine: kobe has no account detector for it" },
6188
6177
  account: { kind: "none" }
@@ -6198,7 +6187,6 @@ var EMPTY_HISTORY, claudeHistoryReader, codexHistoryReader, copilotHistoryReader
6198
6187
  var init_registry = __esm(() => {
6199
6188
  init_vendor();
6200
6189
  init_account_detect();
6201
- init_cost();
6202
6190
  init_history();
6203
6191
  init_hook_adapter();
6204
6192
  init_history2();
@@ -6241,7 +6229,6 @@ var init_registry = __esm(() => {
6241
6229
  displayName: "Claude",
6242
6230
  defaultCommand: ["claude"],
6243
6231
  history: claudeHistoryReader,
6244
- summarizeCost: (worktree) => summarizeClaudeWorktreeCost(worktree),
6245
6232
  detectAccount: (deps) => detectClaudeAccount(deps),
6246
6233
  createHookAdapter: () => new ClaudeHookAdapter,
6247
6234
  createTurnDetector: () => new ClaudeTurnDetector
@@ -6252,7 +6239,6 @@ var init_registry = __esm(() => {
6252
6239
  displayName: "Codex",
6253
6240
  defaultCommand: ["codex"],
6254
6241
  history: codexHistoryReader,
6255
- summarizeCost: null,
6256
6242
  detectAccount: (deps) => detectCodexAccount(deps),
6257
6243
  createHookAdapter: () => new NoopHookAdapter("codex"),
6258
6244
  createTurnDetector: () => new CodexTurnDetector
@@ -6263,7 +6249,6 @@ var init_registry = __esm(() => {
6263
6249
  displayName: "Copilot",
6264
6250
  defaultCommand: ["copilot"],
6265
6251
  history: copilotHistoryReader,
6266
- summarizeCost: null,
6267
6252
  detectAccount: (deps) => detectCopilotAccount(deps),
6268
6253
  createHookAdapter: () => new NoopHookAdapter("copilot"),
6269
6254
  createTurnDetector: () => new UnknownTurnDetector("copilot")
@@ -6277,7 +6262,8 @@ function titleFromMessages(messages) {
6277
6262
  if (!firstUser)
6278
6263
  return "";
6279
6264
  const text = firstUser.blocks.filter((b) => b.type === "text").map((b) => b.text).join(" ");
6280
- return deriveTitleFromPrompt(text);
6265
+ const title = deriveTitleFromPrompt(text);
6266
+ return title.length > 0 ? Buffer.from(title, "utf8").toString("utf8") : title;
6281
6267
  }
6282
6268
  async function deriveTitleFromSession(worktree, vendor = DEFAULT_TASK_VENDOR) {
6283
6269
  if (!worktree)
@@ -6331,6 +6317,7 @@ __export(exports_client, {
6331
6317
  globalTasksPaneWidth: () => globalTasksPaneWidth,
6332
6318
  getSessionOptions: () => getSessionOptions,
6333
6319
  getSessionOption: () => getSessionOption,
6320
+ getServerOptions: () => getServerOptions,
6334
6321
  getServerOption: () => getServerOption,
6335
6322
  ensureFallbackSession: () => ensureFallbackSession,
6336
6323
  currentSessionName: () => currentSessionName,
@@ -6469,6 +6456,22 @@ async function getServerOption(option) {
6469
6456
  const { code, stdout } = await runTmuxCapturing(["show-options", "-sqv", option]);
6470
6457
  return code === 0 ? stdout.trim() : "";
6471
6458
  }
6459
+ async function getServerOptions(options) {
6460
+ const values = Object.fromEntries(options.map((option) => [option, undefined]));
6461
+ const { code, stdout } = await runTmuxSequenceCapturing(options.map((option) => ["show-options", "-sq", option]));
6462
+ if (code !== 0)
6463
+ return values;
6464
+ for (const line of stdout.split(`
6465
+ `)) {
6466
+ const idx = line.indexOf(" ");
6467
+ if (idx <= 0)
6468
+ continue;
6469
+ const option = line.slice(0, idx);
6470
+ if (option in values)
6471
+ values[option] = line.slice(idx + 1).trim();
6472
+ }
6473
+ return values;
6474
+ }
6472
6475
  async function globalTasksPaneWidth() {
6473
6476
  const raw = await getServerOption(TASKS_WIDTH_OPTION);
6474
6477
  const n = Number.parseInt(raw, 10);
@@ -6626,18 +6629,18 @@ async function listChatTabWindows(session, runner = realRunner) {
6626
6629
  "-t",
6627
6630
  `=${session}`,
6628
6631
  "-F",
6629
- `#{window_index} #{${CHAT_TAB_SESSION_ID_OPTION}}`
6632
+ `#{window_index} #{automatic-rename} #{${CHAT_TAB_SESSION_ID_OPTION}}`
6630
6633
  ]);
6631
6634
  if (code !== 0)
6632
6635
  return [];
6633
6636
  const out = [];
6634
6637
  for (const line of stdout.split(`
6635
6638
  `)) {
6636
- const tab = line.indexOf("\t");
6637
- const index = Number.parseInt((tab >= 0 ? line.slice(0, tab) : line).trim(), 10);
6639
+ const [indexField, autoRename, sessionId] = line.split("\t");
6640
+ const index = Number.parseInt((indexField ?? "").trim(), 10);
6638
6641
  if (!Number.isInteger(index))
6639
6642
  continue;
6640
- out.push({ index, sessionId: tab >= 0 ? line.slice(tab + 1).trim() : "" });
6643
+ out.push({ index, sessionId: sessionId?.trim() ?? "", autoRename: autoRename?.trim() ?? "" });
6641
6644
  }
6642
6645
  return out;
6643
6646
  }
@@ -6650,11 +6653,27 @@ async function windowNamedManually(session, index, runner) {
6650
6653
  ]);
6651
6654
  return code === 0 && /\boff\b/.test(stdout);
6652
6655
  }
6656
+ async function globalAutomaticRenameOff(runner) {
6657
+ const { code, stdout } = await runner.capture(["show-window-options", "-g", "automatic-rename"]);
6658
+ return code === 0 && /\boff\b/.test(stdout);
6659
+ }
6653
6660
  async function renameWindow(session, index, title, runner) {
6654
6661
  return await runner.run(["rename-window", "-t", `=${session}:${index}`, "--", title]) === 0;
6655
6662
  }
6656
6663
  async function runChatTabNamingPass(orch, deps = realDeps) {
6657
6664
  let renamed = 0;
6665
+ let globalOff = null;
6666
+ const manuallyNamed = async (session, w) => {
6667
+ if (w.autoRename === "1")
6668
+ return false;
6669
+ if (w.autoRename === "0") {
6670
+ if (globalOff === null)
6671
+ globalOff = await globalAutomaticRenameOff(deps.runner);
6672
+ if (!globalOff)
6673
+ return true;
6674
+ }
6675
+ return windowNamedManually(session, w.index, deps.runner);
6676
+ };
6658
6677
  for (const task of orch.listTasks()) {
6659
6678
  if (task.archived || task.kind === "main" || !task.worktreePath)
6660
6679
  continue;
@@ -6666,7 +6685,7 @@ async function runChatTabNamingPass(orch, deps = realDeps) {
6666
6685
  const vendor = task.vendor ?? DEFAULT_TASK_VENDOR;
6667
6686
  for (const w of windows) {
6668
6687
  try {
6669
- if (await windowNamedManually(session, w.index, deps.runner))
6688
+ if (await manuallyNamed(session, w))
6670
6689
  continue;
6671
6690
  const title = w.sessionId ? await deps.titleFromSessionId(vendor, w.sessionId) : w.index === originIndex ? await deps.titleFromWorktree(task.worktreePath, vendor) : "";
6672
6691
  if (title && await renameWindow(session, w.index, title, deps.runner))
@@ -7071,8 +7090,20 @@ function createDaemonHandlerRegistry() {
7071
7090
  name: "task.ensureWorktree",
7072
7091
  async handle(payload, ctx) {
7073
7092
  const taskId = requireString(payload, "taskId");
7074
- const path11 = await ctx.orch.ensureWorktree(taskId);
7075
- return { worktreePath: path11 };
7093
+ ctx.bus.publish("task.jobs", { taskId, kind: "ensureWorktree", phase: "running" });
7094
+ try {
7095
+ const path11 = await ctx.orch.ensureWorktree(taskId);
7096
+ ctx.bus.publish("task.jobs", { taskId, kind: "ensureWorktree", phase: "done" });
7097
+ return { worktreePath: path11 };
7098
+ } catch (err) {
7099
+ ctx.bus.publish("task.jobs", {
7100
+ taskId,
7101
+ kind: "ensureWorktree",
7102
+ phase: "error",
7103
+ error: err instanceof Error ? err.message : String(err)
7104
+ });
7105
+ throw err;
7106
+ }
7076
7107
  }
7077
7108
  },
7078
7109
  {
@@ -7333,8 +7364,234 @@ var init_ui_prefs_watcher = __esm(() => {
7333
7364
  FOCUS_ACCENT_SLOT_NAMES = ["primary", "success", "info"];
7334
7365
  });
7335
7366
 
7367
+ // src/lib/poll-scheduling.ts
7368
+ import { spawn as spawn2 } from "child_process";
7369
+ function computeNextAllowedAt(startedAt, finishedAt, timedOut, cfg) {
7370
+ if (timedOut)
7371
+ return startedAt + cfg.slowRetryMs;
7372
+ return finishedAt + Math.max(cfg.minIntervalMs, (finishedAt - startedAt) * 5);
7373
+ }
7374
+ function shouldPoll(state, now) {
7375
+ return !state.inFlight && now >= state.nextAllowedAt;
7376
+ }
7377
+ function maybeStartScheduledRun(state, cfg, run, onValue) {
7378
+ const startedAt = Date.now();
7379
+ if (!shouldPoll(state, startedAt))
7380
+ return false;
7381
+ state.inFlight = true;
7382
+ const controller = new AbortController;
7383
+ const timer = setTimeout(() => controller.abort(), cfg.timeoutMs);
7384
+ (async () => {
7385
+ let value;
7386
+ let ok = false;
7387
+ try {
7388
+ value = await run(controller.signal);
7389
+ ok = true;
7390
+ } catch {}
7391
+ clearTimeout(timer);
7392
+ const timedOut = controller.signal.aborted;
7393
+ state.nextAllowedAt = computeNextAllowedAt(startedAt, Date.now(), timedOut, cfg);
7394
+ state.inFlight = false;
7395
+ if (ok && !timedOut)
7396
+ onValue(value);
7397
+ })();
7398
+ return true;
7399
+ }
7400
+ function spawnCapture(cmd, args, opts) {
7401
+ return new Promise((resolve2) => {
7402
+ let out = "";
7403
+ let settled = false;
7404
+ const finish = (status) => {
7405
+ if (settled)
7406
+ return;
7407
+ settled = true;
7408
+ resolve2({ status, stdout: out });
7409
+ };
7410
+ const child = spawn2(cmd, args.slice(), {
7411
+ cwd: opts.cwd,
7412
+ stdio: ["ignore", "pipe", "ignore"],
7413
+ env: opts.env,
7414
+ signal: opts.signal,
7415
+ killSignal: "SIGKILL"
7416
+ });
7417
+ child.stdout?.on("data", (chunk) => {
7418
+ out += String(chunk);
7419
+ });
7420
+ child.on("error", () => finish(null));
7421
+ child.on("close", (code) => finish(code));
7422
+ });
7423
+ }
7424
+ var init_poll_scheduling = () => {};
7425
+
7426
+ // src/tui/panes/sidebar/worktree-changes.ts
7427
+ var exports_worktree_changes = {};
7428
+ __export(exports_worktree_changes, {
7429
+ sameWorktreeChanges: () => sameWorktreeChanges,
7430
+ readWorktreeChanges: () => readWorktreeChanges,
7431
+ pickPushedChanges: () => pickPushedChanges,
7432
+ parsePorcelain: () => parsePorcelain2
7433
+ });
7434
+ import { spawnSync as spawnSync7 } from "child_process";
7435
+ function sameWorktreeChanges(a, b) {
7436
+ return a.added === b.added && a.deleted === b.deleted;
7437
+ }
7438
+ function pickPushedChanges(pushed, worktreePath) {
7439
+ if (!pushed)
7440
+ return null;
7441
+ return pushed.get(worktreePath) ?? ZERO;
7442
+ }
7443
+ function readWorktreeChanges(worktreePath) {
7444
+ if (!worktreePath)
7445
+ return ZERO;
7446
+ try {
7447
+ const out = spawnSync7("git", ["status", "--porcelain=v1"], {
7448
+ cwd: worktreePath,
7449
+ encoding: "utf8",
7450
+ stdio: ["ignore", "pipe", "pipe"],
7451
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" }
7452
+ });
7453
+ if (out.status !== 0 || !out.stdout)
7454
+ return ZERO;
7455
+ return parsePorcelain2(out.stdout);
7456
+ } catch {
7457
+ return ZERO;
7458
+ }
7459
+ }
7460
+ function parsePorcelain2(text) {
7461
+ let added = 0;
7462
+ let deleted = 0;
7463
+ for (const line of text.split(`
7464
+ `)) {
7465
+ if (!line || line.startsWith("##"))
7466
+ continue;
7467
+ const x = line.charAt(0);
7468
+ const y = line.charAt(1);
7469
+ if (x === "D" || y === "D")
7470
+ deleted += 1;
7471
+ else
7472
+ added += 1;
7473
+ }
7474
+ return { added, deleted };
7475
+ }
7476
+ var ZERO;
7477
+ var init_worktree_changes = __esm(() => {
7478
+ ZERO = { added: 0, deleted: 0 };
7479
+ });
7480
+
7481
+ // ../kobe-daemon/src/daemon/worktree-changes-collector.ts
7482
+ async function runGitStatus(worktreePath, signal) {
7483
+ const res = await spawnCapture("git", ["status", "--porcelain=v1"], {
7484
+ cwd: worktreePath,
7485
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
7486
+ signal
7487
+ });
7488
+ if (res.status !== 0)
7489
+ throw new Error("git status failed");
7490
+ return parsePorcelain2(res.stdout);
7491
+ }
7492
+ function trackedWorktreePaths(tasks) {
7493
+ const paths = new Set;
7494
+ for (const task of tasks) {
7495
+ if (task.archived)
7496
+ continue;
7497
+ if (!task.worktreePath)
7498
+ continue;
7499
+ if (isRemoteRepoKey(task.repo) || isRemoteRepoKey(task.worktreePath))
7500
+ continue;
7501
+ paths.add(task.worktreePath);
7502
+ }
7503
+ return paths;
7504
+ }
7505
+
7506
+ class WorktreeChangesCollector {
7507
+ orch;
7508
+ bus;
7509
+ options;
7510
+ entries = new Map;
7511
+ stopped = false;
7512
+ constructor(orch, bus, options = {}) {
7513
+ this.orch = orch;
7514
+ this.bus = bus;
7515
+ this.options = options;
7516
+ }
7517
+ tick() {
7518
+ if (this.stopped)
7519
+ return;
7520
+ try {
7521
+ const tracked = trackedWorktreePaths(this.orch.listTasks());
7522
+ let pruned = false;
7523
+ for (const path11 of this.entries.keys()) {
7524
+ if (tracked.has(path11))
7525
+ continue;
7526
+ const entry = this.entries.get(path11);
7527
+ if (entry?.value)
7528
+ pruned = true;
7529
+ this.entries.delete(path11);
7530
+ }
7531
+ if (pruned)
7532
+ this.publish();
7533
+ for (const path11 of tracked)
7534
+ this.maybeCollect(path11);
7535
+ } catch (err) {
7536
+ logDaemonError("worktree-changes", err);
7537
+ }
7538
+ }
7539
+ stop() {
7540
+ this.stopped = true;
7541
+ }
7542
+ maybeCollect(worktreePath) {
7543
+ let entry = this.entries.get(worktreePath);
7544
+ if (!entry) {
7545
+ entry = { inFlight: false, nextAllowedAt: 0 };
7546
+ this.entries.set(worktreePath, entry);
7547
+ }
7548
+ const cadence = this.options.cadence ?? {
7549
+ timeoutMs: WORKTREE_CHANGES_TIMEOUT_MS,
7550
+ slowRetryMs: WORKTREE_CHANGES_SLOW_RETRY_MS,
7551
+ minIntervalMs: WORKTREE_CHANGES_MIN_INTERVAL_MS
7552
+ };
7553
+ const run = this.options.run ?? runGitStatus;
7554
+ maybeStartScheduledRun(entry, cadence, (signal) => run(worktreePath, signal), (value) => {
7555
+ if (this.stopped)
7556
+ return;
7557
+ if (this.entries.get(worktreePath) !== entry)
7558
+ return;
7559
+ if (entry.value && sameWorktreeChanges(entry.value, value))
7560
+ return;
7561
+ entry.value = value;
7562
+ this.publish();
7563
+ });
7564
+ }
7565
+ publish() {
7566
+ const changes = {};
7567
+ for (const [path11, entry] of this.entries) {
7568
+ if (entry.value)
7569
+ changes[path11] = entry.value;
7570
+ }
7571
+ this.bus.publish("worktree.changes", { changes });
7572
+ }
7573
+ }
7574
+ function startWorktreeChangesCollector(orch, bus, tickMs = DEFAULT_WORKTREE_CHANGES_TICK_MS) {
7575
+ if (tickMs <= 0)
7576
+ return () => {};
7577
+ const collector = new WorktreeChangesCollector(orch, bus);
7578
+ collector.tick();
7579
+ const timer = setInterval(() => collector.tick(), tickMs);
7580
+ timer.unref?.();
7581
+ return () => {
7582
+ clearInterval(timer);
7583
+ collector.stop();
7584
+ };
7585
+ }
7586
+ var DEFAULT_WORKTREE_CHANGES_TICK_MS = 2000, WORKTREE_CHANGES_TIMEOUT_MS = 4000, WORKTREE_CHANGES_SLOW_RETRY_MS = 60000, WORKTREE_CHANGES_MIN_INTERVAL_MS = 1500;
7587
+ var init_worktree_changes_collector = __esm(() => {
7588
+ init_poll_scheduling();
7589
+ init_repos();
7590
+ init_worktree_changes();
7591
+ });
7592
+
7336
7593
  // ../kobe-daemon/src/daemon/server.ts
7337
- import { mkdir as mkdir6, readFile as readFile8, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
7594
+ import { mkdir as mkdir6, readFile as readFile7, unlink as unlink4, writeFile as writeFile4 } from "fs/promises";
7338
7595
  import { createServer } from "net";
7339
7596
  import { dirname as dirname7 } from "path";
7340
7597
  function resolveIdleGraceMs() {
@@ -7436,6 +7693,7 @@ async function startDaemonServer(orch, options = {}) {
7436
7693
  path: defaultKeybindingsPath(options.homeDir),
7437
7694
  debounceMs: options.keybindingsDebounceMs ?? DEFAULT_KEYBINDINGS_DEBOUNCE_MS
7438
7695
  });
7696
+ const stopWorktreeChangesCollector = startWorktreeChangesCollector(orch, bus, options.worktreeChangesTickMs ?? DEFAULT_WORKTREE_CHANGES_TICK_MS);
7439
7697
  const serverApi = {
7440
7698
  socketPath,
7441
7699
  pidPath,
@@ -7450,6 +7708,7 @@ async function startDaemonServer(orch, options = {}) {
7450
7708
  stopAutoTitlePoller();
7451
7709
  stopUiPrefsWatcher();
7452
7710
  stopKeybindingsWatcher();
7711
+ stopWorktreeChangesCollector();
7453
7712
  activity.close();
7454
7713
  broadcast(clients, { type: "event", name: "daemon.stopping", payload: {} });
7455
7714
  for (const client of Array.from(clients)) {
@@ -7546,7 +7805,7 @@ async function startDaemonServer(orch, options = {}) {
7546
7805
  }
7547
7806
  async function readPidFile(pidPath) {
7548
7807
  try {
7549
- const raw = await readFile8(pidPath, "utf8");
7808
+ const raw = await readFile7(pidPath, "utf8");
7550
7809
  const pid = Number(raw.trim());
7551
7810
  return Number.isFinite(pid) ? pid : null;
7552
7811
  } catch {
@@ -7557,10 +7816,12 @@ function writeFrame(client, frame) {
7557
7816
  client.socket.write(frameToLine(frame));
7558
7817
  }
7559
7818
  function broadcast(clients, frame) {
7819
+ let line = null;
7560
7820
  for (const client of clients) {
7561
7821
  if (!client.subscribed && frame.type === "event")
7562
7822
  continue;
7563
- writeFrame(client, frame);
7823
+ line ??= frameToLine(frame);
7824
+ client.socket.write(line);
7564
7825
  }
7565
7826
  }
7566
7827
  var DEFAULT_UPDATE_POLL_MS, DEFAULT_IDLE_GRACE_MS = 3000;
@@ -7573,6 +7834,7 @@ var init_server = __esm(() => {
7573
7834
  init_paths2();
7574
7835
  init_protocol();
7575
7836
  init_ui_prefs_watcher();
7837
+ init_worktree_changes_collector();
7576
7838
  init_handlers();
7577
7839
  DEFAULT_UPDATE_POLL_MS = 6 * 60 * 60 * 1000;
7578
7840
  });
@@ -7641,7 +7903,7 @@ __export(exports_daemon_process, {
7641
7903
  connectOrStartDaemon: () => connectOrStartDaemon,
7642
7904
  connectIfRunning: () => connectIfRunning
7643
7905
  });
7644
- import { spawn as spawn2 } from "child_process";
7906
+ import { spawn as spawn3 } from "child_process";
7645
7907
  import { closeSync, existsSync as existsSync5, mkdirSync as mkdirSync4, openSync } from "fs";
7646
7908
  import { dirname as dirname8, resolve as resolve2 } from "path";
7647
7909
  import { fileURLToPath as fileURLToPath2 } from "url";
@@ -7655,7 +7917,7 @@ function spawnDetachedDaemon(command, args, env, logPath) {
7655
7917
  } catch {
7656
7918
  stdio = "ignore";
7657
7919
  }
7658
- const child = spawn2(command, [...args], { detached: true, stdio, env });
7920
+ const child = spawn3(command, [...args], { detached: true, stdio, env });
7659
7921
  child.unref();
7660
7922
  if (logFd !== undefined) {
7661
7923
  try {
@@ -7938,7 +8200,7 @@ var init_interactive_command = __esm(() => {
7938
8200
  });
7939
8201
 
7940
8202
  // src/lib/feedback.ts
7941
- import { spawnSync as spawnSync7 } from "child_process";
8203
+ import { spawnSync as spawnSync8 } from "child_process";
7942
8204
  function parseRepoSlug(slug) {
7943
8205
  const [owner, name] = slug.split("/");
7944
8206
  if (!owner || !name)
@@ -7993,7 +8255,7 @@ function submitFeedback(input, deps = {}) {
7993
8255
  throw new Error("package repository is not a GitHub repository");
7994
8256
  const { owner, name } = parseRepoSlug(slug);
7995
8257
  const categorySlug = input.categorySlug?.trim() || DEFAULT_FEEDBACK_CATEGORY_SLUG;
7996
- const io = { spawn: deps.spawn ?? spawnSync7 };
8258
+ const io = { spawn: deps.spawn ?? spawnSync8 };
7997
8259
  const categoryData = runGhGraphql(DISCUSSION_CATEGORY_QUERY, { owner, name }, io);
7998
8260
  const repository = categoryData.repository;
7999
8261
  const repositoryId = repository?.id;
@@ -8132,6 +8394,13 @@ var init_keybindings_file = __esm(() => {
8132
8394
  });
8133
8395
 
8134
8396
  // src/tui/lib/keymap-overrides.ts
8397
+ function pairContract(first, second) {
8398
+ const layout = `alternating [${first}, ${second}] pairs`;
8399
+ return {
8400
+ layout,
8401
+ validateCount: (count) => count >= 2 && count % 2 === 0 ? null : `needs ${layout} (an even number of chords \u2014 got ${count})`
8402
+ };
8403
+ }
8135
8404
  function normalizeChord(raw, opts) {
8136
8405
  const trimmed = raw.trim().toLowerCase();
8137
8406
  if (!trimmed)
@@ -8271,6 +8540,14 @@ function applyKeymapOverrides(keymap, entries) {
8271
8540
  warnings.push(`${entry.id}: not customizable \u2014 the key is handled outside the keymap (doc-only row)`);
8272
8541
  continue;
8273
8542
  }
8543
+ const contract = SLOT_CONTRACTS[entry.id];
8544
+ if (contract && entry.keys.length > 0) {
8545
+ const problem = contract.validateCount(entry.keys.length);
8546
+ if (problem) {
8547
+ warnings.push(`${entry.id}: ${problem} \u2014 keeping the default`);
8548
+ continue;
8549
+ }
8550
+ }
8274
8551
  const keys = entry.keys.filter((chord) => {
8275
8552
  if (chord.length === 1 && NO_BARE_LETTER_SCOPES.has(row.scope)) {
8276
8553
  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 +8559,10 @@ function applyKeymapOverrides(keymap, entries) {
8282
8559
  warnings.push(`${entry.id}: no chords survived validation \u2014 keeping the default`);
8283
8560
  continue;
8284
8561
  }
8562
+ if (contract && keys.length !== entry.keys.length) {
8563
+ warnings.push(`${entry.id}: a dropped chord would shift the slot layout (${contract.layout}) \u2014 keeping the default`);
8564
+ continue;
8565
+ }
8285
8566
  const defaultKeys = row.keys;
8286
8567
  const mutable = row;
8287
8568
  mutable.keys = keys;
@@ -8312,21 +8593,23 @@ function applyKeymapOverrides(keymap, entries) {
8312
8593
  }
8313
8594
  return { applied, warnings };
8314
8595
  }
8315
- var FIXED_BINDING_IDS, NO_BARE_LETTER_SCOPES, MOD_ALIASES, KEY_ALIASES, KNOWN_NAMED_KEYS, MOD_ORDER;
8596
+ var FIXED_BINDING_IDS, SLOT_CONTRACTS, NO_BARE_LETTER_SCOPES, MOD_ALIASES, KEY_ALIASES, KNOWN_NAMED_KEYS, MOD_ORDER;
8316
8597
  var init_keymap_overrides = __esm(() => {
8317
8598
  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"
8599
+ "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",
8600
+ "sidebar.goto": "gg vs Shift+G is discriminated via evt.shift; shift+<letter> chords are inexpressible, so a rebind can't carry both halves",
8601
+ "sidebar.pin": "fires on Shift+P via evt.shift; shift+<letter> chords are inexpressible, so a rebind can't work",
8602
+ "sidebar.localMerge": "fires on Shift+M via evt.shift; shift+<letter> chords are inexpressible, so a rebind can't work",
8603
+ "chat.question.nav": "the question picker has no live registration site (display-only row) \u2014 rebinding would change Help without changing behavior",
8604
+ "chat.question.pick-number": "digits map to options positionally and the question picker has no live registration site (display-only row)"
8605
+ };
8606
+ SLOT_CONTRACTS = {
8607
+ "sidebar.nav": pairContract("down", "up"),
8608
+ "files.nav": pairContract("down", "up"),
8609
+ "sidebar.search.nav": pairContract("down", "up"),
8610
+ "files.hierarchy": pairContract("collapse", "expand"),
8611
+ "sidebar.view": pairContract("previous view", "next view"),
8612
+ "files.tab": pairContract("previous tab", "next tab")
8330
8613
  };
8331
8614
  NO_BARE_LETTER_SCOPES = new Set(["global", "workspace", "terminal"]);
8332
8615
  MOD_ALIASES = {
@@ -10183,10 +10466,17 @@ function parseKobePaneRows(stdout) {
10183
10466
  const line = raw.trim();
10184
10467
  if (!line)
10185
10468
  continue;
10186
- const [windowId, paneId, role, version] = line.split("\t");
10469
+ const [windowId, paneId, role, version, paneWidth] = line.split("\t");
10187
10470
  if (!windowId || !paneId || !role)
10188
10471
  continue;
10189
- rows.push({ windowId: windowId.trim(), paneId: paneId.trim(), role: role.trim(), version: version?.trim() ?? "" });
10472
+ const width = Number.parseInt(paneWidth?.trim() ?? "", 10);
10473
+ rows.push({
10474
+ windowId: windowId.trim(),
10475
+ paneId: paneId.trim(),
10476
+ role: role.trim(),
10477
+ version: version?.trim() ?? "",
10478
+ ...Number.isFinite(width) ? { paneWidth: width } : {}
10479
+ });
10190
10480
  }
10191
10481
  return rows;
10192
10482
  }
@@ -10251,38 +10541,18 @@ async function relaunchEngineInAllWindows(session, cwd, command, remoteKey) {
10251
10541
  if (enginePanes.length === 0)
10252
10542
  return false;
10253
10543
  const cmd = keepAlive(wrapEngineLaunch(shellQuoteArgv(command), remoteKey, cwd));
10254
- for (const pane of enginePanes) {
10255
- await runTmux(["respawn-pane", "-k", "-c", localSpawnCwd(cwd), "-t", pane, cmd]);
10256
- }
10544
+ await runTmuxSequence(enginePanes.map((pane) => ["respawn-pane", "-k", "-c", localSpawnCwd(cwd), "-t", pane, cmd]));
10257
10545
  return true;
10258
10546
  }
10259
- async function healTaskPaneWidths(session) {
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)
10274
- return;
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
- };
10547
+ async function globalLayoutPrefs() {
10548
+ const opts = await getServerOptions([TASKS_WIDTH_OPTION, RIGHT_COLUMN_WIDTH_OPTION, OPS_HEIGHT_OPTION]);
10549
+ const rawWidth = Number.parseInt(opts[TASKS_WIDTH_OPTION] ?? "", 10);
10550
+ const tasksWidth = Number.isFinite(rawWidth) && rawWidth > 0 ? clampTasksPaneWidth(rawWidth) : TASKS_PANE_WIDTH;
10551
+ const rcArgs = rightColumnResizeArgs({
10552
+ widthPct: clampPanePercent(Number.parseInt(opts[RIGHT_COLUMN_WIDTH_OPTION] ?? "", 10)),
10553
+ heightPct: clampPanePercent(Number.parseInt(opts[OPS_HEIGHT_OPTION] ?? "", 10))
10554
+ });
10555
+ return { tasksWidth, rcArgs };
10286
10556
  }
10287
10557
  function rightColumnResizeArgs(geom) {
10288
10558
  const args = [];
@@ -10293,33 +10563,35 @@ function rightColumnResizeArgs(geom) {
10293
10563
  return args;
10294
10564
  }
10295
10565
  async function globalRightColumnResizeArgs() {
10296
- return rightColumnResizeArgs(await rightColumnPercents());
10566
+ return (await globalLayoutPrefs()).rcArgs;
10297
10567
  }
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)
10568
+ async function healWorkspaceLayout(session, versions) {
10569
+ const { tasksWidth, rcArgs } = await globalLayoutPrefs();
10570
+ const rows = await listKobePanes(session);
10571
+ if (!rows)
10315
10572
  return;
10316
- await runTmuxSequence(opsPanes.map((pane) => ["resize-pane", "-t", pane, ...args]));
10573
+ const commands = [];
10574
+ for (const row of rows) {
10575
+ if (row.role === "tasks" && row.paneWidth !== tasksWidth) {
10576
+ commands.push(["resize-pane", "-t", row.paneId, "-x", `${tasksWidth}`]);
10577
+ }
10578
+ }
10579
+ if (rcArgs.length > 0) {
10580
+ for (const row of rows) {
10581
+ if (row.role === "ops")
10582
+ commands.push(["resize-pane", "-t", row.paneId, ...rcArgs]);
10583
+ }
10584
+ }
10585
+ if (versions) {
10586
+ commands.push(...respawnCommandsFor(planPaneHeals(rows, { currentVersion: CURRENT_VERSION, force: false }), versions));
10587
+ }
10588
+ if (commands.length > 0)
10589
+ await runTmuxSequence(commands);
10317
10590
  }
10318
10591
  async function healSessionLayout(session) {
10319
10592
  if (!await sessionExists(session))
10320
10593
  return;
10321
- await healTaskPaneWidths(session);
10322
- await healRightColumn(session);
10594
+ await healWorkspaceLayout(session);
10323
10595
  }
10324
10596
  async function captureGlobalLayout(session) {
10325
10597
  const { code, stdout } = await runTmuxCapturing([
@@ -10358,18 +10630,6 @@ async function captureGlobalLayout(session) {
10358
10630
  if (sets.length > 0)
10359
10631
  await runTmuxSequence(sets);
10360
10632
  }
10361
- async function healKobePaneVersions(session, cwd, taskId, vendor) {
10362
- const rows = await listKobePanes(session);
10363
- if (!rows)
10364
- return;
10365
- const commands = respawnCommandsFor(planPaneHeals(rows, { currentVersion: CURRENT_VERSION, force: false }), {
10366
- cwd,
10367
- taskId,
10368
- vendor
10369
- });
10370
- if (commands.length > 0)
10371
- await runTmuxSequence(commands);
10372
- }
10373
10633
  async function refreshKobeWorkspacePanes(session) {
10374
10634
  const sessionOptions = await getSessionOptions(session, ["@kobe_worktree", "@kobe_task", "@kobe_vendor"]);
10375
10635
  const cwd = sessionOptions["@kobe_worktree"] || process.cwd();
@@ -10395,7 +10655,7 @@ var init_pane_heal = __esm(() => {
10395
10655
  init_tmux_border_theme();
10396
10656
  init_version();
10397
10657
  init_launch();
10398
- KOBE_PANE_LIST_FORMAT = `#{window_id} #{pane_id} #{@kobe_role} #{${PANE_VERSION_OPTION}}`;
10658
+ KOBE_PANE_LIST_FORMAT = `#{window_id} #{pane_id} #{@kobe_role} #{${PANE_VERSION_OPTION}} #{pane_width}`;
10399
10659
  });
10400
10660
 
10401
10661
  // src/tui/panes/terminal/chattab.ts
@@ -10625,6 +10885,7 @@ __export(exports_tmux, {
10625
10885
  refreshKobeWorkspacePanes: () => refreshKobeWorkspacePanes,
10626
10886
  quickCreate: () => quickCreate,
10627
10887
  prepareWindowForAttach: () => prepareWindowForAttach,
10888
+ parseObservedSession: () => parseObservedSession,
10628
10889
  openUpdateTab: () => openUpdateTab,
10629
10890
  openSettingsTab: () => openSettingsTab,
10630
10891
  openNewTaskTab: () => openNewTaskTab,
@@ -10633,6 +10894,7 @@ __export(exports_tmux, {
10633
10894
  kobeStatusRight: () => kobeStatusRight,
10634
10895
  killSession: () => killSession,
10635
10896
  healSessionLayout: () => healSessionLayout,
10897
+ focusBindCommand: () => focusBindCommand,
10636
10898
  ensureSession: () => ensureSession,
10637
10899
  currentSessionName: () => currentSessionName,
10638
10900
  chatTabSwitchBindings: () => chatTabSwitchBindings,
@@ -10660,8 +10922,18 @@ async function prepareWindowForAttach(session) {
10660
10922
  const sizeArgs = tmuxInitialSizeArgs();
10661
10923
  if (sizeArgs.length > 0)
10662
10924
  await runTmux(["resize-window", "-t", `=${session}`, ...sizeArgs]);
10663
- await healTaskPaneWidths(session);
10664
- await healRightColumn(session);
10925
+ await healWorkspaceLayout(session);
10926
+ }
10927
+ function focusBindCommand(key, dir) {
10928
+ return [
10929
+ "bind-key",
10930
+ "-n",
10931
+ key,
10932
+ "if-shell",
10933
+ "-F",
10934
+ `#{?window_zoomed_flag,1,#{?${FOCUS_EDGE_VARS[dir]},,1}}`,
10935
+ `select-pane ${dir}`
10936
+ ];
10665
10937
  }
10666
10938
  async function ensureSession(opts) {
10667
10939
  const inflight = ensureSessionLocks.get(opts.name);
@@ -10675,16 +10947,33 @@ async function ensureSession(opts) {
10675
10947
  ensureSessionLocks.delete(opts.name);
10676
10948
  }
10677
10949
  }
10950
+ function parseObservedSession(stdout) {
10951
+ let worktree = "";
10952
+ let vendor = "";
10953
+ let claudePaneAlive = false;
10954
+ const windows = new Set;
10955
+ for (const line of stdout.split(`
10956
+ `)) {
10957
+ const [windowId, active, role, wt, vd] = line.split("\t");
10958
+ if (!windowId?.trim())
10959
+ continue;
10960
+ windows.add(windowId.trim());
10961
+ if (!worktree && wt?.trim())
10962
+ worktree = wt.trim();
10963
+ if (!vendor && vd?.trim())
10964
+ vendor = vd.trim();
10965
+ if (active?.trim() === "1" && role?.trim() === "claude")
10966
+ claudePaneAlive = true;
10967
+ }
10968
+ return { worktree, vendor, claudePaneAlive, windowCount: windows.size };
10969
+ }
10678
10970
  async function observeSession(name) {
10679
10971
  if (!await sessionExists(name))
10680
10972
  return null;
10681
- const sessionOptions = await getSessionOptions(name, ["@kobe_worktree", "@kobe_vendor"]);
10682
- return {
10683
- worktree: sessionOptions["@kobe_worktree"] ?? "",
10684
- vendor: sessionOptions["@kobe_vendor"] ?? "",
10685
- claudePaneAlive: await claudePaneIdStrict(name) !== "",
10686
- windowCount: await windowCount(name)
10687
- };
10973
+ const { code, stdout } = await runTmuxCapturing(["list-panes", "-s", "-t", `=${name}`, "-F", OBSERVE_SESSION_FORMAT]);
10974
+ if (code !== 0)
10975
+ return { worktree: "", vendor: "", claudePaneAlive: false, windowCount: 0 };
10976
+ return parseObservedSession(stdout);
10688
10977
  }
10689
10978
  async function ensureSessionImpl(opts) {
10690
10979
  const observed = await observeSession(opts.name);
@@ -10695,18 +10984,14 @@ async function ensureSessionImpl(opts) {
10695
10984
  });
10696
10985
  const remoteKey = remoteKeyForRepo(opts.repo);
10697
10986
  if (action.kind === "reuse") {
10698
- await healTaskPaneWidths(opts.name);
10699
- await healRightColumn(opts.name);
10700
- await healKobePaneVersions(opts.name, opts.cwd, opts.taskId, opts.vendor);
10987
+ await healWorkspaceLayout(opts.name, { cwd: opts.cwd, taskId: opts.taskId, vendor: opts.vendor });
10701
10988
  return true;
10702
10989
  }
10703
10990
  if (action.kind === "respawn-engine") {
10704
10991
  if (await relaunchEngineInAllWindows(opts.name, opts.cwd, opts.command, remoteKey)) {
10705
10992
  if (opts.vendor)
10706
10993
  await setSessionOption(opts.name, "@kobe_vendor", opts.vendor);
10707
- await healTaskPaneWidths(opts.name);
10708
- await healRightColumn(opts.name);
10709
- await healKobePaneVersions(opts.name, opts.cwd, opts.taskId, opts.vendor);
10994
+ await healWorkspaceLayout(opts.name, { cwd: opts.cwd, taskId: opts.taskId, vendor: opts.vendor });
10710
10995
  return true;
10711
10996
  }
10712
10997
  }
@@ -10781,7 +11066,7 @@ async function ensureSessionImpl(opts) {
10781
11066
  const focusDirections = ["-L", "-D", "-U", "-R"];
10782
11067
  const focusBinds = userKeys.focus.flatMap((bind, i) => {
10783
11068
  const dir = focusDirections[i];
10784
- return bind && dir ? [["bind-key", "-n", bind.key, "select-pane", dir]] : [];
11069
+ return bind && dir ? [focusBindCommand(bind.key, dir)] : [];
10785
11070
  });
10786
11071
  const b = userKeys.binds;
10787
11072
  await runTmuxSequence([
@@ -10841,7 +11126,7 @@ async function selectTasksPane(session) {
10841
11126
  await runTmux(["select-pane", "-t", tasksPane]);
10842
11127
  return tasksPane;
10843
11128
  }
10844
- var ensureSessionLocks;
11129
+ var FOCUS_EDGE_VARS, ensureSessionLocks, OBSERVE_SESSION_FORMAT = "#{window_id}\t#{window_active}\t#{@kobe_role}\t#{@kobe_worktree}\t#{@kobe_vendor}";
10845
11130
  var init_tmux = __esm(() => {
10846
11131
  init_invocation();
10847
11132
  init_interactive_command();
@@ -10857,6 +11142,12 @@ var init_tmux = __esm(() => {
10857
11142
  init_client2();
10858
11143
  init_chattab();
10859
11144
  init_pane_heal();
11145
+ FOCUS_EDGE_VARS = {
11146
+ "-L": "pane_at_left",
11147
+ "-D": "pane_at_bottom",
11148
+ "-U": "pane_at_top",
11149
+ "-R": "pane_at_right"
11150
+ };
10860
11151
  ensureSessionLocks = new Map;
10861
11152
  });
10862
11153
 
@@ -10897,51 +11188,6 @@ var init_repo_init = __esm(() => {
10897
11188
  INIT_PROMPT_REL = join7(".kobe", "init-prompt.md");
10898
11189
  });
10899
11190
 
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
11191
  // src/cli/api-cmd.ts
10946
11192
  var exports_api_cmd = {};
10947
11193
  __export(exports_api_cmd, {
@@ -12913,7 +13159,7 @@ async function handleDiffRequest(req, url) {
12913
13159
  var GIT_TIMEOUT_MS = 15000;
12914
13160
 
12915
13161
  // src/web/notes.ts
12916
- import { mkdir as mkdir7, readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
13162
+ import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
12917
13163
  import { join as join11 } from "path";
12918
13164
  function notesDir() {
12919
13165
  return join11(kobeStateDir(), "notes");
@@ -12932,7 +13178,7 @@ async function handleGet(url) {
12932
13178
  try {
12933
13179
  let markdown = "";
12934
13180
  try {
12935
- markdown = await readFile9(noteFilePath(taskId), "utf8");
13181
+ markdown = await readFile8(noteFilePath(taskId), "utf8");
12936
13182
  } catch (err) {
12937
13183
  if (err.code !== "ENOENT")
12938
13184
  throw err;
@@ -13035,16 +13281,21 @@ class DaemonLink {
13035
13281
  const socketPath = allowSpawn ? await ensureDaemonReachable() : defaultDaemonSocketPath();
13036
13282
  const client = new KobeDaemonClient(socketPath);
13037
13283
  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" });
13284
+ try {
13285
+ const hello = await client.request("hello", {
13286
+ protocolVersion: DAEMON_PROTOCOL_VERSION,
13287
+ minProtocolVersion: MIN_COMPATIBLE_PROTOCOL_VERSION
13288
+ });
13289
+ if (hello.tasks)
13290
+ this.tasks = hello.tasks;
13291
+ this.engineStates = {};
13292
+ client.on("*", (frame) => this.onFrame(frame.name, frame.payload));
13293
+ client.onLifecycle("close", () => this.onDrop(client));
13294
+ await client.subscribe({ role: "gui" });
13295
+ } catch (err) {
13296
+ client.close();
13297
+ throw err;
13298
+ }
13048
13299
  this.client = client;
13049
13300
  this.setConnected(true);
13050
13301
  }
@@ -15101,6 +15352,30 @@ var init_solid = __esm(() => {
15101
15352
  });
15102
15353
 
15103
15354
  // src/client/remote-orchestrator.ts
15355
+ function parseWorktreeChangesPayload(payload) {
15356
+ const changes = payload?.changes;
15357
+ if (!changes || typeof changes !== "object" || Array.isArray(changes))
15358
+ return null;
15359
+ const map = new Map;
15360
+ for (const [path11, value] of Object.entries(changes)) {
15361
+ const counts = value;
15362
+ if (typeof counts?.added !== "number" || typeof counts.deleted !== "number")
15363
+ return null;
15364
+ map.set(path11, { added: counts.added, deleted: counts.deleted });
15365
+ }
15366
+ return map;
15367
+ }
15368
+ function sameWorktreeChangesMap(a, b) {
15369
+ if (a.size !== b.size)
15370
+ return false;
15371
+ for (const [path11, counts] of a) {
15372
+ const other = b.get(path11);
15373
+ if (!other || !sameWorktreeChanges(counts, other))
15374
+ return false;
15375
+ }
15376
+ return true;
15377
+ }
15378
+
15104
15379
  class RemoteOrchestrator {
15105
15380
  client;
15106
15381
  tasksAcc;
@@ -15113,6 +15388,10 @@ class RemoteOrchestrator {
15113
15388
  setDaemonVersionSig;
15114
15389
  engineStateAcc;
15115
15390
  setEngineStateSig;
15391
+ taskJobsAcc;
15392
+ setTaskJobsSig;
15393
+ worktreeChangesAcc;
15394
+ setWorktreeChangesSig;
15116
15395
  uiPrefsAcc;
15117
15396
  setUiPrefsSig;
15118
15397
  keybindingsRevAcc;
@@ -15129,6 +15408,8 @@ class RemoteOrchestrator {
15129
15408
  const [update, setUpdate] = createSignal(null);
15130
15409
  const [daemonVersion, setDaemonVersion] = createSignal(null);
15131
15410
  const [engineState, setEngineState] = createSignal(new Map);
15411
+ const [taskJobs, setTaskJobs] = createSignal(new Map);
15412
+ const [worktreeChanges, setWorktreeChanges] = createSignal(null);
15132
15413
  const [uiPrefs, setUiPrefs] = createSignal(null);
15133
15414
  const [keybindingsRev, setKeybindingsRev] = createSignal(null);
15134
15415
  const [connectionState, setConnectionState] = createSignal("online");
@@ -15142,6 +15423,10 @@ class RemoteOrchestrator {
15142
15423
  this.setDaemonVersionSig = (next) => setDaemonVersion(() => next);
15143
15424
  this.engineStateAcc = engineState;
15144
15425
  this.setEngineStateSig = (next) => setEngineState(() => next);
15426
+ this.taskJobsAcc = taskJobs;
15427
+ this.setTaskJobsSig = (next) => setTaskJobs(() => next);
15428
+ this.worktreeChangesAcc = worktreeChanges;
15429
+ this.setWorktreeChangesSig = (next) => setWorktreeChanges(() => next);
15145
15430
  this.uiPrefsAcc = uiPrefs;
15146
15431
  this.setUiPrefsSig = (next) => setUiPrefs(() => next);
15147
15432
  this.keybindingsRevAcc = keybindingsRev;
@@ -15202,6 +15487,12 @@ class RemoteOrchestrator {
15202
15487
  if (hello.tasks)
15203
15488
  this.setTasks(hello.tasks.map(deserializeTask));
15204
15489
  await this.client.subscribe({ role: this.role });
15490
+ if (hello.capabilities?.includes("worktree.changes")) {
15491
+ if (this.worktreeChangesAcc() === null)
15492
+ this.setWorktreeChangesSig(new Map);
15493
+ } else {
15494
+ this.setWorktreeChangesSig(null);
15495
+ }
15205
15496
  this.setConnectionState("online");
15206
15497
  logClient("orch", `subscribed as ${this.role} (${this.tasksAcc().length} tasks)`);
15207
15498
  }
@@ -15234,6 +15525,12 @@ class RemoteOrchestrator {
15234
15525
  engineStateSignal() {
15235
15526
  return this.engineStateAcc;
15236
15527
  }
15528
+ taskJobsSignal() {
15529
+ return this.taskJobsAcc;
15530
+ }
15531
+ worktreeChangesSignal() {
15532
+ return this.worktreeChangesAcc;
15533
+ }
15237
15534
  uiPrefsSignal() {
15238
15535
  return this.uiPrefsAcc;
15239
15536
  }
@@ -15319,8 +15616,11 @@ class RemoteOrchestrator {
15319
15616
  handleEvent(name, payload) {
15320
15617
  if (name === "task.snapshot") {
15321
15618
  const value = payload?.tasks;
15322
- if (Array.isArray(value))
15619
+ if (Array.isArray(value)) {
15323
15620
  this.setTasks(value.map(deserializeTask));
15621
+ this.pruneEngineState(value);
15622
+ this.pruneTaskJobs(value);
15623
+ }
15324
15624
  return;
15325
15625
  }
15326
15626
  if (name === "active-task") {
@@ -15345,6 +15645,34 @@ class RemoteOrchestrator {
15345
15645
  this.setEngineStateSig(next);
15346
15646
  return;
15347
15647
  }
15648
+ if (name === "task.jobs") {
15649
+ const p = payload;
15650
+ if (typeof p?.taskId !== "string" || p.kind !== "ensureWorktree")
15651
+ return;
15652
+ const current = this.taskJobsAcc();
15653
+ if (p.phase === "running") {
15654
+ const next = new Map(current);
15655
+ next.set(p.taskId, { kind: p.kind });
15656
+ this.setTaskJobsSig(next);
15657
+ return;
15658
+ }
15659
+ if ((p.phase === "done" || p.phase === "error") && current.has(p.taskId)) {
15660
+ const next = new Map(current);
15661
+ next.delete(p.taskId);
15662
+ this.setTaskJobsSig(next);
15663
+ }
15664
+ return;
15665
+ }
15666
+ if (name === "worktree.changes") {
15667
+ const next = parseWorktreeChangesPayload(payload);
15668
+ if (!next)
15669
+ return;
15670
+ const current = this.worktreeChangesAcc();
15671
+ if (current && sameWorktreeChangesMap(current, next))
15672
+ return;
15673
+ this.setWorktreeChangesSig(next);
15674
+ return;
15675
+ }
15348
15676
  if (name === "ui-prefs") {
15349
15677
  const p = payload;
15350
15678
  if (typeof p?.theme !== "string")
@@ -15366,6 +15694,38 @@ class RemoteOrchestrator {
15366
15694
  return;
15367
15695
  }
15368
15696
  }
15697
+ pruneEngineState(tasks) {
15698
+ const current = this.engineStateAcc();
15699
+ if (current.size === 0)
15700
+ return;
15701
+ const live = new Set(tasks.map((t) => t.id));
15702
+ let next = null;
15703
+ for (const key of current.keys()) {
15704
+ if (live.has(key))
15705
+ continue;
15706
+ if (!next)
15707
+ next = new Map(current);
15708
+ next.delete(key);
15709
+ }
15710
+ if (next)
15711
+ this.setEngineStateSig(next);
15712
+ }
15713
+ pruneTaskJobs(tasks) {
15714
+ const current = this.taskJobsAcc();
15715
+ if (current.size === 0)
15716
+ return;
15717
+ const live = new Set(tasks.map((t) => t.id));
15718
+ let next = null;
15719
+ for (const key of current.keys()) {
15720
+ if (live.has(key))
15721
+ continue;
15722
+ if (!next)
15723
+ next = new Map(current);
15724
+ next.delete(key);
15725
+ }
15726
+ if (next)
15727
+ this.setTaskJobsSig(next);
15728
+ }
15369
15729
  }
15370
15730
  function deserializeTask(s) {
15371
15731
  return {
@@ -15389,6 +15749,7 @@ var init_remote_orchestrator = __esm(() => {
15389
15749
  init_daemon_process();
15390
15750
  init_protocol();
15391
15751
  init_dev();
15752
+ init_worktree_changes();
15392
15753
  init_version();
15393
15754
  });
15394
15755
 
@@ -15917,7 +16278,7 @@ function dispatchKeyEvent(bindingStack, evt) {
15917
16278
  continue;
15918
16279
  const hit = cfg.bindings.find((b) => candidates.includes(b.key));
15919
16280
  if (hit) {
15920
- hit.cmd(evt);
16281
+ hit.cmd(evt, hit.slot);
15921
16282
  evt.preventDefault();
15922
16283
  return true;
15923
16284
  }
@@ -16328,7 +16689,7 @@ function joinDrill(typedValue, baseExpanded, name) {
16328
16689
  var init_path_helpers = () => {};
16329
16690
 
16330
16691
  // src/tui/component/new-task-dialog/clone.ts
16331
- import { spawn as spawn3 } from "child_process";
16692
+ import { spawn as spawn4 } from "child_process";
16332
16693
  import * as fs5 from "fs";
16333
16694
  import * as path11 from "path";
16334
16695
  function deriveFolderName(url) {
@@ -16407,7 +16768,7 @@ function cloneRepo(url, target, onProgress) {
16407
16768
  return new Promise((resolve8) => {
16408
16769
  let stderrBuf = "";
16409
16770
  try {
16410
- const child = spawn3("git", ["clone", "--progress", url, target], {
16771
+ const child = spawn4("git", ["clone", "--progress", url, target], {
16411
16772
  stdio: ["ignore", "ignore", "pipe"]
16412
16773
  });
16413
16774
  child.stderr?.setEncoding("utf-8");
@@ -17917,7 +18278,7 @@ function bumpKeymapVersion() {
17917
18278
  setKeymapVersion((v) => v + 1);
17918
18279
  }
17919
18280
  function findBinding(id) {
17920
- return KobeKeymap.find((b) => b.id === id);
18281
+ return KEYMAP_BY_ID.get(id);
17921
18282
  }
17922
18283
  function chordsOf(id) {
17923
18284
  return findBinding(id)?.keys ?? [];
@@ -17933,12 +18294,11 @@ function bindByIds(handlers) {
17933
18294
  console.warn(`[kobe/keybindings] bindByIds: id="${id}" has no chords (or doesn't exist in KobeKeymap)`);
17934
18295
  continue;
17935
18296
  }
17936
- for (const c of chords)
17937
- out.push({ key: c, cmd });
18297
+ chords.forEach((c, slot) => out.push({ key: c, cmd, slot }));
17938
18298
  }
17939
18299
  return out;
17940
18300
  }
17941
- var KobeKeymap, KEYMAP_DEFAULTS, keymapVersion, setKeymapVersion;
18301
+ var KobeKeymap, KEYMAP_DEFAULTS, keymapVersion, setKeymapVersion, KEYMAP_BY_ID;
17942
18302
  var init_keybindings2 = __esm(() => {
17943
18303
  init_dev();
17944
18304
  KobeKeymap = [
@@ -18151,6 +18511,14 @@ var init_keybindings2 = __esm(() => {
18151
18511
  description: "Open the update page (when a new version is available)",
18152
18512
  hint: { keys: "u", label: "update", status: false }
18153
18513
  },
18514
+ {
18515
+ id: "tasks.focusEngine",
18516
+ scope: "sidebar",
18517
+ keys: ["right"],
18518
+ category: "Tasks pane",
18519
+ description: "Focus the engine pane of the current window",
18520
+ hint: { keys: "\u2192", label: "engine", status: false }
18521
+ },
18154
18522
  {
18155
18523
  id: "tasks.toggleKeys",
18156
18524
  scope: "sidebar",
@@ -18400,6 +18768,7 @@ var init_keybindings2 = __esm(() => {
18400
18768
  ];
18401
18769
  KEYMAP_DEFAULTS = new Map(KobeKeymap.map((b) => [b.id, { keys: [...b.keys], hint: b.hint ? { ...b.hint } : undefined }]));
18402
18770
  [keymapVersion, setKeymapVersion] = createSignal(0);
18771
+ KEYMAP_BY_ID = new Map(KobeKeymap.map((b) => [b.id, b]));
18403
18772
  });
18404
18773
 
18405
18774
  // src/tui/context/keybindings-user.ts
@@ -18839,20 +19208,28 @@ function UiPrefsSync(props) {
18839
19208
  focusAccent: props.boot.focusAccent
18840
19209
  });
18841
19210
  const [prefsOrch, setPrefsOrch] = createSignal(null);
19211
+ let disposed = false;
18842
19212
  onMount(() => {
18843
19213
  (async () => {
19214
+ let remote = null;
18844
19215
  try {
18845
19216
  const client = await connectIfRunning();
18846
19217
  if (!client) {
18847
19218
  logClient("ui-prefs", "no daemon \u2014 keeping boot-time visual prefs");
18848
19219
  return;
18849
19220
  }
18850
- const remote = new RemoteOrchestrator(client);
19221
+ remote = new RemoteOrchestrator(client);
18851
19222
  await remote.init();
18852
- setPrefsOrch(remote);
18853
19223
  } catch (err) {
18854
19224
  logClientError("ui-prefs", err);
19225
+ remote?.dispose();
19226
+ return;
19227
+ }
19228
+ if (disposed) {
19229
+ remote.dispose();
19230
+ return;
18855
19231
  }
19232
+ setPrefsOrch(remote);
18856
19233
  })();
18857
19234
  });
18858
19235
  createEffect(() => {
@@ -18872,7 +19249,10 @@ function UiPrefsSync(props) {
18872
19249
  lastKeybindingsRev = rev;
18873
19250
  reloadUserKeybindings();
18874
19251
  });
18875
- onCleanup(() => prefsOrch()?.dispose());
19252
+ onCleanup(() => {
19253
+ disposed = true;
19254
+ prefsOrch()?.dispose();
19255
+ });
18876
19256
  return null;
18877
19257
  }
18878
19258
  async function bootPaneHost(opts) {
@@ -19097,6 +19477,31 @@ function repoBasename(repo) {
19097
19477
  function flattenIds(rows) {
19098
19478
  return rows.map((r) => r.task.id);
19099
19479
  }
19480
+ function sameSidebarRowTask(a, b) {
19481
+ 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;
19482
+ }
19483
+ function reconcileSidebarRows(prev, next) {
19484
+ if (prev.length === 0)
19485
+ return next;
19486
+ const prevById = new Map;
19487
+ for (const row of prev)
19488
+ prevById.set(row.task.id, row);
19489
+ let allReused = prev.length === next.length;
19490
+ const out = new Array(next.length);
19491
+ for (let i = 0;i < next.length; i++) {
19492
+ const fresh = next[i];
19493
+ const old = prevById.get(fresh.task.id);
19494
+ if (old && old.flatIndex === fresh.flatIndex && sameSidebarRowTask(old.task, fresh.task)) {
19495
+ out[i] = old;
19496
+ if (allReused && prev[i] !== old)
19497
+ allReused = false;
19498
+ } else {
19499
+ out[i] = fresh;
19500
+ allReused = false;
19501
+ }
19502
+ }
19503
+ return allReused ? prev : out;
19504
+ }
19100
19505
  var init_groups = () => {};
19101
19506
 
19102
19507
  // src/tui/quick-task/host.tsx
@@ -22039,7 +22444,7 @@ var init_task_actions = __esm(() => {
22039
22444
  });
22040
22445
 
22041
22446
  // src/tui/lib/worktree-opener.ts
22042
- import { spawn as spawn4 } from "child_process";
22447
+ import { spawn as spawn5 } from "child_process";
22043
22448
  import { existsSync as existsSync15 } from "fs";
22044
22449
  import { basename as basename7, delimiter, isAbsolute as isAbsolute2, join as join17 } from "path";
22045
22450
  function executableOnPath(command, env, exists) {
@@ -22098,7 +22503,7 @@ function buildOpenWorktreeCommand(worktreePath, opener) {
22098
22503
  function openWorktree(worktreePath, opener, deps = {}) {
22099
22504
  if (!worktreePath)
22100
22505
  return false;
22101
- const spawnFn = deps.spawn ?? spawn4;
22506
+ const spawnFn = deps.spawn ?? spawn5;
22102
22507
  const [command, args2] = buildOpenWorktreeCommand(worktreePath, opener);
22103
22508
  try {
22104
22509
  const child = spawnFn(command, args2, { detached: true, stdio: "ignore" });
@@ -22147,15 +22552,6 @@ var init_worktree_opener = __esm(() => {
22147
22552
  });
22148
22553
 
22149
22554
  // 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
22555
  function createBackgroundPoller(cfg) {
22160
22556
  const entries = new Map;
22161
22557
  function entryFor(key) {
@@ -22177,95 +22573,75 @@ function createBackgroundPoller(cfg) {
22177
22573
  if (!key)
22178
22574
  return;
22179
22575
  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
- })();
22576
+ maybeStartScheduledRun(entry, cfg, (signal) => cfg.run(key, signal), (value) => entry.write(value));
22200
22577
  },
22201
22578
  reset() {
22202
22579
  entries.clear();
22203
22580
  }
22204
22581
  };
22205
22582
  }
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
22583
  var init_background_poll = __esm(() => {
22231
22584
  init_dev();
22585
+ init_poll_scheduling();
22586
+ init_poll_scheduling();
22232
22587
  });
22233
22588
 
22234
22589
  // src/tui/panes/sidebar/git-head.ts
22590
+ import { stat as stat4 } from "fs/promises";
22591
+ import { join as join18 } from "path";
22592
+ async function headFingerprint(repo) {
22593
+ try {
22594
+ const st = await stat4(join18(repo, ".git", "HEAD"));
22595
+ return `${st.mtimeMs}:${st.size}`;
22596
+ } catch {
22597
+ return null;
22598
+ }
22599
+ }
22600
+ async function resolveBranchHead(repo, signal, spawn6 = spawnCapture) {
22601
+ const fingerprint = await headFingerprint(repo);
22602
+ if (fingerprint !== null) {
22603
+ const cached4 = headCache.get(repo);
22604
+ if (cached4 && cached4.fingerprint === fingerprint)
22605
+ return cached4.value;
22606
+ }
22607
+ let value = "";
22608
+ const ref = await spawn6("git", ["symbolic-ref", "--short", "HEAD"], {
22609
+ cwd: repo,
22610
+ env: gitEnv(),
22611
+ signal
22612
+ });
22613
+ const name = ref.status === 0 ? ref.stdout.trim() : "";
22614
+ if (name && name !== "HEAD") {
22615
+ value = name;
22616
+ } else {
22617
+ const head = await spawn6("git", ["rev-parse", "--verify", "HEAD"], {
22618
+ cwd: repo,
22619
+ env: gitEnv(),
22620
+ signal
22621
+ });
22622
+ if (head.status === 0)
22623
+ value = "(detached)";
22624
+ }
22625
+ if (fingerprint !== null)
22626
+ headCache.set(repo, { fingerprint, value });
22627
+ return value;
22628
+ }
22235
22629
  function currentBranch(repo) {
22236
22630
  return poller.read(repo);
22237
22631
  }
22238
22632
  function pollCurrentBranch(repo) {
22239
22633
  poller.poll(repo);
22240
22634
  }
22241
- var BRANCH_POLL_TIMEOUT_MS = 2000, BRANCH_SLOW_RETRY_MS = 30000, BRANCH_MIN_POLL_INTERVAL_MS = 1500, gitEnv = () => ({ ...process.env, GIT_OPTIONAL_LOCKS: "0" }), poller;
22635
+ var BRANCH_POLL_TIMEOUT_MS = 2000, BRANCH_SLOW_RETRY_MS = 30000, BRANCH_MIN_POLL_INTERVAL_MS = 1500, gitEnv = () => ({ ...process.env, GIT_OPTIONAL_LOCKS: "0" }), headCache, poller;
22242
22636
  var init_git_head = __esm(() => {
22243
22637
  init_background_poll();
22638
+ headCache = new Map;
22244
22639
  poller = createBackgroundPoller({
22245
22640
  initial: "",
22246
22641
  timeoutMs: BRANCH_POLL_TIMEOUT_MS,
22247
22642
  slowRetryMs: BRANCH_SLOW_RETRY_MS,
22248
22643
  minIntervalMs: BRANCH_MIN_POLL_INTERVAL_MS,
22249
- run: async (repo, signal) => {
22250
- const ref = await spawnCapture("git", ["symbolic-ref", "--short", "HEAD"], {
22251
- cwd: repo,
22252
- env: gitEnv(),
22253
- signal
22254
- });
22255
- if (ref.status === 0) {
22256
- const name = ref.stdout.trim();
22257
- if (name && name !== "HEAD")
22258
- return name;
22259
- }
22260
- const head = await spawnCapture("git", ["rev-parse", "--verify", "HEAD"], {
22261
- cwd: repo,
22262
- env: gitEnv(),
22263
- signal
22264
- });
22265
- if (head.status === 0)
22266
- return "(detached)";
22267
- return "";
22268
- }
22644
+ run: (repo, signal) => resolveBranchHead(repo, signal)
22269
22645
  });
22270
22646
  });
22271
22647
 
@@ -22366,20 +22742,18 @@ function useSidebarBindings(opts) {
22366
22742
  useBindings(() => ({
22367
22743
  enabled: opts.focused() && !searchModeAccessor(),
22368
22744
  bindings: bindByIds({
22369
- "sidebar.nav": (evt) => {
22745
+ "sidebar.nav": (_evt, slot) => {
22746
+ const down = (slot ?? 0) % 2 === 0;
22370
22747
  if (moveModeAccessor()) {
22371
22748
  const id = cursorTaskId();
22372
22749
  if (id === undefined)
22373
22750
  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);
22751
+ opts.onMoveRequest?.(id, down ? 1 : -1);
22378
22752
  return;
22379
22753
  }
22380
- if (evt.name === "j" || evt.name === "down")
22754
+ if (down)
22381
22755
  ctrl.moveDown();
22382
- else if (evt.name === "k" || evt.name === "up")
22756
+ else
22383
22757
  ctrl.moveUp();
22384
22758
  },
22385
22759
  "sidebar.select": () => {
@@ -22453,21 +22827,18 @@ function useSidebarBindings(opts) {
22453
22827
  useBindings(() => ({
22454
22828
  enabled: opts.focused(),
22455
22829
  bindings: bindByIds({
22456
- "sidebar.view": (evt) => {
22457
- if (evt.name === "]")
22458
- opts.onViewSwitch?.(1);
22459
- else
22460
- opts.onViewSwitch?.(-1);
22830
+ "sidebar.view": (_evt, slot) => {
22831
+ opts.onViewSwitch?.((slot ?? 0) % 2 === 0 ? -1 : 1);
22461
22832
  }
22462
22833
  })
22463
22834
  }));
22464
22835
  useBindings(() => ({
22465
22836
  enabled: opts.focused() && searchModeAccessor(),
22466
22837
  bindings: bindByIds({
22467
- "sidebar.search.nav": (evt) => {
22468
- if (evt.name === "down")
22838
+ "sidebar.search.nav": (_evt, slot) => {
22839
+ if ((slot ?? 0) % 2 === 0)
22469
22840
  ctrl.moveDown();
22470
- else if (evt.name === "up")
22841
+ else
22471
22842
  ctrl.moveUp();
22472
22843
  },
22473
22844
  "sidebar.search.submit": () => {
@@ -22523,11 +22894,12 @@ function buildSidebarRowView(opts) {
22523
22894
  const activityBadge = activityBadgeFor(activityState);
22524
22895
  const activityLabel = activityLabelFor(activityState);
22525
22896
  const untrackedCustomEngine = isCustomEngineTask(task) && !hasActivity;
22526
- const loading = !untrackedCustomEngine && (activityState === "running" || opts.live || !hasActivity && !isMain && task.status === "in_progress");
22897
+ const materializing = opts.job !== undefined;
22898
+ const loading = materializing || !untrackedCustomEngine && (activityState === "running" || opts.live || !hasActivity && !isMain && task.status === "in_progress");
22527
22899
  const spinner = IN_PROGRESS_SPINNER[opts.spinnerFrame] ?? IN_PROGRESS_SPINNER[0];
22528
- const tone = untrackedCustomEngine ? "textMuted" : activityLabel?.tone ?? (loading ? "primary" : activityBadge?.tone ?? badge.tone);
22900
+ const tone = materializing ? "primary" : untrackedCustomEngine ? "textMuted" : activityLabel?.tone ?? (loading ? "primary" : activityBadge?.tone ?? badge.tone);
22529
22901
  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);
22902
+ 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
22903
  const restGlyph = untrackedCustomEngine ? NO_TRACKING_GLYPH : activityBadge?.glyph ?? badge.glyph;
22532
22904
  const restProjectGlyph = untrackedCustomEngine ? NO_TRACKING_GLYPH : activityBadge?.glyph ?? "\u2605";
22533
22905
  return {
@@ -22540,6 +22912,14 @@ function buildSidebarRowView(opts) {
22540
22912
  tone
22541
22913
  };
22542
22914
  }
22915
+ function withSpinnerFrame(view, frame) {
22916
+ if (!view.loading)
22917
+ return view;
22918
+ const spinner = IN_PROGRESS_SPINNER[frame() % IN_PROGRESS_SPINNER.length] ?? "\u280B";
22919
+ if (spinner === view.stateGlyph && spinner === view.projectGlyph)
22920
+ return view;
22921
+ return { ...view, stateGlyph: spinner, projectGlyph: spinner };
22922
+ }
22543
22923
  function activityBadgeFor(state) {
22544
22924
  switch (state) {
22545
22925
  case "rate_limited":
@@ -22554,7 +22934,7 @@ function activityBadgeFor(state) {
22554
22934
  return null;
22555
22935
  }
22556
22936
  }
22557
- var STATUS_BADGE, STATUS_LABEL, IN_PROGRESS_SPINNER, SPINNER_FRAME_MS = 100, NO_TRACKING_GLYPH = "\xB7", NO_TRACKING_SUBTITLE = "no activity tracking";
22937
+ 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
22938
  var init_row_view = __esm(() => {
22559
22939
  init_vendor();
22560
22940
  init_groups();
@@ -22592,7 +22972,7 @@ var init_worktree_changes_poller = __esm(() => {
22592
22972
  ZERO2 = { added: 0, deleted: 0 };
22593
22973
  poller2 = createBackgroundPoller({
22594
22974
  initial: ZERO2,
22595
- equals: (a, b) => a.added === b.added && a.deleted === b.deleted,
22975
+ equals: sameWorktreeChanges,
22596
22976
  timeoutMs: POLL_TIMEOUT_MS,
22597
22977
  slowRetryMs: SLOW_REPO_RETRY_MS,
22598
22978
  minIntervalMs: MIN_POLL_INTERVAL_MS,
@@ -22680,7 +23060,7 @@ function Sidebar(props) {
22680
23060
  const spinnerInterval = setInterval(() => setSpinnerFrame((n) => (n + 1) % IN_PROGRESS_SPINNER.length), SPINNER_FRAME_MS);
22681
23061
  onCleanup(() => clearInterval(spinnerInterval));
22682
23062
  const sortMode = () => props.sortMode?.() ?? "default";
22683
- const rows = createMemo(() => buildRows(props.tasks(), view(), searchMode() ? searchQuery() : "", sortMode()));
23063
+ const rows = createMemo((prev) => reconcileSidebarRows(prev, buildRows(props.tasks(), view(), searchMode() ? searchQuery() : "", sortMode())), []);
22684
23064
  const flatIds = createMemo(() => flattenIds(rows()));
22685
23065
  const firstTaskFlatIndex = createMemo(() => {
22686
23066
  const r = rows();
@@ -23029,10 +23409,15 @@ function Sidebar(props) {
23029
23409
  }
23030
23410
  };
23031
23411
  const changes = createMemo(() => {
23412
+ const pushed = pickPushedChanges(props.worktreeChanges?.(), task.worktreePath);
23413
+ if (pushed)
23414
+ return pushed;
23032
23415
  branchTick();
23033
23416
  if (!task.archived)
23034
23417
  pollWorktreeChanges(task.worktreePath);
23035
23418
  return worktreeChanges(task.worktreePath);
23419
+ }, undefined, {
23420
+ equals: sameWorktreeChanges
23036
23421
  });
23037
23422
  const projectBranch = createMemo(() => {
23038
23423
  branchTick();
@@ -23040,15 +23425,17 @@ function Sidebar(props) {
23040
23425
  pollCurrentBranch(task.repo);
23041
23426
  return isMain ? currentBranch(task.repo) : "";
23042
23427
  });
23043
- const rowView = createMemo(() => buildSidebarRowView({
23428
+ const baseRowView = createMemo(() => buildSidebarRowView({
23044
23429
  task,
23045
23430
  activity: props.engineState?.().get(task.id),
23431
+ job: props.taskJobs?.().get(task.id),
23046
23432
  live: isLive(),
23047
- spinnerFrame: spinnerFrame(),
23433
+ spinnerFrame: 0,
23048
23434
  subtitleBudget: subtitleBudget(),
23049
23435
  truncateBranch: truncateBranchLabel,
23050
23436
  mainBranch: projectBranch()
23051
23437
  }));
23438
+ const rowView = createMemo(() => withSpinnerFrame(baseRowView(), spinnerFrame));
23052
23439
  const stateColor = () => isMain && !rowView().loading ? theme.primary : toneColor(rowView().tone);
23053
23440
  const barColor = () => isCursor() ? theme.focusAccent : isSelected() ? theme.primary : undefined;
23054
23441
  const barGlyph = () => isCursor() || isSelected() ? "\u258C" : " ";
@@ -23475,6 +23862,7 @@ var init_Sidebar = __esm(() => {
23475
23862
  init_groups();
23476
23863
  init_keys();
23477
23864
  init_row_view();
23865
+ init_worktree_changes();
23478
23866
  init_worktree_changes_poller();
23479
23867
  VIEW_TABS = [{
23480
23868
  view: "active",
@@ -23491,6 +23879,7 @@ __export(exports_host3, {
23491
23879
  startTasksPane: () => startTasksPane
23492
23880
  });
23493
23881
  import { existsSync as existsSync16 } from "fs";
23882
+ import { stat as stat5 } from "fs/promises";
23494
23883
  import { TextAttributes as TextAttributes12 } from "@opentui/core";
23495
23884
  function worktreeCwdUsable(cwd) {
23496
23885
  return !!cwd && worktreeUsable(cwd);
@@ -23672,6 +24061,17 @@ function TasksShell(props) {
23672
24061
  notifyError(`Couldn't open worktree with ${opener.label}`);
23673
24062
  }
23674
24063
  }
24064
+ async function focusEnginePane() {
24065
+ if (!process.env.TMUX_PANE)
24066
+ return;
24067
+ const session = await currentSessionName();
24068
+ if (!session)
24069
+ return;
24070
+ const pane = await claudePaneIdStrict(session);
24071
+ if (!pane)
24072
+ return;
24073
+ await runTmux(["select-pane", "-t", pane]);
24074
+ }
23675
24075
  async function moveTask(id, delta) {
23676
24076
  const task = props.tasks().find((t) => t.id === id);
23677
24077
  if (!task || task.kind === "main" || !props.orch)
@@ -23719,7 +24119,8 @@ function TasksShell(props) {
23719
24119
  if (id)
23720
24120
  cycleVendor(id);
23721
24121
  },
23722
- "tasks.toggleKeys": () => setKeysCollapsed(!keysCollapsed())
24122
+ "tasks.toggleKeys": () => setKeysCollapsed(!keysCollapsed()),
24123
+ "tasks.focusEngine": () => void focusEnginePane().catch((err) => console.error("[kobe tasks] focus engine pane failed:", err))
23723
24124
  })
23724
24125
  }));
23725
24126
  async function switchTo(id) {
@@ -23825,6 +24226,17 @@ function TasksShell(props) {
23825
24226
  get engineState() {
23826
24227
  return memo2(() => !!props.orch)() ? props.orch.engineStateSignal() : undefined;
23827
24228
  },
24229
+ get taskJobs() {
24230
+ return memo2(() => !!props.orch)() ? props.orch.taskJobsSignal() : undefined;
24231
+ },
24232
+ get worktreeChanges() {
24233
+ return props.orch ? () => {
24234
+ const orch = props.orch;
24235
+ if (!orch || orch.connectionStateSignal()() !== "online")
24236
+ return null;
24237
+ return orch.worktreeChangesSignal()();
24238
+ } : undefined;
24239
+ },
23828
24240
  onRenameRequest: (id) => void renameTask(id),
23829
24241
  onDeleteRequest: (id) => void deleteTask(id),
23830
24242
  onArchiveRequest: (id) => void archiveTask(id),
@@ -23949,6 +24361,10 @@ function ShortcutHints(props) {
23949
24361
  k: "enter",
23950
24362
  label: "open"
23951
24363
  },
24364
+ {
24365
+ k: "right",
24366
+ label: "focus engine"
24367
+ },
23952
24368
  {
23953
24369
  k: "n",
23954
24370
  label: "new task"
@@ -24109,10 +24525,21 @@ async function setupTasksPane(opts) {
24109
24525
  await store2.load();
24110
24526
  setFileTasks(store2.list());
24111
24527
  };
24528
+ let lastTasksFileFingerprint = "";
24112
24529
  const timer = setInterval(() => {
24113
24530
  if (orch && orch.connectionStateSignal()() === "online")
24114
24531
  return;
24115
- reload();
24532
+ (async () => {
24533
+ let fingerprint = "missing";
24534
+ try {
24535
+ const st = await stat5(store2.filePath);
24536
+ fingerprint = `${st.mtimeMs}:${st.size}`;
24537
+ } catch {}
24538
+ if (fingerprint === lastTasksFileFingerprint)
24539
+ return;
24540
+ lastTasksFileFingerprint = fingerprint;
24541
+ await reload();
24542
+ })().catch(() => {});
24116
24543
  }, RELOAD_MS);
24117
24544
  return {
24118
24545
  root: () => [createComponent2(TasksShell, {
@@ -25028,26 +25455,24 @@ function useFileTreeBindings(opts) {
25028
25455
  useBindings(() => ({
25029
25456
  enabled: opts.focused(),
25030
25457
  bindings: bindByIds({
25031
- "files.nav": (evt) => {
25032
- if (evt.name === "j" || evt.name === "down")
25458
+ "files.nav": (_evt, slot) => {
25459
+ if ((slot ?? 0) % 2 === 0)
25033
25460
  opts.moveDown();
25034
- else if (evt.name === "k" || evt.name === "up")
25461
+ else
25035
25462
  opts.moveUp();
25036
25463
  },
25037
- "files.hierarchy": (evt) => {
25038
- if (evt.name === "l" || evt.name === "right")
25039
- opts.expandOrDescend();
25040
- else if (evt.name === "h" || evt.name === "left")
25464
+ "files.hierarchy": (_evt, slot) => {
25465
+ if ((slot ?? 0) % 2 === 0)
25041
25466
  opts.collapseOrParent();
25467
+ else
25468
+ opts.expandOrDescend();
25042
25469
  },
25043
- "files.tab": (evt) => {
25470
+ "files.tab": (_evt, slot) => {
25044
25471
  const cur = opts.currentTab();
25045
25472
  const idx = TAB_ORDER.indexOf(cur);
25046
25473
  if (idx < 0)
25047
25474
  return;
25048
- const delta = evt.name === "[" ? -1 : evt.name === "]" ? 1 : 0;
25049
- if (delta === 0)
25050
- return;
25475
+ const delta = (slot ?? 0) % 2 === 0 ? -1 : 1;
25051
25476
  const next = TAB_ORDER[(idx + delta + TAB_ORDER.length) % TAB_ORDER.length];
25052
25477
  if (next)
25053
25478
  opts.setTab(next);