@sma1lboy/kobe 0.7.19 → 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 +315 -230
  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.19",
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",
@@ -5069,66 +5070,6 @@ var init_history = __esm(() => {
5069
5070
  };
5070
5071
  });
5071
5072
 
5072
- // src/engine/claude-code-local/cost.ts
5073
- import { readFile as readFile3 } from "fs/promises";
5074
- async function summarizeClaudeWorktreeCost(worktree) {
5075
- const files = await listSessionFilesForWorktree(worktree);
5076
- const base = {
5077
- sessionCount: files.length,
5078
- inputTokens: 0,
5079
- outputTokens: 0,
5080
- cacheReadTokens: 0,
5081
- cacheCreateTokens: 0,
5082
- lastActivityMs: files[0]?.mtimeMs ?? null
5083
- };
5084
- if (files.length === 0)
5085
- return base;
5086
- let input = 0;
5087
- let output = 0;
5088
- let cacheRead = 0;
5089
- let cacheCreate = 0;
5090
- for (const file of files) {
5091
- let raw;
5092
- try {
5093
- raw = await readFile3(file.path, "utf8");
5094
- } catch {
5095
- continue;
5096
- }
5097
- for (const line of raw.split(`
5098
- `)) {
5099
- if (line.length === 0)
5100
- continue;
5101
- let parsed;
5102
- try {
5103
- parsed = JSON.parse(line);
5104
- } catch {
5105
- continue;
5106
- }
5107
- const usage = parsed.message?.usage;
5108
- if (!usage)
5109
- continue;
5110
- if (typeof usage.input_tokens === "number")
5111
- input += usage.input_tokens;
5112
- if (typeof usage.output_tokens === "number")
5113
- output += usage.output_tokens;
5114
- if (typeof usage.cache_read_input_tokens === "number")
5115
- cacheRead += usage.cache_read_input_tokens;
5116
- if (typeof usage.cache_creation_input_tokens === "number")
5117
- cacheCreate += usage.cache_creation_input_tokens;
5118
- }
5119
- }
5120
- return {
5121
- ...base,
5122
- inputTokens: input,
5123
- outputTokens: output,
5124
- cacheReadTokens: cacheRead,
5125
- cacheCreateTokens: cacheCreate
5126
- };
5127
- }
5128
- var init_cost = __esm(() => {
5129
- init_history();
5130
- });
5131
-
5132
5073
  // src/cli/invocation.ts
5133
5074
  import { fileURLToPath } from "url";
