@sma1lboy/kobe 0.7.21 → 0.7.23

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.
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.21",
93
+ version: "0.7.23",
94
94
  description: "TUI orchestrator for Claude Code (codename)",
95
95
  type: "module",
96
96
  packageManager: "bun@1.3.13",
@@ -3891,6 +3891,18 @@ function isDaemonVersionStale(daemonVersion, clientVersion) {
3891
3891
  return false;
3892
3892
  return daemonVersion !== clientVersion;
3893
3893
  }
3894
+ function isChannelName(value) {
3895
+ return typeof value === "string" && CHANNEL_NAME_SET.has(value);
3896
+ }
3897
+ function normalizeChannelFilter(value) {
3898
+ if (!Array.isArray(value))
3899
+ return null;
3900
+ const set = new Set;
3901
+ for (const name of value)
3902
+ if (isChannelName(name))
3903
+ set.add(name);
3904
+ return set.size > 0 ? set : null;
3905
+ }
3894
3906
  function serializeTask(task) {
3895
3907
  return {
3896
3908
  id: task.id,
@@ -3912,7 +3924,7 @@ function frameToLine(frame) {
3912
3924
  return `${JSON.stringify(frame)}
3913
3925
  `;
3914
3926
  }
3915
- var DAEMON_PROTOCOL_VERSION = 3, MIN_COMPATIBLE_PROTOCOL_VERSION = 2, CHANNEL_NAMES;
3927
+ var DAEMON_PROTOCOL_VERSION = 3, MIN_COMPATIBLE_PROTOCOL_VERSION = 2, CHANNEL_NAMES, CHANNEL_NAME_SET;
3916
3928
  var init_protocol = __esm(() => {
3917
3929
  CHANNEL_NAMES = [
3918
3930
  "task.snapshot",
@@ -3924,6 +3936,7 @@ var init_protocol = __esm(() => {
3924
3936
  "task.jobs",
3925
3937
  "worktree.changes"
3926
3938
  ];
3939
+ CHANNEL_NAME_SET = new Set(CHANNEL_NAMES);
3927
3940
  });
3928
3941
 
3929
3942
  // ../kobe-daemon/src/daemon/paths.ts
@@ -4685,7 +4698,7 @@ async function probeBinary(probe2) {
4685
4698
  return { found: false, error: err instanceof Error ? err.message : String(err) };
4686
4699
  }
4687
4700
  }
4688
- async function detectAvailableVendors(deps = defaultDeps5) {
4701
+ async function probeAvailableVendors(deps) {
4689
4702
  const probes = [
4690
4703
  ["claude", () => deps.findClaudeBinary()],
4691
4704
  ["codex", () => deps.findCodexBinary()],
@@ -4694,6 +4707,18 @@ async function detectAvailableVendors(deps = defaultDeps5) {
4694
4707
  const detected = await Promise.all(probes.map(async ([vendor, probe2]) => (await probeBinary(probe2)).found ? vendor : null));
4695
4708
  return detected.filter((v) => v !== null);
4696
4709
  }
4710
+ function detectAvailableVendors(deps = defaultDeps5) {
4711
+ if (deps !== defaultDeps5)
4712
+ return probeAvailableVendors(deps);
4713
+ if (cachedDefaultVendors)
4714
+ return cachedDefaultVendors;
4715
+ const pending = probeAvailableVendors(deps).catch((err) => {
4716
+ cachedDefaultVendors = null;
4717
+ throw err;
4718
+ });
4719
+ cachedDefaultVendors = pending;
4720
+ return pending;
4721
+ }
4697
4722
  async function availableEngineIds(deps = defaultDeps5) {
4698
4723
  const builtins = await detectAvailableVendors(deps);
4699
4724
  return [...builtins, ...getCustomEngineIds()];
@@ -4850,7 +4875,7 @@ function hasStringDeep(value, interestingKeys, depth = 0) {
4850
4875
  }
4851
4876
  return false;
4852
4877
  }
4853
- var defaultDeps5;
4878
+ var defaultDeps5, cachedDefaultVendors = null;
4854
4879
  var init_account_detect = __esm(() => {
4855
4880
  init_repos();
4856
4881
  init_binary();
@@ -6291,6 +6316,28 @@ var init_auto_title = __esm(() => {
6291
6316
  init_registry();
6292
6317
  });
6293
6318
 
6319
+ // src/tui/panes/terminal/launch.ts
6320
+ function wrapEngineLaunch(engineCmd, remoteKey, remoteCwd) {
6321
+ if (!remoteKey)
6322
+ return engineCmd;
6323
+ const host = execHostForRepo(remoteKey);
6324
+ host.ensureReady();
6325
+ return host.wrapCommand(engineCmd, { tty: true, cwd: remoteCwd });
6326
+ }
6327
+ function inheritedEnvPrefix() {
6328
+ const parts = [];
6329
+ for (const key of ["KOBE_HOME_DIR", "KOBE_DAEMON_SOCKET_PATH", "KOBE_TMUX_SOCKET"]) {
6330
+ const value = process.env[key];
6331
+ if (value && value.length > 0)
6332
+ parts.push(`${key}=${shellQuote(value)}`);
6333
+ }
6334
+ return parts.length > 0 ? `${parts.join(" ")} ` : "";
6335
+ }
6336
+ var REMOTE_KEY_OPTION = "@kobe_remote";
6337
+ var init_launch = __esm(() => {
6338
+ init_resolve();
6339
+ });
6340
+
6294
6341
  // src/tmux/client.ts
6295
6342
  var exports_client = {};
6296
6343
  __export(exports_client, {
@@ -6590,7 +6637,7 @@ async function ensureFallbackSession() {
6590
6637
  "-P",
6591
6638
  "-F",
6592
6639
  "#{pane_id}",
6593
- keepAlive(tasksPaneCommand(kobeCliInvocation()))
6640
+ keepAlive(inheritedEnvPrefix() + tasksPaneCommand(kobeCliInvocation()))
6594
6641
  ]);
6595
6642
  const tasksPane = r1.stdout.trim();
6596
6643
  if (tasksPane) {
@@ -6617,6 +6664,7 @@ async function switchClientBeforeKill(killedName, nextSessionName) {
6617
6664
  var KOBE_TMUX_SOCKET, SAFE_SPAWN_CWD, PANE_ROLE_OPTION = "@kobe_role", CLAUDE_ROLE_OPTION, CLAUDE_ROLE_VALUE = "claude", CHAT_TAB_SESSION_ID_OPTION = "@kobe_session_id", KOBE_HOME_SESSION = "kobe-home", HOME_KIND_OPTION = "@kobe_home";
6618
6665
  var init_client2 = __esm(() => {
6619
6666
  init_invocation();
6667
+ init_launch();
6620
6668
  KOBE_TMUX_SOCKET = process.env.KOBE_TMUX_SOCKET?.trim() || "kobe";
6621
6669
  SAFE_SPAWN_CWD = homedir11() || "/";
6622
6670
  CLAUDE_ROLE_OPTION = PANE_ROLE_OPTION;
@@ -6770,11 +6818,13 @@ async function runAutoTitlePass(orch, derive = deriveTitleFromSession) {
6770
6818
  }
6771
6819
  return renamed;
6772
6820
  }
6773
- function startAutoTitlePoller(orch, intervalMs = DEFAULT_AUTO_TITLE_POLL_MS) {
6821
+ function startAutoTitlePoller(orch, intervalMs = DEFAULT_AUTO_TITLE_POLL_MS, hasSubscribers) {
6774
6822
  if (intervalMs <= 0)
6775
6823
  return () => {};
6776
6824
  let running = false;
6777
6825
  const tick = () => {
6826
+ if (hasSubscribers && !hasSubscribers())
6827
+ return;
6778
6828
  if (running)
6779
6829
  return;
6780
6830
  running = true;
@@ -7517,6 +7567,8 @@ class WorktreeChangesCollector {
7517
7567
  tick() {
7518
7568
  if (this.stopped)
7519
7569
  return;
7570
+ if (this.options.hasSubscribers && !this.options.hasSubscribers())
7571
+ return;
7520
7572
  try {
7521
7573
  const tracked = trackedWorktreePaths(this.orch.listTasks());
7522
7574
  let pruned = false;
@@ -7571,10 +7623,10 @@ class WorktreeChangesCollector {
7571
7623
  this.bus.publish("worktree.changes", { changes });
7572
7624
  }
7573
7625
  }
7574
- function startWorktreeChangesCollector(orch, bus, tickMs = DEFAULT_WORKTREE_CHANGES_TICK_MS) {
7626
+ function startWorktreeChangesCollector(orch, bus, tickMs = DEFAULT_WORKTREE_CHANGES_TICK_MS, hasSubscribers) {
7575
7627
  if (tickMs <= 0)
7576
7628
  return () => {};
7577
- const collector = new WorktreeChangesCollector(orch, bus);
7629
+ const collector = new WorktreeChangesCollector(orch, bus, { hasSubscribers });
7578
7630
  collector.tick();
7579
7631
  const timer = setInterval(() => collector.tick(), tickMs);
7580
7632
  timer.unref?.();
@@ -7617,6 +7669,12 @@ async function startDaemonServer(orch, options = {}) {
7617
7669
  n++;
7618
7670
  return n;
7619
7671
  }
7672
+ function hasSubscribers() {
7673
+ for (const c of clients)
7674
+ if (c.subscribed)
7675
+ return true;
7676
+ return false;
7677
+ }
7620
7678
  function cancelIdleTimer() {
7621
7679
  if (idleTimer) {
7622
7680
  clearTimeout(idleTimer);
@@ -7652,7 +7710,8 @@ async function startDaemonServer(orch, options = {}) {
7652
7710
  socket,
7653
7711
  buffer: "",
7654
7712
  subscribed: false,
7655
- holdsLifetime: false
7713
+ holdsLifetime: false,
7714
+ channels: null
7656
7715
  };
7657
7716
  clients.add(client);
7658
7717
  socket.on("data", (chunk) => {
@@ -7684,7 +7743,7 @@ async function startDaemonServer(orch, options = {}) {
7684
7743
  updateTimer.unref?.();
7685
7744
  }
7686
7745
  const autoTitlePollMs = options.autoTitlePollMs ?? DEFAULT_AUTO_TITLE_POLL_MS;
7687
- const stopAutoTitlePoller = startAutoTitlePoller(orch, autoTitlePollMs);
7746
+ const stopAutoTitlePoller = startAutoTitlePoller(orch, autoTitlePollMs, hasSubscribers);
7688
7747
  const stopUiPrefsWatcher = startUiPrefsWatcher(bus, {
7689
7748
  statePath: defaultUiPrefsStatePath(options.homeDir),
7690
7749
  debounceMs: options.uiPrefsDebounceMs ?? DEFAULT_UI_PREFS_DEBOUNCE_MS
@@ -7693,7 +7752,7 @@ async function startDaemonServer(orch, options = {}) {
7693
7752
  path: defaultKeybindingsPath(options.homeDir),
7694
7753
  debounceMs: options.keybindingsDebounceMs ?? DEFAULT_KEYBINDINGS_DEBOUNCE_MS
7695
7754
  });
7696
- const stopWorktreeChangesCollector = startWorktreeChangesCollector(orch, bus, options.worktreeChangesTickMs ?? DEFAULT_WORKTREE_CHANGES_TICK_MS);
7755
+ const stopWorktreeChangesCollector = startWorktreeChangesCollector(orch, bus, options.worktreeChangesTickMs ?? DEFAULT_WORKTREE_CHANGES_TICK_MS, hasSubscribers);
7697
7756
  const serverApi = {
7698
7757
  socketPath,
7699
7758
  pidPath,
@@ -7743,21 +7802,28 @@ async function startDaemonServer(orch, options = {}) {
7743
7802
  async function dispatch(req, client) {
7744
7803
  if (req.name === "subscribe") {
7745
7804
  const payload = objectPayload(req.payload);
7805
+ const wasSubscribed = client.subscribed;
7746
7806
  client.subscribed = true;
7747
7807
  const role = payload.role === "gui" ? "gui" : "pane";
7748
7808
  client.holdsLifetime = role === "gui";
7809
+ client.channels = normalizeChannelFilter(payload.channels);
7810
+ const firstSubscriber = !wasSubscribed;
7749
7811
  if (client.holdsLifetime)
7750
7812
  cancelIdleTimer();
7751
- logDaemonInfo("conn", `client #${client.id} subscribed as ${role} \u2014 ${clients.size} client(s), ${guiCount()} gui`);
7813
+ logDaemonInfo("conn", `client #${client.id} subscribed as ${role}${client.channels ? ` [${[...client.channels].join(",")}]` : ""} \u2014 ${clients.size} client(s), ${guiCount()} gui${firstSubscriber ? " (collectors resume)" : ""}`);
7752
7814
  for (const event of bus.snapshot()) {
7815
+ if (client.channels && !client.channels.has(event.channel))
7816
+ continue;
7753
7817
  writeFrame(client, { type: "event", name: event.channel, payload: event.payload });
7754
7818
  }
7755
- for (const payload2 of activity.currentNonIdle()) {
7756
- writeFrame(client, {
7757
- type: "event",
7758
- name: "engine-state",
7759
- payload: payload2
7760
- });
7819
+ if (!client.channels || client.channels.has("engine-state")) {
7820
+ for (const payload2 of activity.currentNonIdle()) {
7821
+ writeFrame(client, {
7822
+ type: "event",
7823
+ name: "engine-state",
7824
+ payload: payload2
7825
+ });
7826
+ }
7761
7827
  }
7762
7828
  return {};
7763
7829
  }
@@ -7816,10 +7882,13 @@ function writeFrame(client, frame) {
7816
7882
  client.socket.write(frameToLine(frame));
7817
7883
  }
7818
7884
  function broadcast(clients, frame) {
7885
+ const channel = frame.type === "event" && frame.name !== "daemon.stopping" ? frame.name : null;
7819
7886
  let line = null;
7820
7887
  for (const client of clients) {
7821
7888
  if (!client.subscribed && frame.type === "event")
7822
7889
  continue;
7890
+ if (channel && client.channels && !client.channels.has(channel))
7891
+ continue;
7823
7892
  line ??= frameToLine(frame);
7824
7893
  client.socket.write(line);
7825
7894
  }
@@ -10436,28 +10505,6 @@ var init_tmux_border_theme = __esm(() => {
10436
10505
  FOCUS_ACCENT_SLOT_NAMES2 = ["primary", "success", "info"];
10437
10506
  });
10438
10507
 
10439
- // src/tui/panes/terminal/launch.ts
10440
- function wrapEngineLaunch(engineCmd, remoteKey, remoteCwd) {
10441
- if (!remoteKey)
10442
- return engineCmd;
10443
- const host = execHostForRepo(remoteKey);
10444
- host.ensureReady();
10445
- return host.wrapCommand(engineCmd, { tty: true, cwd: remoteCwd });
10446
- }
10447
- function inheritedEnvPrefix() {
10448
- const parts = [];
10449
- for (const key of ["KOBE_HOME_DIR", "KOBE_DAEMON_SOCKET_PATH", "KOBE_TMUX_SOCKET"]) {
10450
- const value = process.env[key];
10451
- if (value && value.length > 0)
10452
- parts.push(`${key}=${shellQuote(value)}`);
10453
- }
10454
- return parts.length > 0 ? `${parts.join(" ")} ` : "";
10455
- }
10456
- var REMOTE_KEY_OPTION = "@kobe_remote";
10457
- var init_launch = __esm(() => {
10458
- init_resolve();
10459
- });
10460
-
10461
10508
  // src/tui/panes/terminal/pane-heal.ts
10462
10509
  function parseKobePaneRows(stdout) {
10463
10510
  const rows = [];
@@ -10480,9 +10527,6 @@ function parseKobePaneRows(stdout) {
10480
10527
  }
10481
10528
  return rows;
10482
10529
  }
10483
- function paneIdsByRole(rows, role) {
10484
- return rows.filter((row) => row.role === role).map((row) => row.paneId);
10485
- }
10486
10530
  function planPaneHeals(rows, opts) {
10487
10531
  const byWindow = new Map;
10488
10532
  for (const row of rows) {
@@ -10498,7 +10542,8 @@ function planPaneHeals(rows, opts) {
10498
10542
  if (tasksPane && (opts.force || tasksPane.version !== opts.currentVersion)) {
10499
10543
  targets.push({ role: "tasks", paneId: tasksPane.paneId });
10500
10544
  }
10501
- if (opsPane && (opts.force ? true : claudePane && opsPane.version !== opts.currentVersion)) {
10545
+ const opsNeedsRespawn = opts.force ? true : claudePane && (opts.vendorChanged || opsPane?.version !== opts.currentVersion);
10546
+ if (opsPane && opsNeedsRespawn) {
10502
10547
  targets.push({ role: "ops", paneId: opsPane.paneId, claudePaneId: claudePane ?? null });
10503
10548
  }
10504
10549
  }
@@ -10533,15 +10578,22 @@ function respawnCommandsFor(targets, args) {
10533
10578
  }
10534
10579
  return commands;
10535
10580
  }
10536
- async function relaunchEngineInAllWindows(session, cwd, command, remoteKey) {
10581
+ async function relaunchEngineInAllWindows(session, cwd, command, remoteKey, vendor) {
10537
10582
  const rows = await listKobePanes(session);
10538
10583
  if (!rows)
10539
10584
  return false;
10540
- const enginePanes = paneIdsByRole(rows, "claude");
10585
+ const enginePanes = rows.filter((r) => r.role === "claude");
10541
10586
  if (enginePanes.length === 0)
10542
10587
  return false;
10543
- const cmd = keepAlive(wrapEngineLaunch(shellQuoteArgv(command), remoteKey, cwd));
10544
- await runTmuxSequence(enginePanes.map((pane) => ["respawn-pane", "-k", "-c", localSpawnCwd(cwd), "-t", pane, cmd]));
10588
+ const localCwd = localSpawnCwd(cwd);
10589
+ const commands = [];
10590
+ for (const pane of enginePanes) {
10591
+ const launch = withClaudeSessionId(command, vendor);
10592
+ const cmd = keepAlive(wrapEngineLaunch(shellQuoteArgv(launch.argv), remoteKey, cwd));
10593
+ commands.push(["respawn-pane", "-k", "-c", localCwd, "-t", pane.paneId, cmd]);
10594
+ commands.push(launch.sessionId ? ["set-window-option", "-t", pane.paneId, CHAT_TAB_SESSION_ID_OPTION, launch.sessionId] : ["set-window-option", "-u", "-t", pane.paneId, CHAT_TAB_SESSION_ID_OPTION]);
10595
+ }
10596
+ await runTmuxSequence(commands);
10545
10597
  return true;
10546
10598
  }
10547
10599
  async function globalLayoutPrefs() {
@@ -10583,7 +10635,11 @@ async function healWorkspaceLayout(session, versions) {
10583
10635
  }
10584
10636
  }
10585
10637
  if (versions) {
10586
- commands.push(...respawnCommandsFor(planPaneHeals(rows, { currentVersion: CURRENT_VERSION, force: false }), versions));
10638
+ commands.push(...respawnCommandsFor(planPaneHeals(rows, {
10639
+ currentVersion: CURRENT_VERSION,
10640
+ force: false,
10641
+ vendorChanged: versions.vendorChanged
10642
+ }), versions));
10587
10643
  }
10588
10644
  if (commands.length > 0)
10589
10645
  await runTmuxSequence(commands);
@@ -10650,6 +10706,7 @@ async function refreshKobeWorkspacePanes(session) {
10650
10706
  var PANE_VERSION_OPTION = "@kobe_pane_version", KOBE_PANE_LIST_FORMAT;
10651
10707
  var init_pane_heal = __esm(() => {
10652
10708
  init_invocation();
10709
+ init_interactive_command();
10653
10710
  init_resolve();
10654
10711
  init_client2();
10655
10712
  init_tmux_border_theme();
@@ -10988,10 +11045,15 @@ async function ensureSessionImpl(opts) {
10988
11045
  return true;
10989
11046
  }
10990
11047
  if (action.kind === "respawn-engine") {
10991
- if (await relaunchEngineInAllWindows(opts.name, opts.cwd, opts.command, remoteKey)) {
11048
+ if (await relaunchEngineInAllWindows(opts.name, opts.cwd, opts.command, remoteKey, opts.vendor)) {
10992
11049
  if (opts.vendor)
10993
11050
  await setSessionOption(opts.name, "@kobe_vendor", opts.vendor);
10994
- await healWorkspaceLayout(opts.name, { cwd: opts.cwd, taskId: opts.taskId, vendor: opts.vendor });
11051
+ await healWorkspaceLayout(opts.name, {
11052
+ cwd: opts.cwd,
11053
+ taskId: opts.taskId,
11054
+ vendor: opts.vendor,
11055
+ vendorChanged: true
11056
+ });
10995
11057
  return true;
10996
11058
  }
10997
11059
  }
@@ -11643,6 +11705,23 @@ async function setActive(ctx) {
11643
11705
  await daemon.request("task.setActive", { taskId });
11644
11706
  return { ok: true, activeTaskId: taskId };
11645
11707
  }
11708
+ async function archive(ctx) {
11709
+ const daemon = daemonOf(ctx);
11710
+ const taskId = ctx.args.require("task-id");
11711
+ const archived = ctx.args.bool("archived") ?? true;
11712
+ const res = await daemon.request("task.archive", { taskId, archived });
11713
+ if (archived)
11714
+ await ctx.runtime.tearDownSession(taskId);
11715
+ return res;
11716
+ }
11717
+ async function deleteTask(ctx) {
11718
+ const daemon = daemonOf(ctx);
11719
+ const taskId = ctx.args.require("task-id");
11720
+ const force = ctx.args.bool("force") ?? false;
11721
+ const res = await daemon.request("task.delete", { taskId, force });
11722
+ await ctx.runtime.tearDownSession(taskId);
11723
+ return res;
11724
+ }
11646
11725
  async function adopt(ctx) {
11647
11726
  const daemon = daemonOf(ctx);
11648
11727
  const { args } = ctx;
@@ -12006,10 +12085,7 @@ var init_api_cmd = __esm(() => {
12006
12085
  F.taskId(),
12007
12086
  { name: "archived", type: "bool", default: "true", description: "true to archive, false to unarchive." }
12008
12087
  ],
12009
- handler: (ctx) => simpleRpc(ctx, "task.archive", {
12010
- taskId: ctx.args.require("task-id"),
12011
- archived: ctx.args.bool("archived") ?? true
12012
- })
12088
+ handler: archive
12013
12089
  },
12014
12090
  {
12015
12091
  name: "pin",
@@ -12039,10 +12115,7 @@ var init_api_cmd = __esm(() => {
12039
12115
  name: "delete",
12040
12116
  summary: "Permanently remove a task (and its worktree). DESTRUCTIVE \u2014 prefer `archive`. Needs --force on a dirty worktree.",
12041
12117
  flags: [F.taskId(), { name: "force", type: "bool", description: "Delete even with uncommitted changes." }],
12042
- handler: (ctx) => simpleRpc(ctx, "task.delete", {
12043
- taskId: ctx.args.require("task-id"),
12044
- force: ctx.args.bool("force") ?? false
12045
- })
12118
+ handler: deleteTask
12046
12119
  },
12047
12120
  {
12048
12121
  name: "discover-adoptable",
@@ -12086,7 +12159,12 @@ var init_api_cmd = __esm(() => {
12086
12159
  isTaskRunning: (taskId) => sessionExists(tmuxSessionName(taskId)),
12087
12160
  deliverPrompt: (client, target, prompt) => deliverPrompt(client, target, prompt),
12088
12161
  resolveRepoRoot: async (absPath) => (await Promise.resolve().then(() => (init_repos(), exports_repos))).resolveRepoRoot(absPath),
12089
- readWorktreeChanges: async (worktreePath) => (await Promise.resolve().then(() => (init_worktree_changes(), exports_worktree_changes))).readWorktreeChanges(worktreePath)
12162
+ readWorktreeChanges: async (worktreePath) => (await Promise.resolve().then(() => (init_worktree_changes(), exports_worktree_changes))).readWorktreeChanges(worktreePath),
12163
+ tearDownSession: async (taskId) => {
12164
+ const session = tmuxSessionName(taskId);
12165
+ await switchClientBeforeKill(session).catch(() => {});
12166
+ await killSession(session).catch(() => {});
12167
+ }
12090
12168
  };
12091
12169
  });
12092
12170
 
@@ -13071,6 +13149,19 @@ function unquoteGitPath(p) {
13071
13149
  return inner.replace(/\\([\\"nt])/g, (_m, c) => c === "n" ? `
13072
13150
  ` : c === "t" ? "\t" : c);
13073
13151
  }
13152
+ async function mapPool(items, limit, fn) {
13153
+ const results = new Array(items.length);
13154
+ const width = Math.max(1, Math.min(limit, items.length));
13155
+ let next = 0;
13156
+ async function worker() {
13157
+ while (next < items.length) {
13158
+ const index = next++;
13159
+ results[index] = await fn(items[index], index);
13160
+ }
13161
+ }
13162
+ await Promise.all(Array.from({ length: width }, () => worker()));
13163
+ return results;
13164
+ }
13074
13165
  async function untrackedPatch(cwd, relPath) {
13075
13166
  const res = await runGit(cwd, ["diff", "--no-color", "--no-index", "--", "/dev/null", relPath]);
13076
13167
  if (res.stdout.trim())
@@ -13120,6 +13211,7 @@ async function handleDiffRequest(req, url) {
13120
13211
  const stagedByFile = splitDiffByFile(staged.stdout);
13121
13212
  const records = statusRes.stdout.split("\x00");
13122
13213
  const files = [];
13214
+ const untrackedJobs = [];
13123
13215
  for (let i = 0;i < records.length; i++) {
13124
13216
  const rec = records[i];
13125
13217
  if (!rec)
@@ -13136,7 +13228,7 @@ async function handleDiffRequest(req, url) {
13136
13228
  const staged2 = x !== " " && x !== "?";
13137
13229
  let patch = "";
13138
13230
  if (untracked) {
13139
- patch = await untrackedPatch(worktreePath, path11);
13231
+ untrackedJobs.push({ fileIndex: files.length, path: path11 });
13140
13232
  } else {
13141
13233
  patch = stagedByFile.get(path11) ?? unstagedByFile.get(path11) ?? "";
13142
13234
  if (stagedByFile.has(path11) && unstagedByFile.has(path11)) {
@@ -13150,13 +13242,75 @@ async function handleDiffRequest(req, url) {
13150
13242
  patch
13151
13243
  });
13152
13244
  }
13245
+ if (untrackedJobs.length > 0) {
13246
+ const patches = await mapPool(untrackedJobs, UNTRACKED_DIFF_CONCURRENCY, (job) => untrackedPatch(worktreePath, job.path));
13247
+ for (let j = 0;j < untrackedJobs.length; j++) {
13248
+ files[untrackedJobs[j].fileIndex].patch = patches[j];
13249
+ }
13250
+ }
13153
13251
  files.sort((a, b) => a.path.localeCompare(b.path));
13154
13252
  const raw = [staged.stdout, unstaged.stdout].filter(Boolean).join(`
13155
13253
  `);
13156
13254
  const result = { files, raw };
13157
13255
  return Response.json(result);
13158
13256
  }
13159
- var GIT_TIMEOUT_MS = 15000;
13257
+ var GIT_TIMEOUT_MS = 15000, UNTRACKED_DIFF_CONCURRENCY = 8;
13258
+
13259
+ // src/web/history.ts
13260
+ import { isAbsolute } from "path";
13261
+ function isSafeVendor(value) {
13262
+ return typeof value === "string" && value.length > 0 && /^[A-Za-z0-9_-]+$/.test(value);
13263
+ }
13264
+ function isSafeSessionId(value) {
13265
+ return typeof value === "string" && value.length > 0 && /^[A-Za-z0-9._-]+$/.test(value) && !value.includes("..");
13266
+ }
13267
+ async function handleSessions(url) {
13268
+ const worktreePath = url.searchParams.get("worktreePath");
13269
+ const vendor = url.searchParams.get("vendor") ?? "claude";
13270
+ if (!worktreePath || !isAbsolute(worktreePath)) {
13271
+ return Response.json({ error: "worktreePath must be an absolute path" }, { status: 400 });
13272
+ }
13273
+ if (!isSafeVendor(vendor)) {
13274
+ return Response.json({ error: "invalid vendor" }, { status: 400 });
13275
+ }
13276
+ try {
13277
+ const reader = engineEntry(vendor).history;
13278
+ const [sessions, latestMtime] = await Promise.all([
13279
+ reader.listSessionIdsForWorktree(worktreePath),
13280
+ reader.latestTranscriptMtimeForWorktree(worktreePath)
13281
+ ]);
13282
+ return Response.json({ sessions, latestMtime });
13283
+ } catch (err) {
13284
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 });
13285
+ }
13286
+ }
13287
+ async function handleMessages(url) {
13288
+ const vendor = url.searchParams.get("vendor") ?? "claude";
13289
+ const sessionId = url.searchParams.get("sessionId");
13290
+ if (!isSafeVendor(vendor)) {
13291
+ return Response.json({ error: "invalid vendor" }, { status: 400 });
13292
+ }
13293
+ if (!isSafeSessionId(sessionId)) {
13294
+ return Response.json({ error: "invalid sessionId" }, { status: 400 });
13295
+ }
13296
+ try {
13297
+ const messages = await engineEntry(vendor).history.readHistory(sessionId);
13298
+ return Response.json({ messages });
13299
+ } catch (err) {
13300
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 });
13301
+ }
13302
+ }
13303
+ async function handleHistoryRequest(req, url) {
13304
+ if (url.pathname !== SESSIONS_ROUTE && url.pathname !== MESSAGES_ROUTE)
13305
+ return null;
13306
+ if (req.method !== "GET")
13307
+ return Response.json({ error: "method not allowed" }, { status: 405 });
13308
+ return url.pathname === SESSIONS_ROUTE ? handleSessions(url) : handleMessages(url);
13309
+ }
13310
+ var SESSIONS_ROUTE = "/api/history/sessions", MESSAGES_ROUTE = "/api/history/messages";
13311
+ var init_history4 = __esm(() => {
13312
+ init_registry();
13313
+ });
13160
13314
 
13161
13315
  // src/web/notes.ts
13162
13316
  import { mkdir as mkdir7, readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
@@ -13223,6 +13377,104 @@ var init_notes = __esm(() => {
13223
13377
  init_env();
13224
13378
  });
13225
13379
 
13380
+ // src/web/themes.ts
13381
+ function resolveHex(themeJson, value, chain = []) {
13382
+ if (value === undefined)
13383
+ return null;
13384
+ if (typeof value !== "string")
13385
+ return resolveHex(themeJson, value.dark, chain);
13386
+ if (value === "transparent" || value === "none")
13387
+ return null;
13388
+ if (value.startsWith("#"))
13389
+ return value;
13390
+ if (chain.includes(value))
13391
+ return null;
13392
+ const next = themeJson.defs?.[value] ?? themeJson.theme[value];
13393
+ return resolveHex(themeJson, next, [...chain, value]);
13394
+ }
13395
+ function clampByte(n) {
13396
+ return Math.max(0, Math.min(255, Math.round(n)));
13397
+ }
13398
+ function mix(a, b, t) {
13399
+ const pa = Number.parseInt(a.slice(1), 16);
13400
+ const pb = Number.parseInt(b.slice(1), 16);
13401
+ const ch = (p, shift) => p >> shift & 255;
13402
+ const out = clampByte(ch(pa, 16) + (ch(pb, 16) - ch(pa, 16)) * t) << 16 | clampByte(ch(pa, 8) + (ch(pb, 8) - ch(pa, 8)) * t) << 8 | clampByte(ch(pa, 0) + (ch(pb, 0) - ch(pa, 0)) * t);
13403
+ return `#${out.toString(16).padStart(6, "0")}`;
13404
+ }
13405
+ function toWebPalette(themeJson) {
13406
+ const slot = (name) => resolveHex(themeJson, themeJson.theme[name]);
13407
+ const bg = slot("background");
13408
+ const fg = slot("text");
13409
+ if (!bg || !fg)
13410
+ return null;
13411
+ const muted = slot("textMuted") ?? mix(fg, bg, 0.35);
13412
+ const primary = slot("primary") ?? fg;
13413
+ const line = slot("border") ?? mix(fg, bg, 0.8);
13414
+ return {
13415
+ bg,
13416
+ surface: slot("backgroundPanel") ?? bg,
13417
+ inset: slot("backgroundElement") ?? bg,
13418
+ menu: slot("backgroundMenu") ?? slot("backgroundElement") ?? bg,
13419
+ line,
13420
+ "line-subtle": slot("borderSubtle") ?? line,
13421
+ "line-active": slot("borderActive") ?? mix(line, fg, 0.3),
13422
+ fg,
13423
+ muted,
13424
+ subtle: mix(muted, bg, 0.35),
13425
+ primary,
13426
+ "primary-hover": mix(primary, fg, 0.35),
13427
+ "kobe-orange": primary,
13428
+ "kobe-green": slot("success") ?? fg,
13429
+ "kobe-blue": slot("info") ?? fg,
13430
+ "kobe-red": slot("error") ?? fg,
13431
+ "kobe-yellow": slot("warning") ?? fg,
13432
+ "kobe-violet": slot("secondary") ?? primary
13433
+ };
13434
+ }
13435
+ function handleThemesRequest(req, url) {
13436
+ if (url.pathname !== THEMES_ROUTE)
13437
+ return null;
13438
+ if (req.method !== "GET")
13439
+ return Response.json({ error: "method not allowed" }, { status: 405 });
13440
+ return Response.json({ themes: WEB_THEMES });
13441
+ }
13442
+ var THEME_JSONS, WEB_THEMES, THEMES_ROUTE = "/api/themes";
13443
+ var init_themes = __esm(() => {
13444
+ init_claude();
13445
+ init_conductor();
13446
+ init_dracula();
13447
+ init_nord();
13448
+ init_opencode();
13449
+ init_osaka_jade();
13450
+ init_tokyonight();
13451
+ THEME_JSONS = {
13452
+ claude: claude_default,
13453
+ conductor: conductor_default,
13454
+ dracula: dracula_default,
13455
+ nord: nord_default,
13456
+ opencode: opencode_default,
13457
+ "osaka-jade": osaka_jade_default,
13458
+ tokyonight: tokyonight_default
13459
+ };
13460
+ WEB_THEMES = Object.fromEntries(Object.entries(THEME_JSONS).map(([name, json]) => [name, toWebPalette(json)]).filter((entry) => entry[1] !== null));
13461
+ });
13462
+
13463
+ // ../kobe-web/server/spa-channels.ts
13464
+ var SPA_CHANNELS, SPA_CHANNEL_SET;
13465
+ var init_spa_channels = __esm(() => {
13466
+ SPA_CHANNELS = [
13467
+ "task.snapshot",
13468
+ "active-task",
13469
+ "engine-state",
13470
+ "update",
13471
+ "task.jobs",
13472
+ "worktree.changes",
13473
+ "ui-prefs"
13474
+ ];
13475
+ SPA_CHANNEL_SET = new Set(SPA_CHANNELS);
13476
+ });
13477
+
13226
13478
  // ../kobe-web/server/daemon-link.ts
13227
13479
  function sleep2(ms) {
13228
13480
  return new Promise((resolve6) => setTimeout(resolve6, ms));
@@ -13239,6 +13491,9 @@ class DaemonLink {
13239
13491
  activeTaskId = null;
13240
13492
  engineStates = {};
13241
13493
  update = null;
13494
+ jobs = {};
13495
+ worktreeChanges = {};
13496
+ uiPrefs = null;
13242
13497
  async start() {
13243
13498
  await this.connectOnce(true);
13244
13499
  }
@@ -13248,6 +13503,9 @@ class DaemonLink {
13248
13503
  activeTaskId: this.activeTaskId,
13249
13504
  engineStates: this.engineStates,
13250
13505
  update: this.update,
13506
+ jobs: this.jobs,
13507
+ worktreeChanges: this.worktreeChanges,
13508
+ uiPrefs: this.uiPrefs,
13251
13509
  connected: this.connected
13252
13510
  };
13253
13511
  }
@@ -13289,9 +13547,10 @@ class DaemonLink {
13289
13547
  if (hello.tasks)
13290
13548
  this.tasks = hello.tasks;
13291
13549
  this.engineStates = {};
13550
+ this.jobs = {};
13292
13551
  client.on("*", (frame) => this.onFrame(frame.name, frame.payload));
13293
13552
  client.onLifecycle("close", () => this.onDrop(client));
13294
- await client.subscribe({ role: "gui" });
13553
+ await client.subscribe({ role: "gui", channels: SPA_CHANNELS });
13295
13554
  } catch (err) {
13296
13555
  client.close();
13297
13556
  throw err;
@@ -13322,9 +13581,16 @@ class DaemonLink {
13322
13581
  }
13323
13582
  onFrame(name, payload) {
13324
13583
  switch (name) {
13325
- case "task.snapshot":
13326
- this.tasks = payload.tasks;
13584
+ case "task.snapshot": {
13585
+ const tasks = payload.tasks;
13586
+ this.tasks = tasks;
13587
+ const live = new Set(tasks.map((t) => t.id));
13588
+ const kept = Object.entries(this.engineStates).filter(([id]) => live.has(id));
13589
+ if (kept.length !== Object.keys(this.engineStates).length) {
13590
+ this.engineStates = Object.fromEntries(kept);
13591
+ }
13327
13592
  break;
13593
+ }
13328
13594
  case "active-task":
13329
13595
  this.activeTaskId = payload.taskId;
13330
13596
  break;
@@ -13336,11 +13602,29 @@ class DaemonLink {
13336
13602
  case "update":
13337
13603
  this.update = payload.info;
13338
13604
  break;
13605
+ case "task.jobs": {
13606
+ const job = payload;
13607
+ if (job.phase === "running") {
13608
+ this.jobs = { ...this.jobs, [job.taskId]: job };
13609
+ } else {
13610
+ const { [job.taskId]: _done, ...rest } = this.jobs;
13611
+ this.jobs = rest;
13612
+ }
13613
+ break;
13614
+ }
13615
+ case "worktree.changes":
13616
+ this.worktreeChanges = payload.changes;
13617
+ break;
13618
+ case "ui-prefs":
13619
+ this.uiPrefs = payload;
13620
+ break;
13339
13621
  case "daemon.stopping":
13340
13622
  return;
13341
13623
  default:
13342
13624
  break;
13343
13625
  }
13626
+ if (!SPA_CHANNEL_SET.has(name))
13627
+ return;
13344
13628
  const event = { channel: name, payload };
13345
13629
  for (const sink of this.eventSinks)
13346
13630
  sink(event);
@@ -13359,6 +13643,32 @@ var init_daemon_link = __esm(() => {
13359
13643
  init_daemon_process();
13360
13644
  init_paths2();
13361
13645
  init_protocol();
13646
+ init_spa_channels();
13647
+ });
13648
+
13649
+ // ../kobe-web/server/rpc-allowlist.ts
13650
+ var WEB_RPC_ALLOWLIST, WEB_RPC_ALLOWSET;
13651
+ var init_rpc_allowlist = __esm(() => {
13652
+ WEB_RPC_ALLOWLIST = [
13653
+ "daemon.status",
13654
+ "task.list",
13655
+ "task.get",
13656
+ "task.create",
13657
+ "task.archive",
13658
+ "task.rename",
13659
+ "task.setBranch",
13660
+ "task.setVendor",
13661
+ "task.delete",
13662
+ "task.pin",
13663
+ "task.move",
13664
+ "task.status",
13665
+ "task.ensureMain",
13666
+ "task.ensureWorktree",
13667
+ "task.setActive",
13668
+ "worktree.discoverAdoptable",
13669
+ "worktree.adopt"
13670
+ ];
13671
+ WEB_RPC_ALLOWSET = new Set(WEB_RPC_ALLOWLIST);
13362
13672
  });
13363
13673
 
13364
13674
  // ../kobe-web/server/session.ts
@@ -13411,9 +13721,15 @@ async function terminalSpec(link, taskId) {
13411
13721
  const shell = process.env.SHELL?.trim() || "/bin/zsh";
13412
13722
  return { cwd: worktreePath, command: [shell, "-il"] };
13413
13723
  }
13724
+ async function tearDownTaskSession(taskId) {
13725
+ const session = tmuxSessionName(taskId);
13726
+ await switchClientBeforeKill(session).catch(() => {});
13727
+ await killSession(session).catch(() => {});
13728
+ }
13414
13729
  var init_session = __esm(() => {
13415
13730
  init_interactive_command();
13416
13731
  init_repo_init();
13732
+ init_client2();
13417
13733
  init_tmux();
13418
13734
  });
13419
13735
 
@@ -13457,20 +13773,35 @@ data: ${JSON.stringify(data)}
13457
13773
  }
13458
13774
  });
13459
13775
  }
13460
- async function rpcResponse(req, link) {
13776
+ async function rpcResponse(req, link, tearDown) {
13461
13777
  try {
13462
13778
  const { name, payload } = await req.json();
13463
13779
  if (!name)
13464
13780
  return Response.json({ error: "missing rpc name" }, { status: 400 });
13465
- if (name === "hello" || name === "subscribe" || name === "daemon.stop") {
13466
- throw new Error(`rpc ${name} is not exposed to the web UI`);
13781
+ if (!WEB_RPC_ALLOWSET.has(name)) {
13782
+ return Response.json({ error: `rpc ${name} is not exposed to the web UI` }, { status: 403 });
13467
13783
  }
13468
13784
  const result = await link.request(name, payload);
13785
+ const taskId = payload?.taskId;
13786
+ if (typeof taskId === "string") {
13787
+ const archiving = name === "task.archive" && payload.archived !== false;
13788
+ if (name === "task.delete" || archiving)
13789
+ tearDown(taskId);
13790
+ }
13469
13791
  return Response.json({ result });
13470
13792
  } catch (err) {
13471
13793
  return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 });
13472
13794
  }
13473
13795
  }
13796
+ async function enginesResponse() {
13797
+ try {
13798
+ const ids = await availableEngineIds();
13799
+ const engines = ids.map((id) => ({ id, label: engineDisplayName(id) }));
13800
+ return Response.json({ engines: engines.length > 0 ? engines : [{ id: "claude", label: "Claude" }] });
13801
+ } catch (err) {
13802
+ return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 });
13803
+ }
13804
+ }
13474
13805
  async function sessionResponse(req, link) {
13475
13806
  try {
13476
13807
  const { taskId } = await req.json();
@@ -13491,6 +13822,49 @@ async function specResponse(url, link, spec) {
13491
13822
  return Response.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 });
13492
13823
  }
13493
13824
  }
13825
+ function createRequestHandler(deps) {
13826
+ const { link, sseSends, staticDir } = deps;
13827
+ const tearDown = deps.tearDownSession ?? ((taskId) => void tearDownTaskSession(taskId));
13828
+ return async function handle(req) {
13829
+ const url = new URL(req.url);
13830
+ if (url.pathname === WEB_HEALTH_PATH)
13831
+ return new Response(WEB_HEALTH_MARKER);
13832
+ if (url.pathname === "/events") {
13833
+ return sseResponse((send2) => {
13834
+ send2("snapshot", link.snapshot());
13835
+ sseSends.add(send2);
13836
+ return () => {
13837
+ sseSends.delete(send2);
13838
+ };
13839
+ });
13840
+ }
13841
+ if (url.pathname === "/api/rpc" && req.method === "POST")
13842
+ return rpcResponse(req, link, tearDown);
13843
+ if (url.pathname === "/api/session" && req.method === "POST")
13844
+ return sessionResponse(req, link);
13845
+ if (url.pathname === "/api/engine-spec" && req.method === "GET")
13846
+ return specResponse(url, link, engineSpec);
13847
+ if (url.pathname === "/api/terminal-spec" && req.method === "GET")
13848
+ return specResponse(url, link, terminalSpec);
13849
+ if (url.pathname === "/api/engines" && req.method === "GET")
13850
+ return enginesResponse();
13851
+ const notes = await handleNotesRequest(req, url);
13852
+ if (notes)
13853
+ return notes;
13854
+ const diff = await handleDiffRequest(req, url);
13855
+ if (diff)
13856
+ return diff;
13857
+ const history = await handleHistoryRequest(req, url);
13858
+ if (history)
13859
+ return history;
13860
+ const themes = handleThemesRequest(req, url);
13861
+ if (themes)
13862
+ return themes;
13863
+ if (staticDir)
13864
+ return staticResponse(url.pathname, staticDir);
13865
+ return new Response("not found", { status: 404 });
13866
+ };
13867
+ }
13494
13868
  async function staticResponse(pathname, staticDir) {
13495
13869
  const rel = pathname === "/" ? "/index.html" : pathname;
13496
13870
  const resolved = normalize2(join12(staticDir, rel));
@@ -13557,41 +13931,9 @@ async function createBridgeServer(opts = {}) {
13557
13931
  for (const send2 of sseSends)
13558
13932
  send2("snapshot", link.snapshot());
13559
13933
  });
13560
- const server = Bun.serve({
13561
- port,
13562
- idleTimeout: 0,
13563
- async fetch(req) {
13564
- const url = new URL(req.url);
13565
- if (url.pathname === WEB_HEALTH_PATH)
13566
- return new Response(WEB_HEALTH_MARKER);
13567
- if (url.pathname === "/events") {
13568
- return sseResponse((send2) => {
13569
- send2("snapshot", link.snapshot());
13570
- sseSends.add(send2);
13571
- return () => {
13572
- sseSends.delete(send2);
13573
- };
13574
- });
13575
- }
13576
- if (url.pathname === "/api/rpc" && req.method === "POST")
13577
- return rpcResponse(req, link);
13578
- if (url.pathname === "/api/session" && req.method === "POST")
13579
- return sessionResponse(req, link);
13580
- if (url.pathname === "/api/engine-spec" && req.method === "GET")
13581
- return specResponse(url, link, engineSpec);
13582
- if (url.pathname === "/api/terminal-spec" && req.method === "GET")
13583
- return specResponse(url, link, terminalSpec);
13584
- const notes = await handleNotesRequest(req, url);
13585
- if (notes)
13586
- return notes;
13587
- const diff = await handleDiffRequest(req, url);
13588
- if (diff)
13589
- return diff;
13590
- if (staticDir)
13591
- return staticResponse(url.pathname, staticDir);
13592
- return new Response("not found", { status: 404 });
13593
- }
13594
- });
13934
+ const handle = createRequestHandler({ link, sseSends, staticDir });
13935
+ const hostname = process.env.KOBE_WEB_HOST?.trim() || "127.0.0.1";
13936
+ const server = Bun.serve({ port, hostname, idleTimeout: 0, fetch: handle });
13595
13937
  return {
13596
13938
  port: server.port ?? port,
13597
13939
  close() {
@@ -13602,8 +13944,13 @@ async function createBridgeServer(opts = {}) {
13602
13944
  }
13603
13945
  var WEB_HEALTH_MARKER = "kobe-web", WEB_HEALTH_PATH = "/__kobe_web";
13604
13946
  var init_bridge = __esm(() => {
13947
+ init_account_detect();
13948
+ init_interactive_command();
13949
+ init_history4();
13605
13950
  init_notes();
13951
+ init_themes();
13606
13952
  init_daemon_link();
13953
+ init_rpc_allowlist();
13607
13954
  init_session();
13608
13955
  });
13609
13956
 
@@ -13618,8 +13965,13 @@ __export(exports_web_cmd, {
13618
13965
  runWebSubcommand: () => runWebSubcommand
13619
13966
  });
13620
13967
  import { existsSync as existsSync12 } from "fs";
13968
+ import { homedir as homedir17 } from "os";
13621
13969
  import { resolve as resolve6 } from "path";
13622
13970
  import { fileURLToPath as fileURLToPath3 } from "url";
13971
+ function homeLabel() {
13972
+ const explicit = process.env.KOBE_HOME_DIR?.trim();
13973
+ return explicit ? `sandbox: ${explicit}` : `${homedir17()}/.kobe (production)`;
13974
+ }
13623
13975
  function resolveStaticDir() {
13624
13976
  const here = fileURLToPath3(import.meta.url);
13625
13977
  const candidates = [
@@ -13737,10 +14089,14 @@ async function runWebSubcommand(args) {
13737
14089
  };
13738
14090
  if (bridgeOnly) {
13739
14091
  process.stdout.write(`kobe web bridge listening on http://localhost:${bridge.port} (routes only)
14092
+ `);
14093
+ process.stdout.write(` home: ${homeLabel()}
13740
14094
  `);
13741
14095
  } else {
13742
14096
  pty = await startPtyServer({ webPort: bridge.port, takeover });
13743
14097
  process.stdout.write(`kobe web \u2192 http://localhost:${bridge.port}
14098
+ `);
14099
+ process.stdout.write(` home: ${homeLabel()}
13744
14100
  `);
13745
14101
  if (!pty) {
13746
14102
  process.stderr.write(`kobe web: PTY server not found; terminal tabs will be unavailable
@@ -13890,17 +14246,29 @@ var init_skill_cmd = __esm(() => {
13890
14246
  var exports_hook_cmd = {};
13891
14247
  __export(exports_hook_cmd, {
13892
14248
  runHookSubcommand: () => runHookSubcommand,
14249
+ readTextWithTimeout: () => readTextWithTimeout,
13893
14250
  parseWorktreeAddPath: () => parseWorktreeAddPath,
13894
14251
  ensureGlobalKobeHooks: () => ensureGlobalKobeHooks
13895
14252
  });
13896
- import { homedir as homedir17 } from "os";
14253
+ import { homedir as homedir18 } from "os";
13897
14254
  import { join as join13, resolve as resolve7 } from "path";
13898
- async function readStdinPayload() {
14255
+ async function readTextWithTimeout(read, timeoutMs = STDIN_READ_TIMEOUT_MS) {
14256
+ let raceTimer;
13899
14257
  try {
13900
- const text = await Promise.race([
13901
- Bun.stdin.text(),
13902
- new Promise((resolve8) => setTimeout(() => resolve8(""), 500))
14258
+ return await Promise.race([
14259
+ read(),
14260
+ new Promise((resolve8) => {
14261
+ raceTimer = setTimeout(() => resolve8(""), timeoutMs);
14262
+ })
13903
14263
  ]);
14264
+ } finally {
14265
+ if (raceTimer !== undefined)
14266
+ clearTimeout(raceTimer);
14267
+ }
14268
+ }
14269
+ async function readStdinPayload() {
14270
+ try {
14271
+ const text = await readTextWithTimeout(() => Bun.stdin.text());
13904
14272
  if (!text.trim())
13905
14273
  return {};
13906
14274
  const parsed = JSON.parse(text);
@@ -14014,7 +14382,7 @@ function activityHookAdapters() {
14014
14382
  return ALL_VENDORS.map((v) => createEngineHookAdapter(v)).filter((a) => a.supportsHooks());
14015
14383
  }
14016
14384
  function globalSettingsPath() {
14017
- return join13(homedir17(), ".claude", "settings.json");
14385
+ return join13(homedir18(), ".claude", "settings.json");
14018
14386
  }
14019
14387
  function persistedSyncPath(stored) {
14020
14388
  if (!stored || stored === "off")
@@ -14066,7 +14434,7 @@ async function runHookSetup(_argv) {
14066
14434
  ].join(`
14067
14435
  `));
14068
14436
  }
14069
- var WORKTREE_CREATED_VERB = "worktree-created", SYNC_SETTING_KEY = "externalWorktreeSync";
14437
+ var STDIN_READ_TIMEOUT_MS = 500, WORKTREE_CREATED_VERB = "worktree-created", SYNC_SETTING_KEY = "externalWorktreeSync";
14070
14438
  var init_hook_cmd = __esm(() => {
14071
14439
  init_daemon_process();
14072
14440
  init_hook_adapter2();
@@ -15400,6 +15768,8 @@ class RemoteOrchestrator {
15400
15768
  setConnectionState;
15401
15769
  ensureReachable;
15402
15770
  role;
15771
+ channels;
15772
+ subscribesTasks;
15403
15773
  reconnecting = false;
15404
15774
  constructor(client, options = {}) {
15405
15775
  this.client = client;
@@ -15435,6 +15805,8 @@ class RemoteOrchestrator {
15435
15805
  this.setConnectionState = (next) => setConnectionState(() => next);
15436
15806
  this.ensureReachable = options.ensureReachable ?? ensureDaemonReachable;
15437
15807
  this.role = options.role ?? "pane";
15808
+ this.channels = options.channels;
15809
+ this.subscribesTasks = !options.channels || options.channels.includes("task.snapshot");
15438
15810
  this.client.on("*", (frame) => this.handleEvent(frame.name, frame.payload));
15439
15811
  this.client.onLifecycle("close", () => {
15440
15812
  this.setConnectionState("disconnected");
@@ -15484,9 +15856,9 @@ class RemoteOrchestrator {
15484
15856
  throw new Error(`kobe daemon is protocol v${daemonVersion} (min v${daemonMin}); this client is v${DAEMON_PROTOCOL_VERSION} (min v${MIN_COMPATIBLE_PROTOCOL_VERSION}). Restart the daemon (\`kobe daemon restart\`) or upgrade kobe.`);
15485
15857
  }
15486
15858
  this.setDaemonVersionSig(typeof hello.kobeVersion === "string" ? hello.kobeVersion : null);
15487
- if (hello.tasks)
15859
+ if (hello.tasks && this.subscribesTasks)
15488
15860
  this.setTasks(hello.tasks.map(deserializeTask));
15489
- await this.client.subscribe({ role: this.role });
15861
+ await this.client.subscribe({ role: this.role, channels: this.channels });
15490
15862
  if (hello.capabilities?.includes("worktree.changes")) {
15491
15863
  if (this.worktreeChangesAcc() === null)
15492
15864
  this.setWorktreeChangesSig(new Map);
@@ -17190,97 +17562,104 @@ function NewTaskDialogView(props) {
17190
17562
  setCloneParent(joinDrill(cloneParent(), split.base, picked));
17191
17563
  setCloneParentCursor(absoluteIndex);
17192
17564
  }
17565
+ const adoptSelectAll = () => {
17566
+ const list2 = adoptList();
17567
+ if (list2.length === 0)
17568
+ return;
17569
+ const allSelected = list2.every((w) => adoptSelected().has(w.path));
17570
+ setAdoptSelected(allSelected ? new Set : new Set(list2.map((w) => w.path)));
17571
+ };
17193
17572
  useBindings(() => ({
17194
- bindings: [{
17195
- key: "tab",
17196
- cmd: () => setField((f) => nextField(f, tab()))
17197
- }, {
17198
- key: "ctrl+]",
17199
- cmd: () => switchToTab(nextDialogTab(tab()))
17200
- }, {
17201
- key: "ctrl+[",
17202
- cmd: () => switchToTab(nextDialogTab(tab()))
17203
- }, {
17204
- key: "ctrl+e",
17205
- cmd: () => setVendor((v) => nextVendorWithin(availableVendors(), v))
17206
- }, {
17207
- key: "up",
17208
- cmd: () => {
17209
- if (cloneInFlight())
17210
- return;
17211
- if (tab() === "existing" && field() === "repo") {
17212
- const list2 = activeList();
17213
- if (list2.length === 0)
17573
+ bindings: [
17574
+ {
17575
+ key: "tab",
17576
+ cmd: () => setField((f) => nextField(f, tab()))
17577
+ },
17578
+ {
17579
+ key: "ctrl+]",
17580
+ cmd: () => switchToTab(nextDialogTab(tab()))
17581
+ },
17582
+ {
17583
+ key: "ctrl+[",
17584
+ cmd: () => switchToTab(nextDialogTab(tab()))
17585
+ },
17586
+ {
17587
+ key: "ctrl+e",
17588
+ cmd: () => setVendor((v) => nextVendorWithin(availableVendors(), v))
17589
+ },
17590
+ {
17591
+ key: "up",
17592
+ cmd: () => {
17593
+ if (cloneInFlight())
17214
17594
  return;
17215
- setRepoCursor(clampCursor(repoCursor() - 1, list2.length));
17216
- return;
17217
- }
17218
- if (tab() === "existing" && field() === "baseRef") {
17219
- const list2 = branchFiltered();
17220
- if (list2.length === 0)
17595
+ if (tab() === "existing" && field() === "repo") {
17596
+ const list2 = activeList();
17597
+ if (list2.length === 0)
17598
+ return;
17599
+ setRepoCursor(clampCursor(repoCursor() - 1, list2.length));
17221
17600
  return;
17222
- setBranchCursor(clampCursor(branchCursor() - 1, list2.length));
17223
- return;
17224
- }
17225
- if (tab() === "clone" && field() === "cloneParent") {
17226
- const list2 = cloneParentFiltered();
17227
- if (list2.length === 0)
17601
+ }
17602
+ if (tab() === "existing" && field() === "baseRef") {
17603
+ const list2 = branchFiltered();
17604
+ if (list2.length === 0)
17605
+ return;
17606
+ setBranchCursor(clampCursor(branchCursor() - 1, list2.length));
17228
17607
  return;
17229
- setCloneParentCursor(clampCursor(cloneParentCursor() - 1, list2.length));
17230
- return;
17231
- }
17232
- if (tab() === "adopt") {
17233
- const list2 = adoptList();
17234
- if (list2.length === 0)
17608
+ }
17609
+ if (tab() === "clone" && field() === "cloneParent") {
17610
+ const list2 = cloneParentFiltered();
17611
+ if (list2.length === 0)
17612
+ return;
17613
+ setCloneParentCursor(clampCursor(cloneParentCursor() - 1, list2.length));
17235
17614
  return;
17236
- setAdoptCursor(clampCursor(adoptCursor() - 1, list2.length));
17615
+ }
17616
+ if (tab() === "adopt") {
17617
+ const list2 = adoptList();
17618
+ if (list2.length === 0)
17619
+ return;
17620
+ setAdoptCursor(clampCursor(adoptCursor() - 1, list2.length));
17621
+ }
17237
17622
  }
17238
- }
17239
- }, {
17240
- key: "down",
17241
- cmd: () => {
17242
- if (cloneInFlight())
17243
- return;
17244
- if (tab() === "existing" && field() === "repo") {
17245
- const list2 = activeList();
17246
- if (list2.length === 0)
17623
+ },
17624
+ {
17625
+ key: "down",
17626
+ cmd: () => {
17627
+ if (cloneInFlight())
17247
17628
  return;
17248
- setRepoCursor(clampCursor(repoCursor() + 1, list2.length));
17249
- return;
17250
- }
17251
- if (tab() === "existing" && field() === "baseRef") {
17252
- const list2 = branchFiltered();
17253
- if (list2.length === 0)
17629
+ if (tab() === "existing" && field() === "repo") {
17630
+ const list2 = activeList();
17631
+ if (list2.length === 0)
17632
+ return;
17633
+ setRepoCursor(clampCursor(repoCursor() + 1, list2.length));
17254
17634
  return;
17255
- setBranchCursor(clampCursor(branchCursor() + 1, list2.length));
17256
- return;
17257
- }
17258
- if (tab() === "clone" && field() === "cloneParent") {
17259
- const list2 = cloneParentFiltered();
17260
- if (list2.length === 0)
17635
+ }
17636
+ if (tab() === "existing" && field() === "baseRef") {
17637
+ const list2 = branchFiltered();
17638
+ if (list2.length === 0)
17639
+ return;
17640
+ setBranchCursor(clampCursor(branchCursor() + 1, list2.length));
17261
17641
  return;
17262
- setCloneParentCursor(clampCursor(cloneParentCursor() + 1, list2.length));
17263
- return;
17264
- }
17265
- if (tab() === "adopt") {
17266
- const list2 = adoptList();
17267
- if (list2.length === 0)
17642
+ }
17643
+ if (tab() === "clone" && field() === "cloneParent") {
17644
+ const list2 = cloneParentFiltered();
17645
+ if (list2.length === 0)
17646
+ return;
17647
+ setCloneParentCursor(clampCursor(cloneParentCursor() + 1, list2.length));
17268
17648
  return;
17269
- setAdoptCursor(clampCursor(adoptCursor() + 1, list2.length));
17649
+ }
17650
+ if (tab() === "adopt") {
17651
+ const list2 = adoptList();
17652
+ if (list2.length === 0)
17653
+ return;
17654
+ setAdoptCursor(clampCursor(adoptCursor() + 1, list2.length));
17655
+ }
17270
17656
  }
17271
- }
17272
- }, {
17273
- key: "ctrl+a",
17274
- cmd: () => {
17275
- if (tab() !== "adopt")
17276
- return;
17277
- const list2 = adoptList();
17278
- if (list2.length === 0)
17279
- return;
17280
- const allSelected = list2.every((w) => adoptSelected().has(w.path));
17281
- setAdoptSelected(allSelected ? new Set : new Set(list2.map((w) => w.path)));
17282
- }
17283
- }]
17657
+ },
17658
+ ...tab() === "adopt" ? [{
17659
+ key: "ctrl+a",
17660
+ cmd: adoptSelectAll
17661
+ }] : []
17662
+ ]
17284
17663
  }));
17285
17664
  useBindings(() => ({
17286
17665
  enabled: field() === "confirm" && !cloneInFlight(),
@@ -18193,6 +18572,35 @@ var init_quick_task_composer = __esm(() => {
18193
18572
  };
18194
18573
  });
18195
18574
 
18575
+ // src/client/connect-pane-orchestrator.ts
18576
+ async function connectPaneOrchestrator(options = {}) {
18577
+ const tag = options.logTag ?? "orch-connect";
18578
+ const connect2 = options.connect ?? connectIfRunning;
18579
+ let remote = null;
18580
+ try {
18581
+ const client = await connect2();
18582
+ if (!client) {
18583
+ logClient(tag, "no daemon running \u2014 caller degrades");
18584
+ return null;
18585
+ }
18586
+ remote = new RemoteOrchestrator(client, {
18587
+ ...options.orchestratorOptions,
18588
+ channels: options.channels
18589
+ });
18590
+ await remote.init();
18591
+ return remote;
18592
+ } catch (err) {
18593
+ logClientError(tag, err);
18594
+ remote?.dispose();
18595
+ return null;
18596
+ }
18597
+ }
18598
+ var init_connect_pane_orchestrator = __esm(() => {
18599
+ init_client_log();
18600
+ init_daemon_process();
18601
+ init_remote_orchestrator();
18602
+ });
18603
+
18196
18604
  // src/tui/context/focus.tsx
18197
18605
  function FocusProvider(props) {
18198
18606
  const [focused, setFocusedSignal] = createSignal(props.initial ?? "sidebar");
@@ -18909,7 +19317,7 @@ var init_pulse = () => {};
18909
19317
  // src/tui/lib/sound.ts
18910
19318
  import { existsSync as existsSync14, mkdirSync as mkdirSync6 } from "fs";
18911
19319
  import { tmpdir as tmpdir2 } from "os";
18912
- import { basename as basename6, isAbsolute, join as join15, resolve as resolve8 } from "path";
19320
+ import { basename as basename6, isAbsolute as isAbsolute2, join as join15, resolve as resolve8 } from "path";
18913
19321
  function args(player, file, volume) {
18914
19322
  if (player === "ffplay")
18915
19323
  return [player, "-autoexit", "-nodisp", "-af", `volume=${volume}`, file];
@@ -18967,7 +19375,7 @@ function pulse(volume = 0.4) {
18967
19375
  var pulseAsset, DIR, PLAYERS, cachedPlayer, cachedPath;
18968
19376
  var init_sound = __esm(() => {
18969
19377
  init_pulse();
18970
- pulseAsset = isAbsolute(pulse_default) ? pulse_default : resolve8(import.meta.dir, pulse_default);
19378
+ pulseAsset = isAbsolute2(pulse_default) ? pulse_default : resolve8(import.meta.dir, pulse_default);
18971
19379
  DIR = join15(tmpdir2(), "kobe-sfx");
18972
19380
  PLAYERS = [
18973
19381
  "ffplay",
@@ -19195,20 +19603,12 @@ function UiPrefsSync(props) {
19195
19603
  let disposed = false;
19196
19604
  onMount(() => {
19197
19605
  (async () => {
19198
- let remote = null;
19199
- try {
19200
- const client = await connectIfRunning();
19201
- if (!client) {
19202
- logClient("ui-prefs", "no daemon \u2014 keeping boot-time visual prefs");
19203
- return;
19204
- }
19205
- remote = new RemoteOrchestrator(client);
19206
- await remote.init();
19207
- } catch (err) {
19208
- logClientError("ui-prefs", err);
19209
- remote?.dispose();
19606
+ const remote = await connectPaneOrchestrator({
19607
+ logTag: "ui-prefs",
19608
+ channels: ["ui-prefs", "keybindings"]
19609
+ });
19610
+ if (!remote)
19210
19611
  return;
19211
- }
19212
19612
  if (disposed) {
19213
19613
  remote.dispose();
19214
19614
  return;
@@ -19266,9 +19666,8 @@ var init_host_boot = __esm(() => {
19266
19666
  init_solid();
19267
19667
  init_solid();
19268
19668
  init_client_log();
19269
- init_daemon_process();
19270
19669
  init_dev();
19271
- init_remote_orchestrator();
19670
+ init_connect_pane_orchestrator();
19272
19671
  init_focus();
19273
19672
  init_keybindings_user();
19274
19673
  init_kv();
@@ -22430,9 +22829,9 @@ var init_task_actions = __esm(() => {
22430
22829
  // src/tui/lib/worktree-opener.ts
22431
22830
  import { spawn as spawn5 } from "child_process";
22432
22831
  import { existsSync as existsSync15 } from "fs";
22433
- import { basename as basename7, delimiter, isAbsolute as isAbsolute2, join as join17 } from "path";
22832
+ import { basename as basename7, delimiter, isAbsolute as isAbsolute3, join as join17 } from "path";
22434
22833
  function executableOnPath(command, env, exists) {
22435
- if (isAbsolute2(command))
22834
+ if (isAbsolute3(command))
22436
22835
  return exists(command);
22437
22836
  const pathEnv = env.PATH ?? "";
22438
22837
  for (const dir of pathEnv.split(delimiter)) {
@@ -23860,7 +24259,9 @@ var init_Sidebar = __esm(() => {
23860
24259
  // src/tui/tasks-pane/host.tsx
23861
24260
  var exports_host3 = {};
23862
24261
  __export(exports_host3, {
23863
- startTasksPane: () => startTasksPane
24262
+ startTasksPane: () => startTasksPane,
24263
+ legendRowCap: () => legendRowCap,
24264
+ legendCap: () => legendCap
23864
24265
  });
23865
24266
  import { existsSync as existsSync16 } from "fs";
23866
24267
  import { stat as stat5 } from "fs/promises";
@@ -24002,7 +24403,7 @@ function TasksShell(props) {
24002
24403
  async function archiveTask(id) {
24003
24404
  await archiveTaskFlow(taskActions, id);
24004
24405
  }
24005
- async function deleteTask(id) {
24406
+ async function deleteTask2(id) {
24006
24407
  await deleteTaskFlow(taskActions, id);
24007
24408
  }
24008
24409
  async function renameTask(id) {
@@ -24117,13 +24518,16 @@ function TasksShell(props) {
24117
24518
  if (exists) {
24118
24519
  const cwd2 = await getSessionOption(name, "@kobe_worktree") || task?.worktreePath || "";
24119
24520
  if (worktreeCwdUsable(cwd2)) {
24521
+ const init3 = task?.repo ? resolveRepoInit(task.repo, cwd2) : {};
24120
24522
  await ensureSession({
24121
24523
  name,
24122
24524
  cwd: cwd2,
24123
24525
  command: interactiveEngineCommand(task?.vendor),
24124
24526
  taskId: id,
24125
24527
  vendor: task?.vendor,
24126
- repo: task?.repo
24528
+ repo: task?.repo,
24529
+ initScript: init3.initScript,
24530
+ initPrompt: init3.initPrompt
24127
24531
  });
24128
24532
  }
24129
24533
  await runTmux(["switch-client", "-t", `=${name}`]);
@@ -24222,7 +24626,7 @@ function TasksShell(props) {
24222
24626
  } : undefined;
24223
24627
  },
24224
24628
  onRenameRequest: (id) => void renameTask(id),
24225
- onDeleteRequest: (id) => void deleteTask(id),
24629
+ onDeleteRequest: (id) => void deleteTask2(id),
24226
24630
  onArchiveRequest: (id) => void archiveTask(id),
24227
24631
  onLocalMergeRequest: (id) => {
24228
24632
  const task = props.tasks().find((t) => t.id === id);
@@ -24253,6 +24657,17 @@ function TasksShell(props) {
24253
24657
  return _el$;
24254
24658
  })();
24255
24659
  }
24660
+ function legendCap(id) {
24661
+ const row = findBinding(id);
24662
+ if (!row)
24663
+ return null;
24664
+ const cap = row.hint?.keys ?? row.keys[0];
24665
+ return cap && cap.length > 0 ? cap : null;
24666
+ }
24667
+ function legendRowCap(ids) {
24668
+ const caps = ids.map(legendCap).filter((c) => c !== null);
24669
+ return caps.length > 0 ? caps.join("/") : null;
24670
+ }
24256
24671
  function ShortcutHints(props) {
24257
24672
  const {
24258
24673
  theme
@@ -24340,55 +24755,70 @@ function ShortcutHints(props) {
24340
24755
  });
24341
24756
  return out;
24342
24757
  };
24343
- const defaultHints = () => [
24344
- {
24345
- k: "enter",
24346
- label: "open"
24347
- },
24348
- {
24349
- k: "right",
24350
- label: "focus engine"
24351
- },
24352
- {
24353
- k: "n",
24354
- label: "new task"
24355
- },
24356
- {
24357
- k: "s",
24358
- label: "settings"
24359
- },
24360
- {
24361
- k: "o",
24362
- label: "open wt"
24363
- },
24364
- {
24365
- k: "[/]",
24366
- label: "views"
24367
- },
24368
- {
24369
- k: "t",
24370
- label: "sort"
24371
- },
24372
- {
24373
- k: "M",
24374
- label: "move task",
24375
- dimWhenMain: true
24376
- },
24377
- {
24378
- k: "a/d",
24379
- label: "un/archive\xB7delete"
24380
- },
24381
- {
24382
- k: "r/b/v",
24383
- label: "name/branch/engine",
24384
- dimWhenMain: true
24385
- },
24386
- {
24387
- k: "f1",
24388
- label: "help"
24389
- },
24390
- ...tmuxHints()
24391
- ];
24758
+ const defaultHints = () => {
24759
+ keymapVersion();
24760
+ const rows = [
24761
+ {
24762
+ ids: ["sidebar.select"],
24763
+ label: "open"
24764
+ },
24765
+ {
24766
+ ids: ["tasks.focusEngine"],
24767
+ label: "focus engine"
24768
+ },
24769
+ {
24770
+ ids: ["task.new"],
24771
+ label: "new task"
24772
+ },
24773
+ {
24774
+ ids: ["settings.open.sidebar"],
24775
+ label: "settings"
24776
+ },
24777
+ {
24778
+ ids: ["tasks.openWorktree"],
24779
+ label: "open wt"
24780
+ },
24781
+ {
24782
+ ids: ["sidebar.view"],
24783
+ label: "views"
24784
+ },
24785
+ {
24786
+ ids: ["sidebar.sort"],
24787
+ label: "sort"
24788
+ },
24789
+ {
24790
+ ids: ["sidebar.localMerge"],
24791
+ label: "move task",
24792
+ dimWhenMain: true
24793
+ },
24794
+ {
24795
+ ids: ["sidebar.archive", "sidebar.delete"],
24796
+ label: "un/archive\xB7delete"
24797
+ },
24798
+ {
24799
+ ids: ["sidebar.rename", "tasks.renameBranch", "tasks.cycleEngine"],
24800
+ label: "name/branch/engine",
24801
+ dimWhenMain: true
24802
+ },
24803
+ {
24804
+ ids: ["help.open"],
24805
+ label: "help"
24806
+ }
24807
+ ];
24808
+ const out = [];
24809
+ for (const row of rows) {
24810
+ const k = legendRowCap(row.ids);
24811
+ if (k === null)
24812
+ continue;
24813
+ out.push({
24814
+ k,
24815
+ label: row.label,
24816
+ dimWhenMain: row.dimWhenMain
24817
+ });
24818
+ }
24819
+ out.push(...tmuxHints());
24820
+ return out;
24821
+ };
24392
24822
  const MOVE_HINTS = [{
24393
24823
  k: "j/k",
24394
24824
  label: "reorder"
@@ -24658,10 +25088,14 @@ async function startSettingsHost() {
24658
25088
  setup: async () => {
24659
25089
  let orch = null;
24660
25090
  try {
24661
- const client = await connectOrStartDaemon();
24662
- const remote = new RemoteOrchestrator(client);
24663
- await remote.init();
24664
- orch = remote;
25091
+ const client = await connectIfRunning();
25092
+ if (client) {
25093
+ const remote = new RemoteOrchestrator(client);
25094
+ await remote.init();
25095
+ orch = remote;
25096
+ } else {
25097
+ console.error("[kobe settings] no daemon running; Restart backend disabled");
25098
+ }
24665
25099
  } catch (err) {
24666
25100
  console.error("[kobe settings] daemon unavailable; Restart backend disabled:", err);
24667
25101
  }
@@ -26476,10 +26910,16 @@ var init_pr_prompt = __esm(() => {
26476
26910
  var exports_host7 = {};
26477
26911
  __export(exports_host7, {
26478
26912
  startOpsPreview: () => startOpsPreview,
26479
- startOpsHost: () => startOpsHost
26913
+ startOpsHost: () => startOpsHost,
26914
+ nextActivityPollDelay: () => nextActivityPollDelay
26480
26915
  });
26481
26916
  import { createHash as createHash5 } from "crypto";
26482
26917
  import { SyntaxStyle } from "@opentui/core";
26918
+ function nextActivityPollDelay(currentMs, idleStreak) {
26919
+ if (idleStreak < ACTIVITY_IDLE_RAMP_POLLS)
26920
+ return ACTIVITY_POLL_MIN_MS;
26921
+ return Math.min(currentMs * 2, ACTIVITY_POLL_MAX_MS);
26922
+ }
26483
26923
  function OpsShell(props) {
26484
26924
  const {
26485
26925
  theme
@@ -26489,6 +26929,10 @@ function OpsShell(props) {
26489
26929
  let primed = false;
26490
26930
  onMount(() => {
26491
26931
  let disposed = false;
26932
+ let timer;
26933
+ let delayMs = ACTIVITY_POLL_MIN_MS;
26934
+ let idleStreak = 0;
26935
+ let lastSeenMtime = 0;
26492
26936
  async function poll() {
26493
26937
  try {
26494
26938
  const mtime = await latestTranscriptMtime(props.vendor, props.worktree);
@@ -26497,15 +26941,29 @@ function OpsShell(props) {
26497
26941
  if (!primed) {
26498
26942
  primed = true;
26499
26943
  setBaseline(mtime);
26944
+ lastSeenMtime = mtime;
26945
+ }
26946
+ if (mtime > lastSeenMtime) {
26947
+ lastSeenMtime = mtime;
26948
+ idleStreak = 0;
26949
+ } else {
26950
+ idleStreak++;
26500
26951
  }
26501
26952
  setLatest(mtime);
26502
- } catch {}
26953
+ } catch {
26954
+ idleStreak++;
26955
+ } finally {
26956
+ if (!disposed) {
26957
+ delayMs = nextActivityPollDelay(delayMs, idleStreak);
26958
+ timer = setTimeout(() => void poll(), delayMs);
26959
+ }
26960
+ }
26503
26961
  }
26504
26962
  poll();
26505
- const timer = setInterval(() => void poll(), ACTIVITY_POLL_MS);
26506
26963
  onCleanup(() => {
26507
26964
  disposed = true;
26508
- clearInterval(timer);
26965
+ if (timer)
26966
+ clearTimeout(timer);
26509
26967
  });
26510
26968
  });
26511
26969
  const hasNewActivity = () => primed && latest() > baseline();
@@ -26867,7 +27325,7 @@ async function startOpsPreview(args2) {
26867
27325
  })
26868
27326
  });
26869
27327
  }
26870
- var ACTIVITY_POLL_MS = 2500, TURN_STATUS_POLL_MS = 1500, STABLE_POLLS_FOR_DONE = 2, CHAT_TAB_STATE_OPTION2 = "@kobe_tab_state";
27328
+ var ACTIVITY_POLL_MIN_MS = 2500, ACTIVITY_POLL_MAX_MS = 20000, ACTIVITY_IDLE_RAMP_POLLS = 3, TURN_STATUS_POLL_MS = 1500, STABLE_POLLS_FOR_DONE = 2, CHAT_TAB_STATE_OPTION2 = "@kobe_tab_state";
26871
27329
  var init_host7 = __esm(() => {
26872
27330
  init_solid();
26873
27331
  init_solid();