@sma1lboy/kobe 0.7.0 → 0.7.2

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 +537 -242
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -70,7 +70,7 @@ var init_package = __esm(() => {
70
70
  package_default = {
71
71
  $schema: "https://json.schemastore.org/package.json",
72
72
  name: "@sma1lboy/kobe",
73
- version: "0.7.0",
73
+ version: "0.7.2",
74
74
  description: "TUI orchestrator for Claude Code (codename)",
75
75
  type: "module",
76
76
  packageManager: "bun@1.3.13",
@@ -748,7 +748,8 @@ function coerceTask(value) {
748
748
  if (!isTaskStatus(v.status))
749
749
  return null;
750
750
  const archived = typeof v.archived === "boolean" ? v.archived : false;
751
- const healedStatus = v.status === "done" && !archived ? "in_progress" : v.status;
751
+ const kind = v.kind === "main" ? "main" : "task";
752
+ const healedStatus = kind === "main" ? v.status === "in_progress" || v.status === "done" ? "backlog" : v.status : v.status === "done" && !archived ? "in_progress" : v.status;
752
753
  return {
753
754
  id: toTaskId(v.id),
754
755
  title: v.title,
@@ -758,7 +759,7 @@ function coerceTask(value) {
758
759
  status: healedStatus,
759
760
  archived,
760
761
  pinned: typeof v.pinned === "boolean" ? v.pinned : false,
761
- kind: v.kind === "main" ? "main" : "task",
762
+ kind,
762
763
  vendor: isVendorId(v.vendor) ? v.vendor : DEFAULT_TASK_VENDOR,
763
764
  prStatus: coercePRStatus(v.prStatus),
764
765
  createdAt: v.createdAt,
@@ -3227,6 +3228,111 @@ var init_protocol = __esm(() => {
3227
3228
  CHANNEL_NAMES = ["task.snapshot", "active-task", "update"];
3228
3229
  });
3229
3230
 
3231
+ // src/daemon/paths.ts
3232
+ import { createHash as createHash2 } from "crypto";
3233
+ import { homedir as homedir3, tmpdir } from "os";
3234
+ import { join as join3 } from "path";
3235
+ function shortHomeTag(homeDir2) {
3236
+ return createHash2("sha1").update(homeDir2).digest("hex").slice(0, 8);
3237
+ }
3238
+ function fitSocketPath(naturalPath, homeDir2, role, pidTag) {
3239
+ if (Buffer.byteLength(naturalPath, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
3240
+ return naturalPath;
3241
+ const tag = shortHomeTag(homeDir2);
3242
+ const suffix = pidTag === undefined ? "" : `-${pidTag}`;
3243
+ const fallback = join3(tmpdir(), `kobe-${tag}-${role}${suffix}.sock`);
3244
+ if (Buffer.byteLength(fallback, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
3245
+ return fallback;
3246
+ throw new Error(`kobe socket path exceeds ${SOCKET_PATH_SAFETY_LIMIT} bytes even after fallback: ${fallback}`);
3247
+ }
3248
+ function defaultDaemonSocketPath(homeDir2) {
3249
+ const override = process.env.KOBE_DAEMON_SOCKET_PATH;
3250
+ if (override && override.length > 0)
3251
+ return override;
3252
+ const explicit = homeDir2 ?? process.env.KOBE_HOME_DIR;
3253
+ if (explicit && explicit.length > 0) {
3254
+ return fitSocketPath(join3(explicit, ".kobe", "daemon.sock"), explicit, "daemon");
3255
+ }
3256
+ const runtimeDir = process.env.XDG_RUNTIME_DIR;
3257
+ if (runtimeDir && runtimeDir.length > 0) {
3258
+ return fitSocketPath(join3(runtimeDir, "kobe.sock"), runtimeDir, "daemon");
3259
+ }
3260
+ const home = homedir3();
3261
+ return fitSocketPath(join3(home, ".kobe", "daemon.sock"), home, "daemon");
3262
+ }
3263
+ function defaultDaemonPidPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir3()) {
3264
+ const override = process.env.KOBE_DAEMON_PID_PATH;
3265
+ if (override && override.length > 0)
3266
+ return override;
3267
+ return join3(homeDir2, ".kobe", "daemon.pid");
3268
+ }
3269
+ function defaultDaemonLogPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir3()) {
3270
+ return join3(homeDir2, ".kobe", "daemon.log");
3271
+ }
3272
+ function defaultClientLogPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir3()) {
3273
+ return join3(homeDir2, ".kobe", "client.log");
3274
+ }
3275
+ var SOCKET_PATH_SAFETY_LIMIT = 100;
3276
+ var init_paths2 = () => {};
3277
+
3278
+ // src/client/client-log.ts
3279
+ import { appendFile, mkdir as mkdir2 } from "fs/promises";
3280
+ import { dirname as dirname3 } from "path";
3281
+ function setClientLogContext(ctx) {
3282
+ context = ctx;
3283
+ }
3284
+ function coerceError(err) {
3285
+ if (err instanceof Error)
3286
+ return { message: err.message, stack: err.stack };
3287
+ let text;
3288
+ try {
3289
+ text = typeof err === "string" ? err : JSON.stringify(err);
3290
+ } catch {
3291
+ text = String(err);
3292
+ }
3293
+ return { message: text };
3294
+ }
3295
+ function formatClientEntry(subsystem, message, now = new Date) {
3296
+ return `[${now.toISOString()}] client ${context} [${subsystem}] pid=${process.pid}: ${message}
3297
+ `;
3298
+ }
3299
+ function warnOnce() {
3300
+ if (warnedOnce)
3301
+ return;
3302
+ warnedOnce = true;
3303
+ try {
3304
+ process.stderr.write(`[kobe] client log write failed; continuing without it
3305
+ `);
3306
+ } catch {}
3307
+ }
3308
+ function append(line) {
3309
+ const path3 = defaultClientLogPath();
3310
+ writeChain = writeChain.then(async () => {
3311
+ try {
3312
+ await appendFile(path3, line);
3313
+ } catch (err) {
3314
+ if (err?.code === "ENOENT") {
3315
+ await mkdir2(dirname3(path3), { recursive: true });
3316
+ await appendFile(path3, line);
3317
+ } else {
3318
+ throw err;
3319
+ }
3320
+ }
3321
+ }).catch(() => warnOnce());
3322
+ }
3323
+ function logClient(subsystem, message) {
3324
+ append(formatClientEntry(subsystem, message));
3325
+ }
3326
+ function logClientError(subsystem, err) {
3327
+ const e = coerceError(err);
3328
+ append(formatClientEntry(subsystem, e.stack ?? e.message));
3329
+ }
3330
+ var context = "client", warnedOnce = false, writeChain;
3331
+ var init_client_log = __esm(() => {
3332
+ init_paths2();
3333
+ writeChain = Promise.resolve();
3334
+ });
3335
+
3230
3336
  // src/client/index.ts
3231
3337
  import { connect } from "net";
3232
3338
 
@@ -3259,6 +3365,9 @@ class KobeDaemonClient {
3259
3365
  p.then(cleanup, cleanup);
3260
3366
  return p;
3261
3367
  }
3368
+ get isDisposed() {
3369
+ return this.disposed;
3370
+ }
3262
3371
  close() {
3263
3372
  this.disposed = true;
3264
3373
  this.socket?.end();
@@ -3372,7 +3481,13 @@ class KobeDaemonClient {
3372
3481
  }
3373
3482
  }
3374
3483
  onLine(line) {
3375
- const frame = JSON.parse(line);
3484
+ let frame;
3485
+ try {
3486
+ frame = JSON.parse(line);
3487
+ } catch (err) {
3488
+ logClientError("client-frame", err);
3489
+ return;
3490
+ }
3376
3491
  if (frame.type === "event") {
3377
3492
  this.emit(frame);
3378
3493
  return;
@@ -3397,6 +3512,7 @@ class KobeDaemonClient {
3397
3512
  }
3398
3513
  var init_client = __esm(() => {
3399
3514
  init_protocol();
3515
+ init_client_log();
3400
3516
  });
3401
3517
 
3402
3518
  // src/engine/claude-code-local/normalize.ts
@@ -3446,8 +3562,8 @@ function normalizeClaudeContent(content) {
3446
3562
  }
3447
3563
 
3448
3564
  // src/engine/claude-code-local/history.ts
3449
- import { appendFile, mkdir as mkdir2, readFile as readFile2, readdir, stat, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
3450
- import { homedir as homedir3 } from "os";
3565
+ import { appendFile as appendFile2, mkdir as mkdir3, readFile as readFile2, readdir, stat, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
3566
+ import { homedir as homedir4 } from "os";
3451
3567
  import path3 from "path";
3452
3568
  function encodeCwd(cwd) {
3453
3569
  return cwd.replace(/[/.]/g, "-");
@@ -3455,7 +3571,7 @@ function encodeCwd(cwd) {
3455
3571
  async function listSessionFilesForWorktree(worktree) {
3456
3572
  if (!worktree)
3457
3573
  return [];
3458
- const dir = path3.join(homedir3(), ".claude", "projects", encodeCwd(worktree));
3574
+ const dir = path3.join(homedir4(), ".claude", "projects", encodeCwd(worktree));
3459
3575
  let entries;
3460
3576
  try {
3461
3577
  entries = await readdir(dir);
@@ -3561,7 +3677,7 @@ var defaultDeps;
3561
3677
  var init_history = __esm(() => {
3562
3678
  defaultDeps = {
3563
3679
  projectsDir() {
3564
- return path3.join(homedir3(), ".claude", "projects");
3680
+ return path3.join(homedir4(), ".claude", "projects");
3565
3681
  },
3566
3682
  async readdir(p) {
3567
3683
  try {
@@ -3663,7 +3779,7 @@ function validPositive(v) {
3663
3779
 
3664
3780
  // src/engine/codex-local/history.ts
3665
3781
  import { readFile as readFile3, readdir as readdir2, stat as stat2, unlink as unlink3 } from "fs/promises";
3666
- import { homedir as homedir4 } from "os";
3782
+ import { homedir as homedir5 } from "os";
3667
3783
  import path4 from "path";
3668
3784
  async function listRolloutFiles(deps = defaultDeps2) {
3669
3785
  const root = deps.sessionsDir();
@@ -3994,7 +4110,7 @@ var init_history2 = __esm(() => {
3994
4110
  init_synthetic();
3995
4111
  defaultDeps2 = {
3996
4112
  sessionsDir() {
3997
- return path4.join(homedir4(), ".codex", "sessions");
4113
+ return path4.join(homedir5(), ".codex", "sessions");
3998
4114
  },
3999
4115
  async readdir(p) {
4000
4116
  try {
@@ -4028,14 +4144,14 @@ function copilotUsageToSnapshot(value) {
4028
4144
  output += numberOr2(metrics.usage.outputTokens, 0);
4029
4145
  cached += numberOr2(metrics.usage.cacheReadTokens, 0);
4030
4146
  }
4031
- const context = numberOr2(value.currentTokens, 0);
4032
- if (input === 0 && output === 0 && cached === 0 && context === 0)
4147
+ const context2 = numberOr2(value.currentTokens, 0);
4148
+ if (input === 0 && output === 0 && cached === 0 && context2 === 0)
4033
4149
  return;
4034
4150
  return {
4035
4151
  input_tokens: input,
4036
4152
  output_tokens: output,
4037
4153
  ...cached > 0 ? { cache_read_input_tokens: cached } : {},
4038
- ...context > 0 ? { context_tokens: context } : {}
4154
+ ...context2 > 0 ? { context_tokens: context2 } : {}
4039
4155
  };
4040
4156
  }
4041
4157
  function isObject4(v) {
@@ -4047,7 +4163,7 @@ function numberOr2(value, fallback) {
4047
4163
 
4048
4164
  // src/engine/copilot-local/history.ts
4049
4165
  import { readFile as readFile4, readdir as readdir3, rm, stat as stat3 } from "fs/promises";
4050
- import { homedir as homedir5 } from "os";
4166
+ import { homedir as homedir6 } from "os";
4051
4167
  import path5 from "path";
4052
4168
  async function listSessionDirs(deps = defaultDeps3) {
4053
4169
  const root = path5.join(deps.copilotDir(), "session-state");
@@ -4216,7 +4332,7 @@ var init_history3 = __esm(() => {
4216
4332
  const override = process.env.COPILOT_HOME?.trim();
4217
4333
  if (override)
4218
4334
  return override;
4219
- return path5.join(homedir5(), ".copilot");
4335
+ return path5.join(homedir6(), ".copilot");
4220
4336
  },
4221
4337
  async readdir(p) {
4222
4338
  try {
@@ -4288,6 +4404,90 @@ var init_auto_title = __esm(() => {
4288
4404
  init_history3();
4289
4405
  });
4290
4406
 
4407
+ // src/cli/invocation.ts
4408
+ import { fileURLToPath } from "url";
4409
+ function kobeCliInvocation() {
4410
+ const isBuilt = import.meta.url.endsWith(".js");
4411
+ if (isBuilt)
4412
+ return ["kobe"];
4413
+ const entry = fileURLToPath(new URL("./index.ts", import.meta.url));
4414
+ const preload = fileURLToPath(import.meta.resolve("@opentui/solid/preload"));
4415
+ return [process.execPath, "--preload", preload, "--conditions=browser", entry];
4416
+ }
4417
+ var init_invocation = () => {};
4418
+
4419
+ // src/tmux/session-layout.ts
4420
+ function shellQuote(s) {
4421
+ return `'${s.replace(/'/g, "'\\''")}'`;
4422
+ }
4423
+ function shellQuoteArgv(argv) {
4424
+ return argv.map(shellQuote).join(" ");
4425
+ }
4426
+ function keepAlive(cmd) {
4427
+ return `${cmd}; exec "\${SHELL:-/bin/sh}"`;
4428
+ }
4429
+ function shQuote(s) {
4430
+ return `'${s.replace(/'/g, "'\\''")}'`;
4431
+ }
4432
+ function homeWelcomeCommand() {
4433
+ const msg = "\\n No task selected\\n\\n Press N to create a task, or pick one on the left.\\n\\n";
4434
+ return `clear; printf ${shQuote(msg)}; exec "\${SHELL:-/bin/sh}"`;
4435
+ }
4436
+ function engineLaunchLine(engineCmd, init) {
4437
+ const tail = keepAlive(engineCmd);
4438
+ const script = init?.initScript?.trim();
4439
+ if (!script)
4440
+ return tail;
4441
+ const group = ["{", script, "}"].join(`
4442
+ `);
4443
+ if (init?.markerPath) {
4444
+ const marker = shQuote(init.markerPath);
4445
+ const markerDir = shQuote(markerDirOf(init.markerPath));
4446
+ return [
4447
+ `if [ ! -f ${marker} ]; then`,
4448
+ group,
4449
+ `if [ $? -eq 0 ]; then mkdir -p ${markerDir} && : > ${marker}; fi`,
4450
+ "fi",
4451
+ tail
4452
+ ].join(`
4453
+ `);
4454
+ }
4455
+ return [group, tail].join(`
4456
+ `);
4457
+ }
4458
+ function markerDirOf(p) {
4459
+ const i = p.lastIndexOf("/");
4460
+ return i <= 0 ? "." : p.slice(0, i);
4461
+ }
4462
+ function fallbackOpsScript(cwd) {
4463
+ return `cd ${shellQuote(cwd)} && while :; do clear; printf "\\033[1m# %s\\033[0m\\n\\n" ${shellQuote(cwd)}; git status --short --branch 2>/dev/null | sed 's/^/ /' || true; printf "\\n"; if command -v lsd >/dev/null 2>&1; then lsd --tree --git -I node_modules -I .git --depth 2 .; elif command -v eza >/dev/null 2>&1; then eza --tree --git -L 2 -I 'node_modules|.git' .; elif command -v tree >/dev/null 2>&1; then tree -L 2 -I 'node_modules|.git'; else ls -la; fi; sleep 2; done`;
4464
+ }
4465
+ function previewWindowCommand(args) {
4466
+ const wt = shellQuote(args.worktree);
4467
+ const file = shellQuote(args.relPath);
4468
+ const inv = args.cliInvocation.map(shellQuote).join(" ");
4469
+ const fallback = `cd ${wt} && if ! git diff --quiet HEAD -- ${file} 2>/dev/null; then ` + `git diff HEAD -- ${file} | { delta --paging=always 2>/dev/null || less -R; }; ` + `else bat --style=plain --paging=always ${file} 2>/dev/null || \${PAGER:-less} ${file} 2>/dev/null || cat ${file}; fi`;
4470
+ return `${inv} ops --worktree ${wt} --preview ${file} || { ${fallback}; }`;
4471
+ }
4472
+ function updatePageCommand(args) {
4473
+ return `${shellQuoteArgv([...args.cliInvocation, "update-page"])}`;
4474
+ }
4475
+ function tasksPaneCommand(cliInvocation, opts = {}) {
4476
+ const argv = [...cliInvocation, "tasks"];
4477
+ if (opts.initialTaskId)
4478
+ argv.push("--initial-task-id", opts.initialTaskId);
4479
+ return shellQuoteArgv(argv);
4480
+ }
4481
+ function opsPaneCommand(args) {
4482
+ if (args.taskId && args.claudePaneId) {
4483
+ const inv = args.cliInvocation.map(shellQuote).join(" ");
4484
+ const vendorFlag = args.vendor ? ` --vendor ${shellQuote(args.vendor)}` : "";
4485
+ return `KOBE_FILETREE_WATCH=1 ${inv} ops --task-id ${shellQuote(args.taskId)} --worktree ${shellQuote(args.cwd)} ` + `--target-pane ${shellQuote(args.claudePaneId)}${vendorFlag} || { ${fallbackOpsScript(args.cwd)}; }`;
4486
+ }
4487
+ return fallbackOpsScript(args.cwd);
4488
+ }
4489
+ var TASKS_PANE_WIDTH = 32, CLAUDE_PANE_PERCENT = 60, OPS_PANE_PERCENT = 50;
4490
+
4291
4491
  // src/tmux/client.ts
4292
4492
  var exports_client = {};
4293
4493
  __export(exports_client, {
@@ -4325,6 +4525,7 @@ __export(exports_client, {
4325
4525
  CLAUDE_ROLE_OPTION: () => CLAUDE_ROLE_OPTION,
4326
4526
  CHAT_TAB_SESSION_ID_OPTION: () => CHAT_TAB_SESSION_ID_OPTION
4327
4527
  });
4528
+ import { homedir as homedir7 } from "os";
4328
4529
  function tmuxArgs(...args) {
4329
4530
  return ["tmux", "-L", KOBE_TMUX_SOCKET, ...args];
4330
4531
  }
@@ -4344,16 +4545,34 @@ async function drainText(stream) {
4344
4545
  }
4345
4546
  }
4346
4547
  async function runTmux(args) {
4347
- const proc = Bun.spawn(tmuxArgs(...args), { stdin: "ignore", stdout: "ignore", stderr: "pipe" });
4348
- const [errText, code] = await Promise.all([drainText(proc.stderr), proc.exited]);
4349
- if (code !== 0 && errText.trim().length > 0) {
4350
- console.error(`[kobe tmux] ${args.join(" ")} (${code}): ${errText.trim()}`);
4548
+ try {
4549
+ const proc = Bun.spawn(tmuxArgs(...args), {
4550
+ stdin: "ignore",
4551
+ cwd: SAFE_SPAWN_CWD,
4552
+ stdout: "ignore",
4553
+ stderr: "pipe"
4554
+ });
4555
+ const [errText, code] = await Promise.all([drainText(proc.stderr), proc.exited]);
4556
+ if (code !== 0 && errText.trim().length > 0) {
4557
+ console.error(`[kobe tmux] ${args.join(" ")} (${code}): ${errText.trim()}`);
4558
+ }
4559
+ return code;
4560
+ } catch {
4561
+ return 1;
4351
4562
  }
4352
- return code;
4353
4563
  }
4354
4564
  async function runTmuxQuiet(args) {
4355
- const proc = Bun.spawn(tmuxArgs(...args), { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
4356
- return proc.exited;
4565
+ try {
4566
+ const proc = Bun.spawn(tmuxArgs(...args), {
4567
+ stdin: "ignore",
4568
+ cwd: SAFE_SPAWN_CWD,
4569
+ stdout: "ignore",
4570
+ stderr: "ignore"
4571
+ });
4572
+ return await proc.exited;
4573
+ } catch {
4574
+ return 1;
4575
+ }
4357
4576
  }
4358
4577
  function tmuxCommandSequence(commands) {
4359
4578
  const out = [];
@@ -4371,12 +4590,16 @@ async function runTmuxSequence(commands) {
4371
4590
  return args.length === 0 ? 0 : runTmux(args);
4372
4591
  }
4373
4592
  async function runTmuxCapturing(args) {
4374
- const proc = Bun.spawn(tmuxArgs(...args), { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
4375
- const [stdout, errText, code] = await Promise.all([drainText(proc.stdout), drainText(proc.stderr), proc.exited]);
4376
- if (code !== 0 && errText.trim().length > 0) {
4377
- console.error(`[kobe tmux] ${args.join(" ")} (${code}): ${errText.trim()}`);
4593
+ try {
4594
+ const proc = Bun.spawn(tmuxArgs(...args), { stdin: "ignore", cwd: SAFE_SPAWN_CWD, stdout: "pipe", stderr: "pipe" });
4595
+ const [stdout, errText, code] = await Promise.all([drainText(proc.stdout), drainText(proc.stderr), proc.exited]);
4596
+ if (code !== 0 && errText.trim().length > 0) {
4597
+ console.error(`[kobe tmux] ${args.join(" ")} (${code}): ${errText.trim()}`);
4598
+ }
4599
+ return { code, stdout };
4600
+ } catch {
4601
+ return { code: 1, stdout: "" };
4378
4602
  }
4379
- return { code, stdout };
4380
4603
  }
4381
4604
  async function runTmuxSequenceCapturing(commands) {
4382
4605
  const args = tmuxCommandSequence(commands);
@@ -4384,7 +4607,7 @@ async function runTmuxSequenceCapturing(commands) {
4384
4607
  }
4385
4608
  async function tmuxAvailable() {
4386
4609
  try {
4387
- const proc = Bun.spawn(["tmux", "-V"], { stdin: "ignore", stdout: "ignore", stderr: "ignore" });
4610
+ const proc = Bun.spawn(["tmux", "-V"], { stdin: "ignore", cwd: SAFE_SPAWN_CWD, stdout: "ignore", stderr: "ignore" });
4388
4611
  return await proc.exited === 0;
4389
4612
  } catch {
4390
4613
  return false;
@@ -4499,21 +4722,53 @@ async function killSession(name) {
4499
4722
  }
4500
4723
  async function ensureFallbackSession() {
4501
4724
  const name = KOBE_HOME_SESSION;
4502
- if (!await sessionExists(name)) {
4503
- await runTmux([
4504
- "new-session",
4505
- "-d",
4506
- "-s",
4507
- name,
4508
- "-x",
4509
- "220",
4510
- "-y",
4511
- "50",
4512
- "sh",
4725
+ if (await sessionExists(name)) {
4726
+ if (await getSessionOption(name, HOME_KIND_OPTION) === "tasks")
4727
+ return name;
4728
+ await runTmux(["kill-session", "-t", `=${name}`]);
4729
+ }
4730
+ const r0 = await runTmuxCapturing([
4731
+ "new-session",
4732
+ "-d",
4733
+ "-s",
4734
+ name,
4735
+ "-c",
4736
+ SAFE_SPAWN_CWD,
4737
+ "-x",
4738
+ "220",
4739
+ "-y",
4740
+ "50",
4741
+ "-P",
4742
+ "-F",
4743
+ "#{pane_id}",
4744
+ homeWelcomeCommand()
4745
+ ]);
4746
+ const mainPane = r0.stdout.trim();
4747
+ if (mainPane) {
4748
+ const r1 = await runTmuxCapturing([
4749
+ "split-window",
4750
+ "-h",
4751
+ "-b",
4752
+ "-t",
4753
+ mainPane,
4754
+ "-l",
4755
+ `${TASKS_PANE_WIDTH}`,
4513
4756
  "-c",
4514
- 'clear; printf "\\n No active task\\n\\n Select a task from the sidebar to continue.\\n\\n"; exec ${SHELL:-sh}'
4757
+ SAFE_SPAWN_CWD,
4758
+ "-P",
4759
+ "-F",
4760
+ "#{pane_id}",
4761
+ keepAlive(tasksPaneCommand(kobeCliInvocation()))
4515
4762
  ]);
4763
+ const tasksPane = r1.stdout.trim();
4764
+ if (tasksPane) {
4765
+ await runTmuxSequence([
4766
+ ["set-option", "-p", "-t", tasksPane, PANE_ROLE_OPTION, "tasks"],
4767
+ ["select-pane", "-t", tasksPane]
4768
+ ]);
4769
+ }
4516
4770
  }
4771
+ await setSessionOption(name, HOME_KIND_OPTION, "tasks");
4517
4772
  return name;
4518
4773
  }
4519
4774
  async function switchClientBeforeKill(killedName, nextSessionName) {
@@ -4527,9 +4782,11 @@ async function switchClientBeforeKill(killedName, nextSessionName) {
4527
4782
  const fallback = await ensureFallbackSession();
4528
4783
  await runTmux(["switch-client", "-t", `=${fallback}`]);
4529
4784
  }
4530
- var KOBE_TMUX_SOCKET, 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";
4785
+ 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";
4531
4786
  var init_client2 = __esm(() => {
4787
+ init_invocation();
4532
4788
  KOBE_TMUX_SOCKET = process.env.KOBE_TMUX_SOCKET?.trim() || "kobe";
4789
+ SAFE_SPAWN_CWD = homedir7() || "/";
4533
4790
  CLAUDE_ROLE_OPTION = PANE_ROLE_OPTION;
4534
4791
  });
4535
4792
 
@@ -4627,6 +4884,13 @@ function formatDaemonError(subsystem, err, now = new Date) {
4627
4884
  function logDaemonError(subsystem, err) {
4628
4885
  process.stderr.write(formatDaemonError(subsystem, err));
4629
4886
  }
4887
+ function formatDaemonInfo(subsystem, message, now = new Date) {
4888
+ return `[${now.toISOString()}] daemon [${subsystem}]: ${message}
4889
+ `;
4890
+ }
4891
+ function logDaemonInfo(subsystem, message) {
4892
+ process.stderr.write(formatDaemonInfo(subsystem, message));
4893
+ }
4630
4894
  function installDaemonCrashHandlers(log = (l) => process.stderr.write(l)) {
4631
4895
  if (onRejection || onException)
4632
4896
  return;
@@ -4702,54 +4966,10 @@ class DaemonEventBus {
4702
4966
  }
4703
4967
  }
4704
4968
 
4705
- // src/daemon/paths.ts
4706
- import { createHash as createHash2 } from "crypto";
4707
- import { homedir as homedir6, tmpdir } from "os";
4708
- import { join as join3 } from "path";
4709
- function shortHomeTag(homeDir2) {
4710
- return createHash2("sha1").update(homeDir2).digest("hex").slice(0, 8);
4711
- }
4712
- function fitSocketPath(naturalPath, homeDir2, role, pidTag) {
4713
- if (Buffer.byteLength(naturalPath, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
4714
- return naturalPath;
4715
- const tag = shortHomeTag(homeDir2);
4716
- const suffix = pidTag === undefined ? "" : `-${pidTag}`;
4717
- const fallback = join3(tmpdir(), `kobe-${tag}-${role}${suffix}.sock`);
4718
- if (Buffer.byteLength(fallback, "utf8") <= SOCKET_PATH_SAFETY_LIMIT)
4719
- return fallback;
4720
- throw new Error(`kobe socket path exceeds ${SOCKET_PATH_SAFETY_LIMIT} bytes even after fallback: ${fallback}`);
4721
- }
4722
- function defaultDaemonSocketPath(homeDir2) {
4723
- const override = process.env.KOBE_DAEMON_SOCKET_PATH;
4724
- if (override && override.length > 0)
4725
- return override;
4726
- const explicit = homeDir2 ?? process.env.KOBE_HOME_DIR;
4727
- if (explicit && explicit.length > 0) {
4728
- return fitSocketPath(join3(explicit, ".kobe", "daemon.sock"), explicit, "daemon");
4729
- }
4730
- const runtimeDir = process.env.XDG_RUNTIME_DIR;
4731
- if (runtimeDir && runtimeDir.length > 0) {
4732
- return fitSocketPath(join3(runtimeDir, "kobe.sock"), runtimeDir, "daemon");
4733
- }
4734
- const home = homedir6();
4735
- return fitSocketPath(join3(home, ".kobe", "daemon.sock"), home, "daemon");
4736
- }
4737
- function defaultDaemonPidPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir6()) {
4738
- const override = process.env.KOBE_DAEMON_PID_PATH;
4739
- if (override && override.length > 0)
4740
- return override;
4741
- return join3(homeDir2, ".kobe", "daemon.pid");
4742
- }
4743
- function defaultDaemonLogPath(homeDir2 = process.env.KOBE_HOME_DIR ?? homedir6()) {
4744
- return join3(homeDir2, ".kobe", "daemon.log");
4745
- }
4746
- var SOCKET_PATH_SAFETY_LIMIT = 100;
4747
- var init_paths2 = () => {};
4748
-
4749
4969
  // src/daemon/server.ts
4750
- import { mkdir as mkdir3, readFile as readFile5, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
4970
+ import { mkdir as mkdir4, readFile as readFile5, unlink as unlink4, writeFile as writeFile3 } from "fs/promises";
4751
4971
  import { createServer } from "net";
4752
- import { dirname as dirname3 } from "path";
4972
+ import { dirname as dirname4 } from "path";
4753
4973
  function resolveIdleGraceMs() {
4754
4974
  const raw = process.env.KOBE_DAEMON_IDLE_GRACE_MS;
4755
4975
  if (raw === undefined)
@@ -4783,10 +5003,12 @@ async function startDaemonServer(orch, options = {}) {
4783
5003
  if (stopping || guiCount() > 0)
4784
5004
  return;
4785
5005
  cancelIdleTimer();
5006
+ logDaemonInfo("idle", `last gui gone \u2014 arming ${idleGraceMs}ms idle-stop grace`);
4786
5007
  idleTimer = setTimeout(() => {
4787
5008
  idleTimer = null;
4788
5009
  if (stopping || guiCount() > 0)
4789
5010
  return;
5011
+ logDaemonInfo("idle", "grace elapsed with no gui \u2014 self-stopping");
4790
5012
  stopSoon().catch((err) => logDaemonError("daemon-idle-shutdown", err));
4791
5013
  }, idleGraceMs);
4792
5014
  idleTimer.unref?.();
@@ -4795,8 +5017,8 @@ async function startDaemonServer(orch, options = {}) {
4795
5017
  bus.onPublish((event) => {
4796
5018
  broadcast(clients, { type: "event", name: event.channel, payload: event.payload });
4797
5019
  });
4798
- await mkdir3(dirname3(socketPath), { recursive: true });
4799
- await mkdir3(dirname3(pidPath), { recursive: true });
5020
+ await mkdir4(dirname4(socketPath), { recursive: true });
5021
+ await mkdir4(dirname4(pidPath), { recursive: true });
4800
5022
  await unlink4(socketPath).catch(() => {});
4801
5023
  const server = createServer((socket) => {
4802
5024
  const client = {
@@ -4815,6 +5037,9 @@ async function startDaemonServer(orch, options = {}) {
4815
5037
  socket.on("error", () => {});
4816
5038
  socket.on("close", () => {
4817
5039
  clients.delete(client);
5040
+ if (client.subscribed) {
5041
+ logDaemonInfo("conn", `client #${client.id} (${client.holdsLifetime ? "gui" : "pane"}) disconnected \u2014 ${clients.size} client(s), ${guiCount()} gui left`);
5042
+ }
4818
5043
  if (client.holdsLifetime)
4819
5044
  maybeArmIdleShutdown();
4820
5045
  });
@@ -5007,6 +5232,7 @@ async function startDaemonServer(orch, options = {}) {
5007
5232
  client.holdsLifetime = role === "gui";
5008
5233
  if (client.holdsLifetime)
5009
5234
  cancelIdleTimer();
5235
+ logDaemonInfo("conn", `client #${client.id} subscribed as ${role} \u2014 ${clients.size} client(s), ${guiCount()} gui`);
5010
5236
  for (const event of bus.snapshot()) {
5011
5237
  writeFrame(client, { type: "event", name: event.channel, payload: event.payload });
5012
5238
  }
@@ -5186,13 +5412,13 @@ __export(exports_daemon_process, {
5186
5412
  });
5187
5413
  import { spawn } from "child_process";
5188
5414
  import { closeSync, existsSync, mkdirSync as mkdirSync2, openSync } from "fs";
5189
- import { dirname as dirname4, resolve as resolve2 } from "path";
5190
- import { fileURLToPath } from "url";
5415
+ import { dirname as dirname5, resolve as resolve2 } from "path";
5416
+ import { fileURLToPath as fileURLToPath2 } from "url";
5191
5417
  function spawnDetachedDaemon(command, args, env, logPath) {
5192
5418
  let stdio = "ignore";
5193
5419
  let logFd;
5194
5420
  try {
5195
- mkdirSync2(dirname4(logPath), { recursive: true });
5421
+ mkdirSync2(dirname5(logPath), { recursive: true });
5196
5422
  logFd = openSync(logPath, "a");
5197
5423
  stdio = ["ignore", logFd, logFd];
5198
5424
  } catch {
@@ -5255,11 +5481,11 @@ async function testDaemonResponds(socketPath, timeoutMs = DAEMON_HELLO_TIMEOUT_M
5255
5481
  return alive;
5256
5482
  }
5257
5483
  function resolveKobeSpawn(subcommand) {
5258
- const here = fileURLToPath(import.meta.url);
5484
+ const here = fileURLToPath2(import.meta.url);
5259
5485
  if (here.startsWith("/$bunfs") || here.startsWith("B:\\~BUN")) {
5260
5486
  return [process.execPath, ...subcommand];
5261
5487
  }
5262
- const dir = dirname4(here);
5488
+ const dir = dirname5(here);
5263
5489
  const sourceEntry = resolve2(dir, "../cli/index.ts");
5264
5490
  if (existsSync(sourceEntry))
5265
5491
  return [process.execPath, sourceEntry, ...subcommand];
@@ -5513,83 +5739,6 @@ var init_prompt_delivery = __esm(() => {
5513
5739
  init_client2();
5514
5740
  });
5515
5741
 
5516
- // src/cli/invocation.ts
5517
- import { fileURLToPath as fileURLToPath2 } from "url";
5518
- function kobeCliInvocation() {
5519
- const isBuilt = import.meta.url.endsWith(".js");
5520
- if (isBuilt)
5521
- return ["kobe"];
5522
- const entry = fileURLToPath2(new URL("./index.ts", import.meta.url));
5523
- const preload = fileURLToPath2(import.meta.resolve("@opentui/solid/preload"));
5524
- return [process.execPath, "--preload", preload, "--conditions=browser", entry];
5525
- }
5526
- var init_invocation = () => {};
5527
-
5528
- // src/tmux/session-layout.ts
5529
- function shellQuote(s) {
5530
- return `'${s.replace(/'/g, "'\\''")}'`;
5531
- }
5532
- function shellQuoteArgv(argv) {
5533
- return argv.map(shellQuote).join(" ");
5534
- }
5535
- function keepAlive(cmd) {
5536
- return `${cmd}; exec "\${SHELL:-/bin/sh}"`;
5537
- }
5538
- function shQuote(s) {
5539
- return `'${s.replace(/'/g, "'\\''")}'`;
5540
- }
5541
- function engineLaunchLine(engineCmd, init) {
5542
- const tail = keepAlive(engineCmd);
5543
- const script = init?.initScript?.trim();
5544
- if (!script)
5545
- return tail;
5546
- const group = ["{", script, "}"].join(`
5547
- `);
5548
- if (init?.markerPath) {
5549
- const marker = shQuote(init.markerPath);
5550
- const markerDir = shQuote(markerDirOf(init.markerPath));
5551
- return [
5552
- `if [ ! -f ${marker} ]; then`,
5553
- group,
5554
- `if [ $? -eq 0 ]; then mkdir -p ${markerDir} && : > ${marker}; fi`,
5555
- "fi",
5556
- tail
5557
- ].join(`
5558
- `);
5559
- }
5560
- return [group, tail].join(`
5561
- `);
5562
- }
5563
- function markerDirOf(p) {
5564
- const i = p.lastIndexOf("/");
5565
- return i <= 0 ? "." : p.slice(0, i);
5566
- }
5567
- function fallbackOpsScript(cwd) {
5568
- return `cd ${shellQuote(cwd)} && while :; do clear; printf "\\033[1m# %s\\033[0m\\n\\n" ${shellQuote(cwd)}; git status --short --branch 2>/dev/null | sed 's/^/ /' || true; printf "\\n"; if command -v lsd >/dev/null 2>&1; then lsd --tree --git -I node_modules -I .git --depth 2 .; elif command -v eza >/dev/null 2>&1; then eza --tree --git -L 2 -I 'node_modules|.git' .; elif command -v tree >/dev/null 2>&1; then tree -L 2 -I 'node_modules|.git'; else ls -la; fi; sleep 2; done`;
5569
- }
5570
- function previewWindowCommand(args) {
5571
- const wt = shellQuote(args.worktree);
5572
- const file = shellQuote(args.relPath);
5573
- const inv = args.cliInvocation.map(shellQuote).join(" ");
5574
- const fallback = `cd ${wt} && if ! git diff --quiet HEAD -- ${file} 2>/dev/null; then ` + `git diff HEAD -- ${file} | { delta --paging=always 2>/dev/null || less -R; }; ` + `else bat --style=plain --paging=always ${file} 2>/dev/null || \${PAGER:-less} ${file} 2>/dev/null || cat ${file}; fi`;
5575
- return `${inv} ops --worktree ${wt} --preview ${file} || { ${fallback}; }`;
5576
- }
5577
- function updatePageCommand(args) {
5578
- return `${shellQuoteArgv([...args.cliInvocation, "update-page"])}`;
5579
- }
5580
- function tasksPaneCommand(cliInvocation) {
5581
- return shellQuoteArgv([...cliInvocation, "tasks"]);
5582
- }
5583
- function opsPaneCommand(args) {
5584
- if (args.taskId && args.claudePaneId) {
5585
- const inv = args.cliInvocation.map(shellQuote).join(" ");
5586
- const vendorFlag = args.vendor ? ` --vendor ${shellQuote(args.vendor)}` : "";
5587
- return `KOBE_FILETREE_WATCH=1 ${inv} ops --task-id ${shellQuote(args.taskId)} --worktree ${shellQuote(args.cwd)} ` + `--target-pane ${shellQuote(args.claudePaneId)}${vendorFlag} || { ${fallbackOpsScript(args.cwd)}; }`;
5588
- }
5589
- return fallbackOpsScript(args.cwd);
5590
- }
5591
- var TASKS_PANE_WIDTH = 32, CLAUDE_PANE_PERCENT = 60, OPS_PANE_PERCENT = 50;
5592
-
5593
5742
  // src/tui/panes/terminal/tmux.ts
5594
5743
  var exports_tmux = {};
5595
5744
  __export(exports_tmux, {
@@ -5712,6 +5861,7 @@ async function ensureSessionImpl(opts) {
5712
5861
  const chooseEngineTmuxCommand = `run-shell ${shellQuote(chooseEngineCommand)}`;
5713
5862
  await runTmuxSequence([
5714
5863
  ["set-option", "-g", "status", "on"],
5864
+ ["set-window-option", "-g", "aggressive-resize", "on"],
5715
5865
  ["set-option", "-g", "monitor-activity", "on"],
5716
5866
  ["set-option", "-g", "visual-activity", "off"],
5717
5867
  ["set-option", "-g", "window-status-format", CHAT_TAB_STATUS_FORMAT],
@@ -5811,7 +5961,15 @@ async function healKobePaneVersions(session, cwd, taskId, vendor) {
5811
5961
  const tasksPane = panes.find((pane) => pane.role === "tasks");
5812
5962
  const opsPane = panes.find((pane) => pane.role === "ops");
5813
5963
  if (tasksPane && tasksPane.version !== CURRENT_VERSION) {
5814
- commands.push(["respawn-pane", "-k", "-t", tasksPane.paneId, "-c", cwd, keepAlive(envPrefix + tasksPaneCommand(inv))], ["set-option", "-p", "-t", tasksPane.paneId, "@kobe_role", "tasks"], ["set-option", "-p", "-t", tasksPane.paneId, PANE_VERSION_OPTION, CURRENT_VERSION]);
5964
+ commands.push([
5965
+ "respawn-pane",
5966
+ "-k",
5967
+ "-t",
5968
+ tasksPane.paneId,
5969
+ "-c",
5970
+ cwd,
5971
+ keepAlive(envPrefix + tasksPaneCommand(inv, { initialTaskId: taskId }))
5972
+ ], ["set-option", "-p", "-t", tasksPane.paneId, "@kobe_role", "tasks"], ["set-option", "-p", "-t", tasksPane.paneId, PANE_VERSION_OPTION, CURRENT_VERSION]);
5815
5973
  }
5816
5974
  if (opsPane && claudePane && opsPane.version !== CURRENT_VERSION) {
5817
5975
  commands.push([
@@ -5863,9 +6021,17 @@ async function refreshKobeWorkspacePanes(session) {
5863
6021
  const tasksPane = panes.find((pane) => pane.role === "tasks");
5864
6022
  const opsPane = panes.find((pane) => pane.role === "ops");
5865
6023
  if (tasksPane) {
5866
- commands.push(["respawn-pane", "-k", "-t", tasksPane.paneId, "-c", cwd, keepAlive(envPrefix + tasksPaneCommand(inv))], ["set-option", "-p", "-t", tasksPane.paneId, "@kobe_role", "tasks"], ["set-option", "-p", "-t", tasksPane.paneId, PANE_VERSION_OPTION, CURRENT_VERSION]);
6024
+ commands.push([
6025
+ "respawn-pane",
6026
+ "-k",
6027
+ "-t",
6028
+ tasksPane.paneId,
6029
+ "-c",
6030
+ cwd,
6031
+ keepAlive(envPrefix + tasksPaneCommand(inv, { initialTaskId: taskId }))
6032
+ ], ["set-option", "-p", "-t", tasksPane.paneId, "@kobe_role", "tasks"], ["set-option", "-p", "-t", tasksPane.paneId, PANE_VERSION_OPTION, CURRENT_VERSION]);
5867
6033
  }
5868
- if (opsPane && claudePane) {
6034
+ if (opsPane) {
5869
6035
  commands.push([
5870
6036
  "respawn-pane",
5871
6037
  "-k",
@@ -5876,7 +6042,7 @@ async function refreshKobeWorkspacePanes(session) {
5876
6042
  keepAlive(envPrefix + opsPaneCommand({
5877
6043
  cwd,
5878
6044
  taskId,
5879
- claudePaneId: claudePane,
6045
+ claudePaneId: claudePane ?? null,
5880
6046
  cliInvocation: inv,
5881
6047
  vendor
5882
6048
  }))
@@ -5920,7 +6086,7 @@ async function buildPanesAround(claudePane, args) {
5920
6086
  "-P",
5921
6087
  "-F",
5922
6088
  "tasks=#{pane_id}",
5923
- keepAlive(envPrefix + tasksPaneCommand(args.inv))
6089
+ keepAlive(envPrefix + tasksPaneCommand(args.inv, { initialTaskId: args.taskId }))
5924
6090
  ],
5925
6091
  [
5926
6092
  "split-window",
@@ -6921,9 +7087,9 @@ var init_theme = __esm(() => {
6921
7087
  });
6922
7088
 
6923
7089
  // src/core/index.ts
6924
- import { homedir as homedir7 } from "os";
7090
+ import { homedir as homedir8 } from "os";
6925
7091
  async function createKobeCore(options = {}) {
6926
- const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir7();
7092
+ const homeDir2 = options.homeDir ?? process.env.KOBE_HOME_DIR ?? homedir8();
6927
7093
  const store = new TaskIndexStore({ homeDir: homeDir2 });
6928
7094
  await store.load();
6929
7095
  const worktrees = new GitWorktreeManager;
@@ -7041,10 +7207,10 @@ var init_daemon_cmd = __esm(() => {
7041
7207
 
7042
7208
  // src/lib/skill-install.ts
7043
7209
  import { existsSync as existsSync4 } from "fs";
7044
- import { homedir as homedir8 } from "os";
7210
+ import { homedir as homedir9 } from "os";
7045
7211
  import { join as join7 } from "path";
7046
7212
  function kobeSkillPaths(opts = {}) {
7047
- const home = opts.home ?? homedir8();
7213
+ const home = opts.home ?? homedir9();
7048
7214
  const cwd = opts.cwd ?? process.cwd();
7049
7215
  return [join7(home, SKILL_REL_PATH), join7(cwd, SKILL_REL_PATH)];
7050
7216
  }
@@ -7073,6 +7239,7 @@ var init_skill_install = __esm(() => {
7073
7239
  var exports_maintenance = {};
7074
7240
  __export(exports_maintenance, {
7075
7241
  runResetSubcommand: () => runResetSubcommand,
7242
+ runReloadSubcommand: () => runReloadSubcommand,
7076
7243
  runDoctorSubcommand: () => runDoctorSubcommand
7077
7244
  });
7078
7245
  import { existsSync as existsSync5, readFileSync as readFileSync6, statSync } from "fs";
@@ -7326,6 +7493,54 @@ Stop daemon and kill kobe sessions? [y/N] `);
7326
7493
  console.log(`
7327
7494
  kobe: reset complete. Relaunch kobe to start fresh.`);
7328
7495
  }
7496
+ async function runReloadSubcommand(argv = []) {
7497
+ if (argv.includes("--help") || argv.includes("-h") || argv.includes("help")) {
7498
+ process.stdout.write([
7499
+ "Usage: kobe reload",
7500
+ "",
7501
+ "Restart kobe's Tasks + Ops panes in every live session, in place.",
7502
+ "Picks up new kobe code without `kobe reset` \u2014 the engine (claude)",
7503
+ "panes and your running turns are never touched. Takes no options.",
7504
+ ""
7505
+ ].join(`
7506
+ `));
7507
+ return;
7508
+ }
7509
+ const unknown = argv.find((a) => a.length > 0);
7510
+ if (unknown !== undefined) {
7511
+ process.stderr.write(`kobe reload: unexpected argument "${unknown}"
7512
+
7513
+ Usage: kobe reload (takes no options)
7514
+ `);
7515
+ process.exit(2);
7516
+ }
7517
+ if (!await tmuxAvailable()) {
7518
+ console.log("kobe reload: tmux is not installed \u2014 no panes to reload");
7519
+ return;
7520
+ }
7521
+ const { code, stdout } = await tmuxQuiet(["list-sessions", "-F", "#{session_name}"]);
7522
+ if (code !== 0) {
7523
+ console.log(`kobe reload: no kobe tmux sessions on the \`${KOBE_TMUX_SOCKET}\` socket`);
7524
+ return;
7525
+ }
7526
+ const sessions = stdout.split(`
7527
+ `).map((l) => l.trim()).filter((l) => l.length > 0);
7528
+ if (sessions.length === 0) {
7529
+ console.log(`kobe reload: no kobe tmux sessions on the \`${KOBE_TMUX_SOCKET}\` socket`);
7530
+ return;
7531
+ }
7532
+ const { refreshKobeWorkspacePanes: refreshKobeWorkspacePanes2 } = await Promise.resolve().then(() => (init_tmux(), exports_tmux));
7533
+ let reloaded = 0;
7534
+ for (const session of sessions) {
7535
+ try {
7536
+ await refreshKobeWorkspacePanes2(session);
7537
+ reloaded++;
7538
+ } catch (err) {
7539
+ console.error(` failed to reload session "${session}": ${err instanceof Error ? err.message : String(err)}`);
7540
+ }
7541
+ }
7542
+ console.log(`kobe: reloaded Tasks/Ops panes in ${reloaded}/${sessions.length} session(s) \u2014 engine panes untouched`);
7543
+ }
7329
7544
  var init_maintenance = __esm(() => {
7330
7545
  init_client();
7331
7546
  init_lifecycle();
@@ -8625,6 +8840,7 @@ class RemoteOrchestrator {
8625
8840
  setConnectionState;
8626
8841
  ensureReachable;
8627
8842
  role;
8843
+ reconnecting = false;
8628
8844
  constructor(client, options = {}) {
8629
8845
  this.client = client;
8630
8846
  const [tasks, setTasks] = createSignal([]);
@@ -8642,7 +8858,37 @@ class RemoteOrchestrator {
8642
8858
  this.ensureReachable = options.ensureReachable ?? ensureDaemonReachable;
8643
8859
  this.role = options.role ?? "pane";
8644
8860
  this.client.on("*", (frame) => this.handleEvent(frame.name, frame.payload));
8645
- this.client.onLifecycle("close", () => this.setConnectionState("disconnected"));
8861
+ this.client.onLifecycle("close", () => {
8862
+ this.setConnectionState("disconnected");
8863
+ if (this.role === "pane") {
8864
+ logClient("orch", "daemon socket closed \u2014 starting non-spawning reconnect loop");
8865
+ this.reconnectLoop();
8866
+ }
8867
+ });
8868
+ }
8869
+ async reconnectLoop() {
8870
+ if (this.reconnecting)
8871
+ return;
8872
+ this.reconnecting = true;
8873
+ let delayMs = 500;
8874
+ let attempt = 0;
8875
+ while (!this.client.isDisposed) {
8876
+ await new Promise((r) => setTimeout(r, delayMs));
8877
+ if (this.client.isDisposed)
8878
+ break;
8879
+ attempt++;
8880
+ try {
8881
+ await this.init();
8882
+ logClient("orch", `reconnected and re-subscribed after ${attempt} attempt(s) \u2014 task list re-synced`);
8883
+ this.reconnecting = false;
8884
+ return;
8885
+ } catch (err) {
8886
+ if (attempt === 1 || attempt % 10 === 0)
8887
+ logClientError("orch-reconnect", err);
8888
+ delayMs = Math.min(delayMs * 2, 3000);
8889
+ }
8890
+ }
8891
+ this.reconnecting = false;
8646
8892
  }
8647
8893
  async init() {
8648
8894
  const hello = await this.client.request("hello", {
@@ -8663,6 +8909,7 @@ class RemoteOrchestrator {
8663
8909
  this.setTasks(hello.tasks.map(deserializeTask));
8664
8910
  await this.client.subscribe({ role: this.role });
8665
8911
  this.setConnectionState("online");
8912
+ logClient("orch", `subscribed as ${this.role} (${this.tasksAcc().length} tasks)`);
8666
8913
  }
8667
8914
  connectionStateSignal() {
8668
8915
  return this.connectionStateAcc;
@@ -8796,6 +9043,7 @@ function deserializeTask(s) {
8796
9043
  var init_remote_orchestrator = __esm(() => {
8797
9044
  init_dev();
8798
9045
  init_protocol();
9046
+ init_client_log();
8799
9047
  init_daemon_process();
8800
9048
  });
8801
9049
 
@@ -12476,7 +12724,7 @@ var init_rename_task_dialog = __esm(() => {
12476
12724
  // src/engine/claude-code-local/binary.ts
12477
12725
  import { spawnSync as spawnSync5 } from "child_process";
12478
12726
  import { existsSync as existsSync7, statSync as statSync3 } from "fs";
12479
- import { homedir as homedir10 } from "os";
12727
+ import { homedir as homedir11 } from "os";
12480
12728
  import path7 from "path";
12481
12729
  async function findClaudeBinary(deps = defaultDeps4) {
12482
12730
  const checked = [];
@@ -12549,7 +12797,7 @@ var init_binary = __esm(() => {
12549
12797
  return process.env[name];
12550
12798
  },
12551
12799
  home() {
12552
- return homedir10();
12800
+ return homedir11();
12553
12801
  },
12554
12802
  which(name) {
12555
12803
  const cmd = process.platform === "win32" ? "where" : "which";
@@ -12580,7 +12828,7 @@ var init_binary = __esm(() => {
12580
12828
  // src/engine/codex-local/binary.ts
12581
12829
  import { spawnSync as spawnSync6 } from "child_process";
12582
12830
  import { existsSync as existsSync8, statSync as statSync4 } from "fs";
12583
- import { homedir as homedir11 } from "os";
12831
+ import { homedir as homedir12 } from "os";
12584
12832
  import path8 from "path";
12585
12833
  async function findCodexBinary(deps = defaultDeps5) {
12586
12834
  const checked = [];
@@ -12637,7 +12885,7 @@ var init_binary2 = __esm(() => {
12637
12885
  return process.env[name];
12638
12886
  },
12639
12887
  home() {
12640
- return homedir11();
12888
+ return homedir12();
12641
12889
  },
12642
12890
  which(name) {
12643
12891
  const cmd = process.platform === "win32" ? "where" : "which";
@@ -12668,7 +12916,7 @@ var init_binary2 = __esm(() => {
12668
12916
  // src/engine/copilot-local/binary.ts
12669
12917
  import { spawnSync as spawnSync7 } from "child_process";
12670
12918
  import { existsSync as existsSync9, statSync as statSync5 } from "fs";
12671
- import { homedir as homedir12 } from "os";
12919
+ import { homedir as homedir13 } from "os";
12672
12920
  import path9 from "path";
12673
12921
  async function findCopilotBinary(deps = defaultDeps6) {
12674
12922
  const checked = [];
@@ -12749,7 +12997,7 @@ var init_binary3 = __esm(() => {
12749
12997
  return process.env[name];
12750
12998
  },
12751
12999
  home() {
12752
- return homedir12();
13000
+ return homedir13();
12753
13001
  },
12754
13002
  which(name) {
12755
13003
  const cmd = process.platform === "win32" ? "where" : "which";
@@ -12774,7 +13022,7 @@ var init_binary3 = __esm(() => {
12774
13022
 
12775
13023
  // src/engine/account-detect.ts
12776
13024
  import { readFileSync as readFileSync7, statSync as statSync6 } from "fs";
12777
- import { homedir as homedir13 } from "os";
13025
+ import { homedir as homedir14 } from "os";
12778
13026
  import path10 from "path";
12779
13027
  function claudeGlobalConfigPath(env, home) {
12780
13028
  const override = env("CLAUDE_CONFIG_DIR")?.trim();
@@ -12992,7 +13240,7 @@ var init_account_detect = __esm(() => {
12992
13240
  return process.env[name];
12993
13241
  },
12994
13242
  home() {
12995
- return homedir13();
13243
+ return homedir14();
12996
13244
  },
12997
13245
  findClaudeBinary() {
12998
13246
  return findClaudeBinary();
@@ -14538,7 +14786,7 @@ var init_focus = __esm(() => {
14538
14786
 
14539
14787
  // src/tui/context/kv.tsx
14540
14788
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
14541
- import { dirname as dirname5 } from "path";
14789
+ import { dirname as dirname6 } from "path";
14542
14790
  function loadInitial() {
14543
14791
  const statePath2 = kvStatePath();
14544
14792
  try {
@@ -14566,7 +14814,7 @@ var init_kv = __esm(() => {
14566
14814
  function writeNow(label) {
14567
14815
  const statePath2 = kvStatePath();
14568
14816
  try {
14569
- mkdirSync4(dirname5(statePath2), {
14817
+ mkdirSync4(dirname6(statePath2), {
14570
14818
  recursive: true
14571
14819
  });
14572
14820
  const tmp = `${statePath2}.tmp`;
@@ -14621,7 +14869,7 @@ var init_kv = __esm(() => {
14621
14869
  }
14622
14870
  const statePath2 = kvStatePath();
14623
14871
  try {
14624
- mkdirSync4(dirname5(statePath2), {
14872
+ mkdirSync4(dirname6(statePath2), {
14625
14873
  recursive: true
14626
14874
  });
14627
14875
  const tmp = `${statePath2}.tmp`;
@@ -15771,6 +16019,8 @@ function Sidebar(props) {
15771
16019
  createEffect(on(view, () => {
15772
16020
  const ids = flatIds();
15773
16021
  setCursorIndex(ids.length > 0 ? 0 : -1);
16022
+ }, {
16023
+ defer: true
15774
16024
  }));
15775
16025
  createEffect(on([searchMode, searchQuery], () => {
15776
16026
  if (!searchMode())
@@ -16019,7 +16269,7 @@ function Sidebar(props) {
16019
16269
  return readWorktreeChanges(task.worktreePath);
16020
16270
  });
16021
16271
  const titleText = isMain ? repoBasename(task.repo) : task.title;
16022
- const loading = () => isLive() || task.status === "in_progress";
16272
+ const loading = () => isLive() || !isMain && task.status === "in_progress";
16023
16273
  const subtitleText = createMemo(() => {
16024
16274
  if (task.branch.length > 0)
16025
16275
  return truncateBranchLabel(task.branch, subtitleBudget());
@@ -16467,13 +16717,20 @@ function TasksShell(props) {
16467
16717
  } = themeCtx;
16468
16718
  const dialog = useDialog();
16469
16719
  const kv = useKV();
16470
- const [selectedId, setSelectedId] = createSignal(props.tasks()[0]?.id ?? null);
16720
+ const [selectedId, setSelectedId] = createSignal(props.tasks().some((t) => t.id === props.initialTaskId) ? props.initialTaskId : props.tasks()[0]?.id ?? null);
16471
16721
  const [updateInfo, setUpdateInfo] = createSignal(null);
16472
16722
  const dimensions = useTerminalDimensions();
16723
+ let activeConfirmed = !props.initialTaskId;
16473
16724
  createEffect(() => {
16474
16725
  const active = props.orch?.activeTaskSignal()();
16475
- if (active)
16476
- setSelectedId(active);
16726
+ if (!active)
16727
+ return;
16728
+ if (!activeConfirmed) {
16729
+ if (active !== props.initialTaskId)
16730
+ return;
16731
+ activeConfirmed = true;
16732
+ }
16733
+ setSelectedId(active);
16477
16734
  });
16478
16735
  onMount(() => {
16479
16736
  themeCtx.setTransparentBackground(props.transparent);
@@ -16582,6 +16839,7 @@ function TasksShell(props) {
16582
16839
  await switchClientBeforeKill(tmuxSessionName(id), nextTask ? tmuxSessionName(nextTask.id) : undefined).catch((err) => {
16583
16840
  console.error("[kobe tasks] switch-client failed:", err);
16584
16841
  });
16842
+ await props.orch.setActiveTask(nextTask?.id ?? null).catch(() => {});
16585
16843
  await killSession(tmuxSessionName(id)).catch((err) => {
16586
16844
  console.error("[kobe tasks] kill tmux session failed:", err);
16587
16845
  });
@@ -16627,13 +16885,14 @@ function TasksShell(props) {
16627
16885
  await switchClientBeforeKill(tmuxSessionName(id), nextTask ? tmuxSessionName(nextTask.id) : undefined).catch((err) => {
16628
16886
  console.error("[kobe tasks] switch-client failed:", err);
16629
16887
  });
16888
+ await props.orch.setActiveTask(nextTask?.id ?? null).catch(() => {});
16630
16889
  await killSession(tmuxSessionName(id)).catch((err) => {
16631
16890
  console.error("[kobe tasks] kill tmux session failed:", err);
16632
16891
  });
16633
16892
  await props.reload();
16634
16893
  if (selectedId() === id) {
16635
16894
  const remaining = props.tasks();
16636
- setSelectedId((remaining.find((t) => !t.archived) ?? remaining[0])?.id ?? null);
16895
+ setSelectedId(nextTask?.id ?? (remaining.find((t) => !t.archived) ?? remaining[0])?.id ?? null);
16637
16896
  }
16638
16897
  }
16639
16898
  async function renameTask(id) {
@@ -16949,7 +17208,8 @@ function ShortcutHints() {
16949
17208
  return _el$3;
16950
17209
  })();
16951
17210
  }
16952
- async function startTasksPane() {
17211
+ async function startTasksPane(opts = {}) {
17212
+ setClientLogContext("tasks");
16953
17213
  for (const {
16954
17214
  name,
16955
17215
  theme
@@ -16964,21 +17224,28 @@ async function startTasksPane() {
16964
17224
  const [fileTasks, setFileTasks] = createSignal(store2.list());
16965
17225
  let orch = null;
16966
17226
  try {
16967
- const client = await connectOrStartDaemon();
16968
- const remote = new RemoteOrchestrator(client);
16969
- await remote.init();
16970
- orch = remote;
17227
+ const client = await connectIfRunning();
17228
+ if (client) {
17229
+ const remote = new RemoteOrchestrator(client);
17230
+ await remote.init();
17231
+ orch = remote;
17232
+ } else {
17233
+ logClient("tasks-boot", "no daemon running \u2014 polling tasks.json (a gui owns daemon lifecycle)");
17234
+ }
16971
17235
  } catch (err) {
16972
- console.error("[kobe tasks] daemon subscribe unavailable, polling tasks.json:", err);
17236
+ logClientError("tasks-boot", err);
17237
+ logClient("tasks-boot", "daemon subscribe failed \u2014 polling tasks.json");
16973
17238
  }
16974
- const tasks = orch ? orch.tasksSignal() : fileTasks;
17239
+ const tasks = () => orch && orch.connectionStateSignal()() === "online" ? orch.tasksSignal()() : fileTasks();
16975
17240
  const reload = async () => {
16976
- if (orch)
16977
- return;
16978
17241
  await store2.load();
16979
17242
  setFileTasks(store2.list());
16980
17243
  };
16981
- const timer = orch ? undefined : setInterval(() => void reload(), RELOAD_MS);
17244
+ const timer = setInterval(() => {
17245
+ if (orch && orch.connectionStateSignal()() === "online")
17246
+ return;
17247
+ reload();
17248
+ }, RELOAD_MS);
16982
17249
  await render(() => createComponent2(ThemeProvider, {
16983
17250
  mode: "dark",
16984
17251
  get theme() {
@@ -16994,6 +17261,9 @@ async function startTasksPane() {
16994
17261
  get children() {
16995
17262
  return createComponent2(TasksShell, {
16996
17263
  tasks,
17264
+ get initialTaskId() {
17265
+ return opts.initialTaskId;
17266
+ },
16997
17267
  orch,
16998
17268
  get transparent() {
16999
17269
  return prefs.transparent;
@@ -17035,6 +17305,7 @@ var init_host = __esm(() => {
17035
17305
  init_client2();
17036
17306
  init_solid();
17037
17307
  init_dev();
17308
+ init_client_log();
17038
17309
  init_daemon_process();
17039
17310
  init_remote_orchestrator();
17040
17311
  init_interactive_command();
@@ -19135,14 +19406,16 @@ function OpsShell(props) {
19135
19406
  onMount(() => {
19136
19407
  let disposed = false;
19137
19408
  async function poll() {
19138
- const mtime = await latestTranscriptMtime(props.vendor, props.worktree);
19139
- if (disposed)
19140
- return;
19141
- if (!primed) {
19142
- primed = true;
19143
- setBaseline(mtime);
19144
- }
19145
- setLatest(mtime);
19409
+ try {
19410
+ const mtime = await latestTranscriptMtime(props.vendor, props.worktree);
19411
+ if (disposed)
19412
+ return;
19413
+ if (!primed) {
19414
+ primed = true;
19415
+ setBaseline(mtime);
19416
+ }
19417
+ setLatest(mtime);
19418
+ } catch {}
19146
19419
  }
19147
19420
  poll();
19148
19421
  const timer = setInterval(() => void poll(), ACTIVITY_POLL_MS);
@@ -19176,31 +19449,35 @@ function OpsShell(props) {
19176
19449
  await setWindowOption(props.targetPane, CHAT_TAB_STATE_OPTION2, state);
19177
19450
  }
19178
19451
  async function prime() {
19179
- paneHash = fingerprint(await capturePaneById(props.targetPane, 80));
19180
- baselineCompletionId = (await detector.latestCompletion(props.worktree))?.id ?? null;
19181
- await publish(detector.supportsCompletionMarkers() ? "idle" : "unknown");
19452
+ try {
19453
+ paneHash = fingerprint(await capturePaneById(props.targetPane, 80));
19454
+ baselineCompletionId = (await detector.latestCompletion(props.worktree))?.id ?? null;
19455
+ await publish(detector.supportsCompletionMarkers() ? "idle" : "unknown");
19456
+ } catch {}
19182
19457
  }
19183
19458
  async function poll() {
19184
- const nextPaneHash = fingerprint(await capturePaneById(props.targetPane, 80));
19185
- if (disposed)
19186
- return;
19187
- if (nextPaneHash !== paneHash) {
19188
- paneHash = nextPaneHash;
19189
- observedPaneActivity = true;
19459
+ try {
19460
+ const nextPaneHash = fingerprint(await capturePaneById(props.targetPane, 80));
19461
+ if (disposed)
19462
+ return;
19463
+ if (nextPaneHash !== paneHash) {
19464
+ paneHash = nextPaneHash;
19465
+ observedPaneActivity = true;
19466
+ stablePolls = 0;
19467
+ await publish(detector.supportsCompletionMarkers() ? "running" : "unknown");
19468
+ } else if (observedPaneActivity) {
19469
+ stablePolls++;
19470
+ }
19471
+ if (!detector.supportsCompletionMarkers() || !observedPaneActivity || stablePolls < STABLE_POLLS_FOR_DONE)
19472
+ return;
19473
+ const marker = await detector.latestCompletion(props.worktree);
19474
+ if (disposed || !marker || marker.id === baselineCompletionId)
19475
+ return;
19476
+ baselineCompletionId = marker.id;
19477
+ observedPaneActivity = false;
19190
19478
  stablePolls = 0;
19191
- await publish(detector.supportsCompletionMarkers() ? "running" : "unknown");
19192
- } else if (observedPaneActivity) {
19193
- stablePolls++;
19194
- }
19195
- if (!detector.supportsCompletionMarkers() || !observedPaneActivity || stablePolls < STABLE_POLLS_FOR_DONE)
19196
- return;
19197
- const marker = await detector.latestCompletion(props.worktree);
19198
- if (disposed || !marker || marker.id === baselineCompletionId)
19199
- return;
19200
- baselineCompletionId = marker.id;
19201
- observedPaneActivity = false;
19202
- stablePolls = 0;
19203
- await publish("done");
19479
+ await publish("done");
19480
+ } catch {}
19204
19481
  }
19205
19482
  prime();
19206
19483
  const timer = setInterval(() => void poll(), TURN_STATUS_POLL_MS);
@@ -19286,6 +19563,7 @@ function OpsApp(props) {
19286
19563
  });
19287
19564
  }
19288
19565
  async function startOpsHost(args) {
19566
+ setClientLogContext("ops");
19289
19567
  for (const {
19290
19568
  name,
19291
19569
  theme
@@ -19557,6 +19835,7 @@ var init_host5 = __esm(() => {
19557
19835
  init_solid();
19558
19836
  init_solid();
19559
19837
  init_invocation();
19838
+ init_client_log();
19560
19839
  init_turn_detector();
19561
19840
  init_activity();
19562
19841
  init_client2();
@@ -19627,6 +19906,7 @@ async function ensureRepos(orchestrator) {
19627
19906
  return repos[0] ?? resolve6(process.cwd());
19628
19907
  }
19629
19908
  async function startDirectTmux() {
19909
+ setClientLogContext("gui");
19630
19910
  if (!await tmuxAvailable()) {
19631
19911
  console.error("kobe: tmux not found on PATH \u2014 install tmux to use kobe 0.6 direct mode");
19632
19912
  process.exitCode = 1;
@@ -19644,8 +19924,11 @@ async function startDirectTmux() {
19644
19924
  cwdRepo
19645
19925
  });
19646
19926
  if (!task) {
19647
- console.error("kobe: no task available to enter");
19648
- process.exitCode = 1;
19927
+ const home = await ensureFallbackSession();
19928
+ if (await attachTmux(attachArgv(home)) === null) {
19929
+ console.error("kobe: failed to attach to the kobe-home session");
19930
+ process.exitCode = 1;
19931
+ }
19649
19932
  return;
19650
19933
  }
19651
19934
  const cwd = task.worktreePath || await orchestrator.ensureWorktree(task.id);
@@ -19693,6 +19976,7 @@ async function startDirectTmux() {
19693
19976
  }
19694
19977
  }
19695
19978
  var init_direct = __esm(() => {
19979
+ init_client_log();
19696
19980
  init_daemon_process();
19697
19981
  init_remote_orchestrator();
19698
19982
  init_interactive_command();
@@ -19700,6 +19984,7 @@ var init_direct = __esm(() => {
19700
19984
  init_core();
19701
19985
  init_repo_init();
19702
19986
  init_repos();
19987
+ init_client2();
19703
19988
  init_tmux();
19704
19989
  });
19705
19990
 
@@ -21335,7 +21620,7 @@ var exports_app = {};
21335
21620
  __export(exports_app, {
21336
21621
  startApp: () => startApp
21337
21622
  });
21338
- import { homedir as homedir14 } from "os";
21623
+ import { homedir as homedir15 } from "os";
21339
21624
  function Shell(props) {
21340
21625
  const themeCtx = useTheme();
21341
21626
  const {
@@ -21843,7 +22128,7 @@ async function startApp() {
21843
22128
  } of loadUserThemes()) {
21844
22129
  addTheme2(name, theme);
21845
22130
  }
21846
- const homeDir2 = process.env.KOBE_HOME_DIR ?? homedir14();
22131
+ const homeDir2 = process.env.KOBE_HOME_DIR ?? homedir15();
21847
22132
  let orchestrator;
21848
22133
  if (process.env.KOBE_NO_DAEMON === "1") {
21849
22134
  const store2 = new TaskIndexStore({
@@ -21978,6 +22263,7 @@ function topLevelUsage() {
21978
22263
  " update [target] Self-update kobe",
21979
22264
  " doctor Diagnose daemon / tmux / state (read-only)",
21980
22265
  " reset [--hard] Recover a wedged install",
22266
+ " reload Restart Tasks/Ops panes in place (engine untouched)",
21981
22267
  " kill-sessions Tear down kobe's tmux server (dev reset)",
21982
22268
  "",
21983
22269
  "Options:",
@@ -22168,6 +22454,9 @@ function parseOpsFlags(argv) {
22168
22454
  } else if (flag === "--repo") {
22169
22455
  flags.repo = value;
22170
22456
  i++;
22457
+ } else if (flag === "--initial-task-id") {
22458
+ flags.initialTaskId = value;
22459
+ i++;
22171
22460
  }
22172
22461
  }
22173
22462
  return flags;
@@ -22227,6 +22516,11 @@ async function main() {
22227
22516
  await runResetSubcommand2(rest);
22228
22517
  return;
22229
22518
  }
22519
+ if (subcommand === "reload") {
22520
+ const { runReloadSubcommand: runReloadSubcommand2 } = await Promise.resolve().then(() => (init_maintenance(), exports_maintenance));
22521
+ await runReloadSubcommand2(rest);
22522
+ return;
22523
+ }
22230
22524
  if (subcommand === "new-chattab") {
22231
22525
  const flags = parseOpsFlags(rest);
22232
22526
  const session = flags.session;
@@ -22264,8 +22558,9 @@ async function main() {
22264
22558
  return;
22265
22559
  }
22266
22560
  if (subcommand === "tasks") {
22561
+ const flags = parseOpsFlags(rest);
22267
22562
  const { startTasksPane: startTasksPane2 } = await Promise.resolve().then(() => (init_host(), exports_host));
22268
- await startTasksPane2();
22563
+ await startTasksPane2({ initialTaskId: flags.initialTaskId });
22269
22564
  return;
22270
22565
  }
22271
22566
  if (subcommand === "settings") {
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.0",
4
+ "version": "0.7.2",
5
5
  "description": "TUI orchestrator for Claude Code (codename)",
6
6
  "type": "module",
7
7
  "packageManager": "bun@1.3.13",