5134
5075
  function kobeCliInvocation() {
@@ -5225,7 +5166,7 @@ function opsPaneCommand(args) {
5225
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;
5226
5167
 
5227
5168
  // src/engine/claude-code-local/hook-adapter.ts
5228
- 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";
5229
5170
  import { dirname as dirname4 } from "path";
5230
5171
  function failureFromErrorType(errorType) {
5231
5172
  if (typeof errorType !== "string")
@@ -5241,7 +5182,7 @@ function isObject2(v) {
5241
5182
  }
5242
5183
  async function readJsonObject(path8) {
5243
5184
  try {
5244
- const parsed = JSON.parse(await readFile4(path8, "utf8"));
5185
+ const parsed = JSON.parse(await readFile3(path8, "utf8"));
5245
5186
  return isObject2(parsed) ? parsed : {};
5246
5187
  } catch {
5247
5188
  return {};
@@ -5455,7 +5396,7 @@ function validPositive(v) {
5455
5396
  }
5456
5397
 
5457
5398
  // src/engine/codex-local/history.ts
5458
- 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";
5459
5400
  import { homedir as homedir9 } from "os";
5460
5401
  import path8 from "path";
5461
5402
  async function listRolloutFiles(deps = defaultDeps7) {
@@ -5502,6 +5443,26 @@ function rolloutCwd(raw) {
5502
5443
  }
5503
5444
  return "";
5504
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
+ }
5505
5466
  async function listSessionIdsForWorktree(worktree, deps = defaultDeps7) {
5506
5467
  if (!worktree)
5507
5468
  return [];
@@ -5512,13 +5473,7 @@ async function listSessionIdsForWorktree(worktree, deps = defaultDeps7) {
5512
5473
  if (scanned >= MAX_WORKTREE_SCAN)
5513
5474
  break;
5514
5475
  scanned++;
5515
- let raw;
5516
- try {
5517
- raw = await deps.readFile(file);
5518
- } catch {
5519
- continue;
5520
- }
5521
- if (rolloutCwd(raw) !== worktree)
5476
+ if (await rolloutCwdForFile(file, deps) !== worktree)
5522
5477
  continue;
5523
5478
  const id = path8.basename(file).match(UUID_AT_END)?.[1];
5524
5479
  if (id)
@@ -5526,30 +5481,25 @@ async function listSessionIdsForWorktree(worktree, deps = defaultDeps7) {
5526
5481
  }
5527
5482
  return matches.reverse();
5528
5483
  }
5529
- async function latestTranscriptMtimeForWorktree2(worktree, deps = defaultDeps7) {
5484
+ async function findLatestRolloutForWorktree(worktree, deps = defaultDeps7) {
5530
5485
  if (!worktree)
5531
- return 0;
5486
+ return null;
5532
5487
  const files = await listRolloutFiles(deps);
5533
5488
  let scanned = 0;
5534
5489
  for (const file of files) {
5535
5490
  if (scanned >= MAX_MTIME_SCAN)
5536
5491
  break;
5537
5492
  scanned++;
5538
- let raw;
5539
- try {
5540
- raw = await deps.readFile(file);
5541
- } catch {
5542
- continue;
5543
- }
5544
- if (rolloutCwd(raw) !== worktree)
5493
+ if (await rolloutCwdForFile(file, deps) !== worktree)
5545
5494
  continue;
5546
5495
  try {
5547
- return (await deps.stat(file)).mtimeMs;
5548
- } catch {
5549
- return 0;
5550
- }
5496
+ return { path: file, mtimeMs: (await deps.stat(file)).mtimeMs };
5497
+ } catch {}
5551
5498
  }
5552
- return 0;
5499
+ return null;
5500
+ }
5501
+ async function latestTranscriptMtimeForWorktree2(worktree, deps = defaultDeps7) {
5502
+ return (await findLatestRolloutForWorktree(worktree, deps))?.mtimeMs ?? 0;
5553
5503
  }
5554
5504
  async function readHistory2(sessionId, deps = defaultDeps7) {
5555
5505
  return (await readHistoryWithMetrics(sessionId, deps)).messages;
@@ -5782,7 +5732,7 @@ function parseTimestampMs(value) {
5782
5732
  function isObject4(v) {
5783
5733
  return typeof v === "object" && v !== null && !Array.isArray(v);
5784
5734
  }
5785
- 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;
5786
5736
  var init_history2 = __esm(() => {
5787
5737
  init_synthetic();
5788
5738
  defaultDeps7 = {
@@ -5797,11 +5747,12 @@ var init_history2 = __esm(() => {
5797
5747
  }
5798
5748
  },
5799
5749
  async readFile(p) {
5800
- return await readFile5(p, "utf8");
5750
+ return await readFile4(p, "utf8");
5801
5751
  },
5802
5752
  stat: stat2
5803
5753
  };
5804
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;
5805
5756
  });
5806
5757
 
5807
5758
  // src/engine/copilot-local/usage.ts
@@ -5839,7 +5790,7 @@ function numberOr2(value, fallback) {
5839
5790
  }
5840
5791
 
5841
5792
  // src/engine/copilot-local/history.ts
5842
- 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";
5843
5794
  import { homedir as homedir10 } from "os";
5844
5795
  import path9 from "path";
5845
5796
  async function listSessionDirs(deps = defaultDeps8) {
@@ -6019,7 +5970,7 @@ var init_history3 = __esm(() => {
6019
5970
  }
6020
5971
  },
6021
5972
  async readFile(p) {
6022
- return await readFile6(p, "utf8");
5973
+ return await readFile5(p, "utf8");
6023
5974
  },
6024
5975
  stat: stat3,
6025
5976
  async rm(p) {
@@ -6058,7 +6009,7 @@ var init_hook_adapter2 = __esm(() => {
6058
6009
  });
6059
6010
 
6060
6011
  // src/engine/turn-detector.ts
6061
- import { readFile as readFile7 } from "fs/promises";
6012
+ import { readFile as readFile6 } from "fs/promises";
6062
6013
 
6063
6014
  class EngineTurnDetector {
6064
6015
  supportsCompletionMarkers() {
@@ -6131,42 +6082,71 @@ function timestampFromRecord(record, fallback) {
6131
6082
  function isObject7(v) {
6132
6083
  return typeof v === "object" && v !== null && !Array.isArray(v);
6133
6084
  }
6134
- var ClaudeTurnDetector, CodexTurnDetector, UnknownTurnDetector;
6085
+ var defaultClaudeDeps, ClaudeTurnDetector, defaultCodexDeps, CodexTurnDetector, UnknownTurnDetector;
6135
6086
  var init_turn_detector = __esm(() => {
6136
6087
  init_history();
6137
6088
  init_history2();
6138
6089
  init_registry();
6090
+ defaultClaudeDeps = {
6091
+ listSessionFiles: (worktree) => listSessionFilesForWorktree(worktree),
6092
+ readFile: (path10) => readFile6(path10, "utf8")
6093
+ };
6139
6094
  ClaudeTurnDetector = class ClaudeTurnDetector extends EngineTurnDetector {
6095
+ deps;
6140
6096
  vendor = "claude";
6097
+ cache = new Map;
6098
+ constructor(deps = defaultClaudeDeps) {
6099
+ super();
6100
+ this.deps = deps;
6101
+ }
6141
6102
  async latestCompletion(worktree) {
6142
- const files = await listSessionFilesForWorktree(worktree);
6103
+ const files = await this.deps.listSessionFiles(worktree);
6143
6104
  let latest = null;
6105
+ const next = new Map;
6144
6106
  for (const file of files.slice(0, 4)) {
6145
- const raw = await readFile7(file.path, "utf8").catch(() => "");
6146
- 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 });
6147
6116
  if (marker && (!latest || marker.timestampMs > latest.timestampMs))
6148
6117
  latest = marker;
6149
6118
  }
6119
+ this.cache = next;
6150
6120
  return latest;
6151
6121
  }
6152
6122
  };
6123
+ defaultCodexDeps = {
6124
+ findLatestRollout: (worktree) => findLatestRolloutForWorktree(worktree),
6125
+ readFile: (path10) => readFile6(path10, "utf8")
6126
+ };
6153
6127
  CodexTurnDetector = class CodexTurnDetector extends EngineTurnDetector {
6128
+ deps;
6154
6129
  vendor = "codex";
6130
+ cache = null;
6131
+ constructor(deps = defaultCodexDeps) {
6132
+ super();
6133
+ this.deps = deps;
6134
+ }
6155
6135
  async latestCompletion(worktree) {
6156
6136
  if (!worktree)
6157
6137
  return null;
6158
- const files = await listRolloutFiles();
6159
- let scanned = 0;
6160
- for (const file of files) {
6161
- if (scanned >= 12)
6162
- break;
6163
- scanned++;
6164
- const raw = await readFile7(file, "utf8").catch(() => "");
6165
- if (!raw || rolloutCwd(raw) !== worktree)
6166
- continue;
6167
- 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;
6168
6143
  }
6169
- 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;
6170
6150
  }
6171
6151
  };
6172
6152
  UnknownTurnDetector = class UnknownTurnDetector extends EngineTurnDetector {
@@ -6192,7 +6172,6 @@ function customEngineEntry(vendor) {
6192
6172
  displayName: vendor,
6193
6173
  defaultCommand: [vendor],
6194
6174
  history: EMPTY_HISTORY,
6195
- summarizeCost: null,
6196
6175
  detectAccount: async () => ({
6197
6176
  binary: { found: false, error: "custom engine: kobe has no account detector for it" },
6198
6177
  account: { kind: "none" }
@@ -6208,7 +6187,6 @@ var EMPTY_HISTORY, claudeHistoryReader, codexHistoryReader, copilotHistoryReader
6208
6187
  var init_registry = __esm(() => {
6209
6188
  init_vendor();
6210
6189
  init_account_detect();
6211
- init_cost();
6212
6190
  init_history();
6213
6191
  init_hook_adapter();
6214
6192
  init_history2();
@@ -6251,7 +6229,6 @@ var init_registry = __esm(() => {
6251
6229
  displayName: "Claude",
6252
6230
  defaultCommand: ["claude"],
6253
6231
  history: claudeHistoryReader,
6254
- summarizeCost: (worktree) => summarizeClaudeWorktreeCost(worktree),
6255
6232
  detectAccount: (deps) => detectClaudeAccount(deps),
6256
6233
  createHookAdapter: () => new ClaudeHookAdapter,
6257
6234
  createTurnDetector: () => new ClaudeTurnDetector
@@ -6262,7 +6239,6 @@ var init_registry = __esm(() => {
6262
6239
  displayName: "Codex",
6263
6240
  defaultCommand: ["codex"],
6264
6241
  history: codexHistoryReader,
6265
- summarizeCost: null,
6266
6242
  detectAccount: (deps) => detectCodexAccount(deps),
6267
6243
  createHookAdapter: () => new NoopHookAdapter("codex"),
6268
6244
  createTurnDetector: () => new CodexTurnDetector
@@ -6273,7 +6249,6 @@ var init_registry = __esm(() => {
6273
6249
  displayName: "Copilot",
6274
6250
  defaultCommand: ["copilot"],
6275
6251
  history: copilotHistoryReader,
6276
- summarizeCost: null,
6277
6252
  detectAccount: (deps) => detectCopilotAccount(deps),
6278
6253
  createHookAdapter: () => new NoopHookAdapter("copilot"),
6279
6254
  createTurnDetector: () => new UnknownTurnDetector("copilot")
@@ -6342,6 +6317,7 @@ __export(exports_client, {
6342
6317
  globalTasksPaneWidth: () => globalTasksPaneWidth,
6343
6318
  getSessionOptions: () => getSessionOptions,
6344
6319
  getSessionOption: () => getSessionOption,
6320
+ getServerOptions: () => getServerOptions,
6345
6321
  getServerOption: () => getServerOption,
6346
6322
  ensureFallbackSession: () => ensureFallbackSession,
6347
6323
  currentSessionName: () => currentSessionName,
@@ -6480,6 +6456,22 @@ async function getServerOption(option) {
6480
6456
  const { code, stdout } = await runTmuxCapturing(["show-options", "-sqv", option]);
6481
6457
  return code === 0 ? stdout.trim() : "";
6482
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
+ }
6483
6475
  async function globalTasksPaneWidth() {
6484
6476
  const raw = await getServerOption(TASKS_WIDTH_OPTION);
6485
6477
  const n = Number.parseInt(raw, 10);
@@ -6637,18 +6629,18 @@ async function listChatTabWindows(session, runner = realRunner) {
6637
6629
  "-t",
6638
6630
  `=${session}`,
6639
6631
  "-F",
6640
- `#{window_index} #{${CHAT_TAB_SESSION_ID_OPTION}}`
6632
+ `#{window_index} #{automatic-rename} #{${CHAT_TAB_SESSION_ID_OPTION}}`
6641
6633
  ]);
6642
6634
  if (code !== 0)
6643
6635
  return [];
6644
6636
  const out = [];
6645
6637
  for (const line of stdout.split(`
6646
6638
  `)) {
6647
- const tab = line.indexOf("\t");
6648
- 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);
6649
6641
  if (!Number.isInteger(index))
6650
6642
  continue;
6651
- out.push({ index, sessionId: tab >= 0 ? line.slice(tab + 1).trim() : "" });
6643
+ out.push({ index, sessionId: sessionId?.trim() ?? "", autoRename: autoRename?.trim() ?? "" });
6652
6644
  }
6653
6645
  return out;
6654
6646
  }
@@ -6661,11 +6653,27 @@ async function windowNamedManually(session, index, runner) {
6661
6653
  ]);
6662
6654
  return code === 0 && /\boff\b/.test(stdout);
6663
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
+ }
6664
6660
  async function renameWindow(session, index, title, runner) {
6665
6661
  return await runner.run(["rename-window", "-t", `=${session}:${index}`, "--", title]) === 0;
6666
6662
  }
6667
6663
  async function runChatTabNamingPass(orch, deps = realDeps) {
6668
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
+ };
6669
6677
  for (const task of orch.listTasks()) {
6670
6678
  if (task.archived || task.kind === "main" || !task.worktreePath)
6671
6679
  continue;
@@ -6677,7 +6685,7 @@ async function runChatTabNamingPass(orch, deps = realDeps) {
6677
6685
  const vendor = task.vendor ?? DEFAULT_TASK_VENDOR;
6678
6686
  for (const w of windows) {
6679
6687
  try {
6680
- if (await windowNamedManually(session, w.index, deps.runner))
6688
+ if (await manuallyNamed(session, w))
6681
6689
  continue;
6682
6690
  const title = w.sessionId ? await deps.titleFromSessionId(vendor, w.sessionId) : w.index === originIndex ? await deps.titleFromWorktree(task.worktreePath, vendor) : "";
6683
6691
  if (title && await renameWindow(session, w.index, title, deps.runner))
@@ -7583,7 +7591,7 @@ var init_worktree_changes_collector = __esm(() => {
7583
7591
  });
7584
7592
 
7585
7593
  // ../kobe-daemon/src/daemon/server.ts
7586
- 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";
7587
7595
  import { createServer } from "net";
7588
7596
  import { dirname as dirname7 } from "path";
7589
7597
  function resolveIdleGraceMs() {
@@ -7797,7 +7805,7 @@ async function startDaemonServer(orch, options = {}) {
7797
7805
  }
7798
7806
  async function readPidFile(pidPath) {
7799
7807
  try {
7800
- const raw = await readFile8(pidPath, "utf8");
7808
+ const raw = await readFile7(pidPath, "utf8");
7801
7809
  const pid = Number(raw.trim());
7802
7810
  return Number.isFinite(pid) ? pid : null;
7803
7811
  } catch {
@@ -7808,10 +7816,12 @@ function writeFrame(client, frame) {
7808
7816
  client.socket.write(frameToLine(frame));
7809
7817
  }
7810
7818
  function broadcast(clients, frame) {
7819
+ let line = null;
7811
7820
  for (const client of clients) {
7812
7821
  if (!client.subscribed && frame.type === "event")
7813
7822
  continue;
7814
- writeFrame(client, frame);
7823
+ line ??= frameToLine(frame);
7824
+ client.socket.write(line);
7815
7825
  }
7816
7826
  }
7817
7827
  var DEFAULT_UPDATE_POLL_MS, DEFAULT_IDLE_GRACE_MS = 3000;
@@ -10456,10 +10466,17 @@ function parseKobePaneRows(stdout) {
10456
10466
  const line = raw.trim();
10457
10467
  if (!line)
10458
10468
  continue;
10459
- const [windowId, paneId, role, version] = line.split("\t");
10469
+ const [windowId, paneId, role, version, paneWidth] = line.split("\t");
10460
10470
  if (!windowId || !paneId || !role)
10461
10471
  continue;
10462
- 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
+ });
10463
10480
  }
10464
10481
  return rows;
10465
10482
  }
@@ -10524,38 +10541,18 @@ async function relaunchEngineInAllWindows(session, cwd, command, remoteKey) {
10524
10541
  if (enginePanes.length === 0)
10525
10542
  return false;
10526
10543
  const cmd = keepAlive(wrapEngineLaunch(shellQuoteArgv(command), remoteKey, cwd));
10527
- for (const pane of enginePanes) {
10528
- await runTmux(["respawn-pane", "-k", "-c", localSpawnCwd(cwd), "-t", pane, cmd]);
10529
- }
10544
+ await runTmuxSequence(enginePanes.map((pane) => ["respawn-pane", "-k", "-c", localSpawnCwd(cwd), "-t", pane, cmd]));
10530
10545
  return true;
10531
10546
  }
10532
- async function healTaskPaneWidths(session) {
10533
- const target = await globalTasksPaneWidth();
10534
- const { code, stdout } = await runTmuxCapturing([
10535
- "list-panes",
10536
- "-s",
10537
- "-t",
10538
- `=${session}`,
10539
- "-F",
10540
- "#{pane_id}\t#{@kobe_role}\t#{pane_width}"
10541
- ]);
10542
- if (code !== 0)
10543
- return;
10544
- const mismatched = stdout.split(`
10545
- `).map((line) => line.split("\t")).filter(([, role]) => role?.trim() === "tasks").filter(([, , width]) => Number.parseInt(width?.trim() ?? "", 10) !== target).map(([id]) => id?.trim()).filter((id) => !!id);
10546
- if (mismatched.length === 0)
10547
- return;
10548
- await runTmuxSequence(mismatched.map((pane) => ["resize-pane", "-t", pane, "-x", `${target}`]));
10549
- }
10550
- async function rightColumnPercents() {
10551
- const [width, height] = await Promise.all([
10552
- getServerOption(RIGHT_COLUMN_WIDTH_OPTION),
10553
- getServerOption(OPS_HEIGHT_OPTION)
10554
- ]);
10555
- return {
10556
- widthPct: clampPanePercent(Number.parseInt(width, 10)),
10557
- heightPct: clampPanePercent(Number.parseInt(height, 10))
10558
- };
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 };
10559
10556
  }
10560
10557
  function rightColumnResizeArgs(geom) {
10561
10558
  const args = [];
@@ -10566,33 +10563,35 @@ function rightColumnResizeArgs(geom) {
10566
10563
  return args;
10567
10564
  }
10568
10565
  async function globalRightColumnResizeArgs() {
10569
- return rightColumnResizeArgs(await rightColumnPercents());
10566
+ return (await globalLayoutPrefs()).rcArgs;
10570
10567
  }
10571
- async function healRightColumn(session) {
10572
- const args = await globalRightColumnResizeArgs();
10573
- if (args.length === 0)
10574
- return;
10575
- const { code, stdout } = await runTmuxCapturing([
10576
- "list-panes",
10577
- "-s",
10578
- "-t",
10579
- `=${session}`,
10580
- "-F",
10581
- "#{pane_id}\t#{@kobe_role}"
10582
- ]);
10583
- if (code !== 0)
10584
- return;
10585
- const opsPanes = stdout.split(`
10586
- `).map((line) => line.split("\t")).filter(([, role]) => role?.trim() === "ops").map(([id]) => id?.trim()).filter((id) => !!id);
10587
- if (opsPanes.length === 0)
10568
+ async function healWorkspaceLayout(session, versions) {
10569
+ const { tasksWidth, rcArgs } = await globalLayoutPrefs();
10570
+ const rows = await listKobePanes(session);
10571
+ if (!rows)
10588
10572
  return;
10589
- 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);
10590
10590
  }
10591
10591
  async function healSessionLayout(session) {
10592
10592
  if (!await sessionExists(session))
10593
10593
  return;
10594
- await healTaskPaneWidths(session);
10595
- await healRightColumn(session);
10594
+ await healWorkspaceLayout(session);
10596
10595
  }
10597
10596
  async function captureGlobalLayout(session) {
10598
10597
  const { code, stdout } = await runTmuxCapturing([
@@ -10631,18 +10630,6 @@ async function captureGlobalLayout(session) {
10631
10630
  if (sets.length > 0)
10632
10631
  await runTmuxSequence(sets);
10633
10632
  }
10634
- async function healKobePaneVersions(session, cwd, taskId, vendor) {
10635
- const rows = await listKobePanes(session);
10636
- if (!rows)
10637
- return;
10638
- const commands = respawnCommandsFor(planPaneHeals(rows, { currentVersion: CURRENT_VERSION, force: false }), {
10639
- cwd,
10640
- taskId,
10641
- vendor
10642
- });
10643
- if (commands.length > 0)
10644
- await runTmuxSequence(commands);
10645
- }
10646
10633
  async function refreshKobeWorkspacePanes(session) {
10647
10634
  const sessionOptions = await getSessionOptions(session, ["@kobe_worktree", "@kobe_task", "@kobe_vendor"]);
10648
10635
  const cwd = sessionOptions["@kobe_worktree"] || process.cwd();
@@ -10668,7 +10655,7 @@ var init_pane_heal = __esm(() => {
10668
10655
  init_tmux_border_theme();
10669
10656
  init_version();
10670
10657
  init_launch();
10671
- 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}`;
10672
10659
  });
10673
10660
 
10674
10661
  // src/tui/panes/terminal/chattab.ts
@@ -10898,6 +10885,7 @@ __export(exports_tmux, {
10898
10885
  refreshKobeWorkspacePanes: () => refreshKobeWorkspacePanes,
10899
10886
  quickCreate: () => quickCreate,
10900
10887
  prepareWindowForAttach: () => prepareWindowForAttach,
10888
+ parseObservedSession: () => parseObservedSession,
10901
10889
  openUpdateTab: () => openUpdateTab,
10902
10890
  openSettingsTab: () => openSettingsTab,
10903
10891
  openNewTaskTab: () => openNewTaskTab,
@@ -10906,6 +10894,7 @@ __export(exports_tmux, {
10906
10894
  kobeStatusRight: () => kobeStatusRight,
10907
10895
  killSession: () => killSession,
10908
10896
  healSessionLayout: () => healSessionLayout,
10897
+ focusBindCommand: () => focusBindCommand,
10909
10898
  ensureSession: () => ensureSession,
10910
10899
  currentSessionName: () => currentSessionName,
10911
10900
  chatTabSwitchBindings: () => chatTabSwitchBindings,
@@ -10933,8 +10922,18 @@ async function prepareWindowForAttach(session) {
10933
10922
  const sizeArgs = tmuxInitialSizeArgs();
10934
10923
  if (sizeArgs.length > 0)
10935
10924
  await runTmux(["resize-window", "-t", `=${session}`, ...sizeArgs]);
10936
- await healTaskPaneWidths(session);
10937
- 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
+ ];
10938
10937
  }
10939
10938
  async function ensureSession(opts) {
10940
10939
  const inflight = ensureSessionLocks.get(opts.name);
@@ -10948,16 +10947,33 @@ async function ensureSession(opts) {
10948
10947
  ensureSessionLocks.delete(opts.name);
10949
10948
  }
10950
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
+ }
10951
10970
  async function observeSession(name) {
10952
10971
  if (!await sessionExists(name))
10953
10972
  return null;
10954
- const sessionOptions = await getSessionOptions(name, ["@kobe_worktree", "@kobe_vendor"]);
10955
- return {
10956
- worktree: sessionOptions["@kobe_worktree"] ?? "",
10957
- vendor: sessionOptions["@kobe_vendor"] ?? "",
10958
- claudePaneAlive: await claudePaneIdStrict(name) !== "",
10959
- windowCount: await windowCount(name)
10960
- };
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);
10961
10977
  }
10962
10978
  async function ensureSessionImpl(opts) {
10963
10979
  const observed = await observeSession(opts.name);
@@ -10968,18 +10984,14 @@ async function ensureSessionImpl(opts) {
10968
10984
  });
10969
10985
  const remoteKey = remoteKeyForRepo(opts.repo);
10970
10986
  if (action.kind === "reuse") {
10971
- await healTaskPaneWidths(opts.name);
10972
- await healRightColumn(opts.name);
10973
- await healKobePaneVersions(opts.name, opts.cwd, opts.taskId, opts.vendor);
10987
+ await healWorkspaceLayout(opts.name, { cwd: opts.cwd, taskId: opts.taskId, vendor: opts.vendor });
10974
10988
  return true;
10975
10989
  }
10976
10990
  if (action.kind === "respawn-engine") {
10977
10991
  if (await relaunchEngineInAllWindows(opts.name, opts.cwd, opts.command, remoteKey)) {
10978
10992
  if (opts.vendor)
10979
10993
  await setSessionOption(opts.name, "@kobe_vendor", opts.vendor);
10980
- await healTaskPaneWidths(opts.name);
10981
- await healRightColumn(opts.name);
10982
- await healKobePaneVersions(opts.name, opts.cwd, opts.taskId, opts.vendor);
10994
+ await healWorkspaceLayout(opts.name, { cwd: opts.cwd, taskId: opts.taskId, vendor: opts.vendor });
10983
10995
  return true;
10984
10996
  }
10985
10997
  }
@@ -11054,7 +11066,7 @@ async function ensureSessionImpl(opts) {
11054
11066
  const focusDirections = ["-L", "-D", "-U", "-R"];
11055
11067
  const focusBinds = userKeys.focus.flatMap((bind, i) => {
11056
11068
  const dir = focusDirections[i];
11057
- return bind && dir ? [["bind-key", "-n", bind.key, "select-pane", dir]] : [];
11069
+ return bind && dir ? [focusBindCommand(bind.key, dir)] : [];
11058
11070
  });
11059
11071
  const b = userKeys.binds;
11060
11072
  await runTmuxSequence([
@@ -11114,7 +11126,7 @@ async function selectTasksPane(session) {
11114
11126
  await runTmux(["select-pane", "-t", tasksPane]);
11115
11127
  return tasksPane;
11116
11128
  }
11117
- 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}";
11118
11130
  var init_tmux = __esm(() => {
11119
11131
  init_invocation();
11120
11132
  init_interactive_command();
@@ -11130,6 +11142,12 @@ var init_tmux = __esm(() => {
11130
11142
  init_client2();
11131
11143
  init_chattab();
11132
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
+ };
11133
11151
  ensureSessionLocks = new Map;
11134
11152
  });
11135
11153
 
@@ -13141,7 +13159,7 @@ async function handleDiffRequest(req, url) {
13141
13159
  var GIT_TIMEOUT_MS = 15000;
13142
13160
 
13143
13161
  // src/web/notes.ts
13144
- 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";
13145
13163
  import { join as join11 } from "path";
13146
13164
  function notesDir() {
13147
13165
  return join11(kobeStateDir(), "notes");
@@ -13160,7 +13178,7 @@ async function handleGet(url) {
13160
13178
  try {
13161
13179
  let markdown = "";
13162
13180
  try {
13163
- markdown = await readFile9(noteFilePath(taskId), "utf8");
13181
+ markdown = await readFile8(noteFilePath(taskId), "utf8");
13164
13182
  } catch (err) {
13165
13183
  if (err.code !== "ENOENT")
13166
13184
  throw err;
@@ -18260,7 +18278,7 @@ function bumpKeymapVersion() {
18260
18278
  setKeymapVersion((v) => v + 1);
18261
18279
  }
18262
18280
  function findBinding(id) {
18263
- return KobeKeymap.find((b) => b.id === id);
18281
+ return KEYMAP_BY_ID.get(id);
18264
18282
  }
18265
18283
  function chordsOf(id) {
18266
18284
  return findBinding(id)?.keys ?? [];
@@ -18280,7 +18298,7 @@ function bindByIds(handlers) {
18280
18298
  }
18281
18299
  return out;
18282
18300
  }
18283
- var KobeKeymap, KEYMAP_DEFAULTS, keymapVersion, setKeymapVersion;
18301
+ var KobeKeymap, KEYMAP_DEFAULTS, keymapVersion, setKeymapVersion, KEYMAP_BY_ID;
18284
18302
  var init_keybindings2 = __esm(() => {
18285
18303
  init_dev();
18286
18304
  KobeKeymap = [
@@ -18493,6 +18511,14 @@ var init_keybindings2 = __esm(() => {
18493
18511
  description: "Open the update page (when a new version is available)",
18494
18512
  hint: { keys: "u", label: "update", status: false }
18495
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
+ },
18496
18522
  {
18497
18523
  id: "tasks.toggleKeys",
18498
18524
  scope: "sidebar",
@@ -18742,6 +18768,7 @@ var init_keybindings2 = __esm(() => {
18742
18768
  ];
18743
18769
  KEYMAP_DEFAULTS = new Map(KobeKeymap.map((b) => [b.id, { keys: [...b.keys], hint: b.hint ? { ...b.hint } : undefined }]));
18744
18770
  [keymapVersion, setKeymapVersion] = createSignal(0);
18771
+ KEYMAP_BY_ID = new Map(KobeKeymap.map((b) => [b.id, b]));
18745
18772
  });
18746
18773
 
18747
18774
  // src/tui/context/keybindings-user.ts
@@ -22560,40 +22587,61 @@ var init_background_poll = __esm(() => {
22560
22587
  });
22561
22588
 
22562
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
+ }
22563
22629
  function currentBranch(repo) {
22564
22630
  return poller.read(repo);
22565
22631
  }
22566
22632
  function pollCurrentBranch(repo) {
22567
22633
  poller.poll(repo);
22568
22634
  }
22569
- 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;
22570
22636
  var init_git_head = __esm(() => {
22571
22637
  init_background_poll();
22638
+ headCache = new Map;
22572
22639
  poller = createBackgroundPoller({
22573
22640
  initial: "",
22574
22641
  timeoutMs: BRANCH_POLL_TIMEOUT_MS,
22575
22642
  slowRetryMs: BRANCH_SLOW_RETRY_MS,
22576
22643
  minIntervalMs: BRANCH_MIN_POLL_INTERVAL_MS,
22577
- run: async (repo, signal) => {
22578
- const ref = await spawnCapture("git", ["symbolic-ref", "--short", "HEAD"], {
22579
- cwd: repo,
22580
- env: gitEnv(),
22581
- signal
22582
- });
22583
- if (ref.status === 0) {
22584
- const name = ref.stdout.trim();
22585
- if (name && name !== "HEAD")
22586
- return name;
22587
- }
22588
- const head = await spawnCapture("git", ["rev-parse", "--verify", "HEAD"], {
22589
- cwd: repo,
22590
- env: gitEnv(),
22591
- signal
22592
- });
22593
- if (head.status === 0)
22594
- return "(detached)";
22595
- return "";
22596
- }
22644
+ run: (repo, signal) => resolveBranchHead(repo, signal)
22597
22645
  });
22598
22646
  });
22599
22647
 
@@ -22864,6 +22912,14 @@ function buildSidebarRowView(opts) {
22864
22912
  tone
22865
22913
  };
22866
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
+ }
22867
22923
  function activityBadgeFor(state) {
22868
22924
  switch (state) {
22869
22925
  case "rate_limited":
@@ -23369,16 +23425,17 @@ function Sidebar(props) {
23369
23425
  pollCurrentBranch(task.repo);
23370
23426
  return isMain ? currentBranch(task.repo) : "";
23371
23427
  });
23372
- const rowView = createMemo(() => buildSidebarRowView({
23428
+ const baseRowView = createMemo(() => buildSidebarRowView({
23373
23429
  task,
23374
23430
  activity: props.engineState?.().get(task.id),
23375
23431
  job: props.taskJobs?.().get(task.id),
23376
23432
  live: isLive(),
23377
- spinnerFrame: spinnerFrame(),
23433
+ spinnerFrame: 0,
23378
23434
  subtitleBudget: subtitleBudget(),
23379
23435
  truncateBranch: truncateBranchLabel,
23380
23436
  mainBranch: projectBranch()
23381
23437
  }));
23438
+ const rowView = createMemo(() => withSpinnerFrame(baseRowView(), spinnerFrame));
23382
23439
  const stateColor = () => isMain && !rowView().loading ? theme.primary : toneColor(rowView().tone);
23383
23440
  const barColor = () => isCursor() ? theme.focusAccent : isSelected() ? theme.primary : undefined;
23384
23441
  const barGlyph = () => isCursor() || isSelected() ? "\u258C" : " ";
@@ -23822,6 +23879,7 @@ __export(exports_host3, {
23822
23879
  startTasksPane: () => startTasksPane
23823
23880
  });
23824
23881
  import { existsSync as existsSync16 } from "fs";
23882
+ import { stat as stat5 } from "fs/promises";
23825
23883
  import { TextAttributes as TextAttributes12 } from "@opentui/core";
23826
23884
  function worktreeCwdUsable(cwd) {
23827
23885
  return !!cwd && worktreeUsable(cwd);
@@ -24003,6 +24061,17 @@ function TasksShell(props) {
24003
24061
  notifyError(`Couldn't open worktree with ${opener.label}`);
24004
24062
  }
24005
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
+ }
24006
24075
  async function moveTask(id, delta) {
24007
24076
  const task = props.tasks().find((t) => t.id === id);
24008
24077
  if (!task || task.kind === "main" || !props.orch)
@@ -24050,7 +24119,8 @@ function TasksShell(props) {
24050
24119
  if (id)
24051
24120
  cycleVendor(id);
24052
24121
  },
24053
- "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))
24054
24124
  })
24055
24125
  }));
24056
24126
  async function switchTo(id) {
@@ -24291,6 +24361,10 @@ function ShortcutHints(props) {
24291
24361
  k: "enter",
24292
24362
  label: "open"
24293
24363
  },
24364
+ {
24365
+ k: "right",
24366
+ label: "focus engine"
24367
+ },
24294
24368
  {
24295
24369
  k: "n",
24296
24370
  label: "new task"
@@ -24451,10 +24525,21 @@ async function setupTasksPane(opts) {
24451
24525
  await store2.load();
24452
24526
  setFileTasks(store2.list());
24453
24527
  };
24528
+ let lastTasksFileFingerprint = "";
24454
24529
  const timer = setInterval(() => {
24455
24530
  if (orch && orch.connectionStateSignal()() === "online")
24456
24531
  return;
24457
- 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(() => {});
24458
24543
  }, RELOAD_MS);
24459
24544
  return {
24460
24545
  root: () => [createComponent2(TasksShell, {
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.19",
4
+ "version": "0.7.20",
5
5
  "description": "TUI orchestrator for Claude Code (codename)",
6
6
  "type": "module",
7
7
  "packageManager": "bun@1.3.13",
@@ -34,6 +34,7 @@
34
34
  "test:fast": "vitest run --passWithNoTests",
35
35
  "test:socket": "KOBE_INCLUDE_SOCKET=1 vitest run test/daemon --pool forks --minWorkers=1 --maxWorkers=1 --passWithNoTests",
36
36
  "test:behavior": "KOBE_INCLUDE_BEHAVIOR=1 vitest run test/behavior --passWithNoTests",
37
+ "bench": "vitest bench --run",
37
38
  "lint": "biome check .",
38
39
  "knip": "knip-bun",
39
40
  "postinstall": "bun run scripts/check-preview-deps.ts || true",