@band-app/server 0.54.2 → 0.54.4

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 (76) hide show
  1. package/dist/client/_shell.html +0 -0
  2. package/dist/client/assets/{TerminalPanel-zm8MgtWD.js → TerminalPanel-CxzXrgZc.js} +1 -1
  3. package/dist/client/assets/{_basePickBy-Df9AWPQJ.js → _basePickBy-ESHGoJr1.js} +1 -1
  4. package/dist/client/assets/{_baseUniq-C8iGnWYN.js → _baseUniq-Caza5c4E.js} +1 -1
  5. package/dist/client/assets/{arc-Dotr_YMW.js → arc-DaZonhJY.js} +1 -1
  6. package/dist/client/assets/{architectureDiagram-VXUJARFQ-BkG8VmpL.js → architectureDiagram-VXUJARFQ-Cq7YbAnJ.js} +1 -1
  7. package/dist/client/assets/{blockDiagram-VD42YOAC-DZYGOHoq.js → blockDiagram-VD42YOAC-C8s0ghb3.js} +1 -1
  8. package/dist/client/assets/{c4Diagram-YG6GDRKO-Ce9RV_Np.js → c4Diagram-YG6GDRKO-DIBhFHGh.js} +1 -1
  9. package/dist/client/assets/channel-D_oNvIqF.js +1 -0
  10. package/dist/client/assets/{chunk-4BX2VUAB-Ck1qaTAA.js → chunk-4BX2VUAB-D3SZ3P8V.js} +1 -1
  11. package/dist/client/assets/{chunk-55IACEB6-CeH02gXd.js → chunk-55IACEB6-BfA-9jtI.js} +1 -1
  12. package/dist/client/assets/{chunk-B4BG7PRW-DqBzx79X.js → chunk-B4BG7PRW-DAygCOyX.js} +1 -1
  13. package/dist/client/assets/{chunk-DI55MBZ5-p68JZT5Z.js → chunk-DI55MBZ5-CuikvpNq.js} +1 -1
  14. package/dist/client/assets/{chunk-FMBD7UC4-DC8dY12t.js → chunk-FMBD7UC4-JCmcJDsw.js} +1 -1
  15. package/dist/client/assets/{chunk-QN33PNHL-D1Af7sn_.js → chunk-QN33PNHL-3o6_7ioT.js} +1 -1
  16. package/dist/client/assets/{chunk-QZHKN3VN-BHBPr8iM.js → chunk-QZHKN3VN-5B8nia1f.js} +1 -1
  17. package/dist/client/assets/{chunk-TZMSLE5B-BP9v1VXR.js → chunk-TZMSLE5B-stQp3cfp.js} +1 -1
  18. package/dist/client/assets/classDiagram-2ON5EDUG-C8EJzaAq.js +1 -0
  19. package/dist/client/assets/classDiagram-v2-WZHVMYZB-C8EJzaAq.js +1 -0
  20. package/dist/client/assets/clone-BhU1fGlA.js +1 -0
  21. package/dist/client/assets/{cose-bilkent-S5V4N54A-C7S-iRjs.js → cose-bilkent-S5V4N54A-CVQzgyKs.js} +1 -1
  22. package/dist/client/assets/{dagre-6UL2VRFP-CIdLKDDa.js → dagre-6UL2VRFP-C95ZZlB4.js} +1 -1
  23. package/dist/client/assets/{diagram-PSM6KHXK-_VfU4I5J.js → diagram-PSM6KHXK-Sb4x5piz.js} +1 -1
  24. package/dist/client/assets/{diagram-QEK2KX5R-ByPoixdO.js → diagram-QEK2KX5R-TTDYZins.js} +1 -1
  25. package/dist/client/assets/{diagram-S2PKOQOG-CU2nF5Pi.js → diagram-S2PKOQOG-C8joLdiY.js} +1 -1
  26. package/dist/client/assets/{erDiagram-Q2GNP2WA-DuReqla0.js → erDiagram-Q2GNP2WA-HXkzVcZE.js} +1 -1
  27. package/dist/client/assets/{flowDiagram-NV44I4VS-BzbJhnQR.js → flowDiagram-NV44I4VS-BPzPMXG7.js} +1 -1
  28. package/dist/client/assets/{ganttDiagram-JELNMOA3-BeBP4O51.js → ganttDiagram-JELNMOA3-5JGkIYiO.js} +1 -1
  29. package/dist/client/assets/{gitGraphDiagram-V2S2FVAM-7rU4MgM6.js → gitGraphDiagram-V2S2FVAM-CDCkC2dq.js} +1 -1
  30. package/dist/client/assets/{graph-v9lPIGDx.js → graph-CQ3VI3Kb.js} +1 -1
  31. package/dist/client/assets/{highlighted-body-B3W2YXNL-Bt5DPhhI.js → highlighted-body-B3W2YXNL-jA7b7ztt.js} +1 -1
  32. package/dist/client/assets/{index-XAyPsPHs.js → index-0DuoTbku.js} +1 -1
  33. package/dist/client/assets/{index-Dy40JkoS.js → index-B6FlIvZu.js} +1 -1
  34. package/dist/client/assets/{index-DjGPjARU.js → index-Bcr3D2nL.js} +1 -1
  35. package/dist/client/assets/{index-jC0z3Kaq.js → index-BedjQBXV.js} +1 -1
  36. package/dist/client/assets/{index-CJefa5Go.js → index-C7Xj47iO.js} +1 -1
  37. package/dist/client/assets/{index-CsZW3tVv.js → index-CZqDBAUL.js} +1 -1
  38. package/dist/client/assets/{index-Qk1h8PGS.js → index-Ce8SticC.js} +1 -1
  39. package/dist/client/assets/{index-D2wFO-D3.js → index-DWepK3T1.js} +1 -1
  40. package/dist/client/assets/{index-DrM0qB0a.js → index-Dt3jdJaQ.js} +1 -1
  41. package/dist/client/assets/{index-Be1D1vwS.js → index-F2GbFYex.js} +1 -1
  42. package/dist/client/assets/{index-DoGYlvHl.js → index-SjDPUTbU.js} +1 -1
  43. package/dist/client/assets/{index-DGN6wKkW.js → index-sTT9joMB.js} +1 -1
  44. package/dist/client/assets/{index-Q6D-8JjG.js → index-xEgVFQfI.js} +1 -1
  45. package/dist/client/assets/{infoDiagram-HS3SLOUP-BOjyAfYW.js → infoDiagram-HS3SLOUP-DXlzs7_8.js} +1 -1
  46. package/dist/client/assets/{journeyDiagram-XKPGCS4Q-daUwYLfO.js → journeyDiagram-XKPGCS4Q-B7mdzrEf.js} +1 -1
  47. package/dist/client/assets/{kanban-definition-3W4ZIXB7-Dkcl1vfA.js → kanban-definition-3W4ZIXB7-C3yAE4ky.js} +1 -1
  48. package/dist/client/assets/{layout-CSSEtaEF.js → layout-BpyKMBbb.js} +1 -1
  49. package/dist/client/assets/{main-BzXVe5ia.js → main-B169v3Mr.js} +245 -245
  50. package/dist/client/assets/main-DWmz5QXO.css +1 -0
  51. package/dist/client/assets/{mindmap-definition-VGOIOE7T-D17_XGFA.js → mindmap-definition-VGOIOE7T-BV1ylQ_o.js} +1 -1
  52. package/dist/client/assets/{pieDiagram-ADFJNKIX-D07iYAxA.js → pieDiagram-ADFJNKIX-C68Mdt_x.js} +1 -1
  53. package/dist/client/assets/{quadrantDiagram-AYHSOK5B-B9imU2k-.js → quadrantDiagram-AYHSOK5B-5wmdYa_y.js} +1 -1
  54. package/dist/client/assets/{requirementDiagram-UZGBJVZJ-CMH8dVSi.js → requirementDiagram-UZGBJVZJ-CXCtBgk5.js} +1 -1
  55. package/dist/client/assets/{sankeyDiagram-TZEHDZUN-CWS08bde.js → sankeyDiagram-TZEHDZUN-CwuKYu6l.js} +1 -1
  56. package/dist/client/assets/{sequenceDiagram-WL72ISMW-BcDiQ1t6.js → sequenceDiagram-WL72ISMW-BwFU-IAs.js} +1 -1
  57. package/dist/client/assets/{stateDiagram-FKZM4ZOC-Be7DSpGK.js → stateDiagram-FKZM4ZOC-B46B2gqH.js} +1 -1
  58. package/dist/client/assets/stateDiagram-v2-4FDKWEC3-BvDRMNEQ.js +1 -0
  59. package/dist/client/assets/{timeline-definition-IT6M3QCI-BwTGYKzr.js → timeline-definition-IT6M3QCI-DHJrxfmL.js} +1 -1
  60. package/dist/client/assets/{treemap-GDKQZRPO-DDASk1M2.js → treemap-GDKQZRPO-Bn3yodwG.js} +1 -1
  61. package/dist/client/assets/{worktree._worktreeId-C3DGXQD8.js → worktree._worktreeId-7oupLNxr.js} +1 -1
  62. package/dist/client/assets/{worktree._worktreeId-DsTjUoT9.js → worktree._worktreeId-MU4bksud.js} +1 -1
  63. package/dist/client/assets/{xychartDiagram-PRI3JC2R-CShK1tn5.js → xychartDiagram-PRI3JC2R-DUqpx4dY.js} +1 -1
  64. package/dist/migrations/20261028200323_worktree_title/migration.sql +1 -0
  65. package/dist/migrations/20261028200323_worktree_title/snapshot.json +5228 -0
  66. package/dist/migrations/20261029080757_worktree_unread/migration.sql +1 -0
  67. package/dist/migrations/20261029080757_worktree_unread/snapshot.json +5238 -0
  68. package/dist/openapi.json +186 -1
  69. package/dist/start-server.mjs +411 -54
  70. package/package.json +11 -6
  71. package/dist/client/assets/channel-BQWEKiCC.js +0 -1
  72. package/dist/client/assets/classDiagram-2ON5EDUG-Bkz5oJ-y.js +0 -1
  73. package/dist/client/assets/classDiagram-v2-WZHVMYZB-Bkz5oJ-y.js +0 -1
  74. package/dist/client/assets/clone-Dr7z_3vQ.js +0 -1
  75. package/dist/client/assets/main-BYKQWpTo.css +0 -1
  76. package/dist/client/assets/stateDiagram-v2-4FDKWEC3-CSGrJZGI.js +0 -1
@@ -2556,7 +2556,7 @@ var require_thread_stream = __commonJS({
2556
2556
  var require_transport = __commonJS({
2557
2557
  "../../node_modules/.pnpm/pino@9.14.0/node_modules/pino/lib/transport.js"(exports, module) {
2558
2558
  "use strict";
2559
- var { createRequire: createRequire2 } = __require("module");
2559
+ var { createRequire: createRequire3 } = __require("module");
2560
2560
  var getCallers = require_caller();
2561
2561
  var { join: join59, isAbsolute: isAbsolute7, sep: sep11 } = __require("node:path");
2562
2562
  var sleep3 = require_atomic_sleep();
@@ -2667,7 +2667,7 @@ var require_transport = __commonJS({
2667
2667
  for (const filePath of callers) {
2668
2668
  try {
2669
2669
  const context = filePath === "node:repl" ? process.cwd() + sep11 : filePath;
2670
- fixTarget2 = createRequire2(context).resolve(origin);
2670
+ fixTarget2 = createRequire3(context).resolve(origin);
2671
2671
  break;
2672
2672
  } catch (err) {
2673
2673
  continue;
@@ -51994,6 +51994,11 @@ var init_schema2 = __esm({
51994
51994
  // id with no foreign key, like the origin columns: only `RepoQueries.setGroup` writes it, and
51995
51995
  // deleting a group nulls it.
51996
51996
  groupId: text3("group_id"),
51997
+ // Marked unread by a person: the sidebar shows a dot on every device until the worktree is
51998
+ // opened or marked read. Only `RepoQueries.setUnread` writes it.
51999
+ unread: integer4("unread", { mode: "boolean" }).notNull().default(false),
52000
+ // A custom title shown in place of the name, or null. Only `RepoQueries.setTitle` writes it.
52001
+ title: text3("title"),
51997
52002
  // Set when the worktree was restored on another host: the id of the replacement. The copy
51998
52003
  // stays on disk untouched and is listed as superseded until a person removes it.
51999
52004
  supersededBy: text3("superseded_by"),
@@ -54078,8 +54083,12 @@ var init_mcp_stdio = __esm({
54078
54083
 
54079
54084
  // ../../packages/host-local/src/process/cli-binary.ts
54080
54085
  import { lstatSync as lstatSync3, realpathSync as realpathSync4 } from "node:fs";
54086
+ import { createRequire as createRequire2 } from "node:module";
54081
54087
  import { platform } from "node:os";
54082
- import { delimiter as delimiter3, join as join19, resolve as resolve4 } from "node:path";
54088
+ import { delimiter as delimiter3, dirname as dirOf, join as join19, resolve as resolve4 } from "node:path";
54089
+ function cliPackageName(os = platform(), arch = process.arch) {
54090
+ return `@band-app/worker-cli-${os}-${arch}`;
54091
+ }
54083
54092
  function findCliBinaryAt(opts) {
54084
54093
  const { cwd, dirname: dirname18 } = opts;
54085
54094
  const exe = platform() === "win32" ? "band.exe" : "band";
@@ -54131,6 +54140,15 @@ function findCliBinaryAt(opts) {
54131
54140
  } catch {
54132
54141
  }
54133
54142
  }
54143
+ try {
54144
+ const pkgJson = createRequire2(join19(dirname18, "noop.js")).resolve(
54145
+ `${cliPackageName()}/package.json`
54146
+ );
54147
+ const p2 = join19(dirOf(pkgJson), "bin", exe);
54148
+ lstatSync3(p2);
54149
+ return p2;
54150
+ } catch {
54151
+ }
54134
54152
  for (const dir of (process.env.PATH ?? "").split(delimiter3)) {
54135
54153
  if (!dir) continue;
54136
54154
  try {
@@ -61047,8 +61065,42 @@ import {
61047
61065
  import { homedir as homedir16, hostname as hostname4, tmpdir as tmpdir2 } from "node:os";
61048
61066
  import { join as join34 } from "node:path";
61049
61067
  async function createWorktree(spec) {
61068
+ const refs = await execGit(
61069
+ ["for-each-ref", "--format=%(refname)", "refs/heads", "refs/remotes/origin"],
61070
+ spec.repoPath
61071
+ );
61072
+ const local = [];
61073
+ const known = [];
61074
+ for (const ref of refs.split("\n").filter(Boolean)) {
61075
+ if (ref.startsWith("refs/heads/")) {
61076
+ local.push(ref.slice("refs/heads/".length));
61077
+ known.push(ref.slice("refs/heads/".length));
61078
+ } else if (ref.startsWith("refs/remotes/origin/")) {
61079
+ known.push(ref.slice("refs/remotes/origin/".length));
61080
+ }
61081
+ }
61082
+ const clash = known.find(
61083
+ (b2) => b2 !== spec.branch && b2.toLowerCase() === spec.branch.toLowerCase()
61084
+ );
61085
+ if (clash) {
61086
+ throw new Error(
61087
+ `Branch "${spec.branch}" differs only in case from the existing branch "${clash}". Use "${clash}" or pick another name.`
61088
+ );
61089
+ }
61050
61090
  const args = ["worktree", "add", "-b", spec.branch, spec.path];
61051
- if (spec.base) args.push(spec.base);
61091
+ if (spec.base) {
61092
+ args.push(spec.base);
61093
+ } else if (!local.includes(spec.branch)) {
61094
+ const remote = `origin/${spec.branch}`;
61095
+ const found = await execGit(
61096
+ ["rev-parse", "--verify", "--quiet", `refs/remotes/${remote}`],
61097
+ spec.repoPath
61098
+ ).then(() => true).catch(() => false);
61099
+ if (found) {
61100
+ args.splice(2, 0, "--track");
61101
+ args.push(remote);
61102
+ }
61103
+ }
61052
61104
  await execGit(args, spec.repoPath);
61053
61105
  return { path: spec.path, branch: spec.branch };
61054
61106
  }
@@ -61379,8 +61431,8 @@ var init_local_host = __esm({
61379
61431
  return new Uint8Array(await readFile8(path2));
61380
61432
  },
61381
61433
  realpath: (path2) => realpath3(path2),
61382
- async *readStream(path2) {
61383
- for await (const chunk of createReadStream3(path2)) {
61434
+ async *readStream(path2, range) {
61435
+ for await (const chunk of createReadStream3(path2, range)) {
61384
61436
  const buffer = chunk;
61385
61437
  yield new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
61386
61438
  }
@@ -61443,7 +61495,21 @@ function reconcileKindForRepo(repo) {
61443
61495
  if (detectedKind === repo.kind) return false;
61444
61496
  repo.kind = detectedKind;
61445
61497
  if (detectedKind === "plain") {
61446
- repo.worktrees = [{ name: "main", branch: "main", path: repo.path, pinned: false }];
61498
+ const main2 = repo.worktrees.find(
61499
+ (wt) => wt.name === "main" && (wt.hostId ?? "local") === "local"
61500
+ );
61501
+ repo.worktrees = [
61502
+ {
61503
+ name: "main",
61504
+ branch: "main",
61505
+ path: repo.path,
61506
+ pinned: false,
61507
+ ...main2?.origin ? { origin: main2.origin } : {},
61508
+ ...main2?.groupId ? { groupId: main2.groupId } : {},
61509
+ ...main2?.unread ? { unread: true } : {},
61510
+ ...main2?.title ? { title: main2.title } : {}
61511
+ }
61512
+ ];
61447
61513
  }
61448
61514
  return true;
61449
61515
  }
@@ -61492,6 +61558,8 @@ var init_repos = __esm({
61492
61558
  }
61493
61559
  } : {},
61494
61560
  ...row.groupId ? { groupId: row.groupId } : {},
61561
+ ...row.unread ? { unread: true } : {},
61562
+ ...row.title ? { title: row.title } : {},
61495
61563
  ...row.supersededBy ? { supersededBy: row.supersededBy } : {},
61496
61564
  ...row.continuedFrom ? { continuedFrom: row.continuedFrom } : {},
61497
61565
  ...row.divergence ? { divergence: row.divergence } : {}
@@ -61545,7 +61613,9 @@ var init_repos = __esm({
61545
61613
  originWorktreeId: worktrees.originWorktreeId,
61546
61614
  originChatId: worktrees.originChatId,
61547
61615
  originTerminalId: worktrees.originTerminalId,
61548
- groupId: worktrees.groupId
61616
+ groupId: worktrees.groupId,
61617
+ unread: worktrees.unread,
61618
+ title: worktrees.title
61549
61619
  }).from(worktrees).all().map((w2) => [`${w2.repoName}\0${w2.name}`, w2])
61550
61620
  );
61551
61621
  const continuation = new Map(
@@ -61587,6 +61657,8 @@ var init_repos = __esm({
61587
61657
  originChatId: origin?.originChatId ?? null,
61588
61658
  originTerminalId: origin?.originTerminalId ?? null,
61589
61659
  groupId: origin?.groupId ?? null,
61660
+ unread: origin?.unread ?? false,
61661
+ title: origin?.title ?? null,
61590
61662
  repoName: repo.name,
61591
61663
  name: wt.name,
61592
61664
  branch: wt.branch,
@@ -61795,6 +61867,16 @@ var init_repos = __esm({
61795
61867
  }).from(repos).where(eq2(repos.name, name5)).get();
61796
61868
  return row ? { allowedHostIds: row.allowedHostIds ?? null, primaryHostId: row.primaryHostId ?? null } : void 0;
61797
61869
  }
61870
+ /** Sets a worktree's custom title, or clears it with `null`. Focused UPDATE, like `setGroup`. */
61871
+ setTitle(repoName, name5, title, hostId2) {
61872
+ getDb().update(worktrees).set({ title }).where(
61873
+ and2(
61874
+ eq2(worktrees.repoName, repoName),
61875
+ eq2(worktrees.name, name5),
61876
+ ...hostId2 === void 0 ? [] : [eq2(worktrees.hostId, hostId2)]
61877
+ )
61878
+ ).run();
61879
+ }
61798
61880
  /**
61799
61881
  * Puts a worktree in a group, or in none with `null`. Focused UPDATE, because `saveAll` keeps
61800
61882
  * the stored group and never writes the one in its snapshot.
@@ -61808,6 +61890,19 @@ var init_repos = __esm({
61808
61890
  )
61809
61891
  ).run();
61810
61892
  }
61893
+ /**
61894
+ * Marks a worktree unread or read. Focused UPDATE, because `saveAll` keeps the stored flag and
61895
+ * never writes the one in its snapshot.
61896
+ */
61897
+ setUnread(repoName, name5, unread, hostId2) {
61898
+ getDb().update(worktrees).set({ unread }).where(
61899
+ and2(
61900
+ eq2(worktrees.repoName, repoName),
61901
+ eq2(worktrees.name, name5),
61902
+ ...hostId2 === void 0 ? [] : [eq2(worktrees.hostId, hostId2)]
61903
+ )
61904
+ ).run();
61905
+ }
61811
61906
  /**
61812
61907
  * Records where a worktree was started from, or clears it with `null`. Focused UPDATE, because
61813
61908
  * `saveAll` keeps the stored origin and never writes the one in its snapshot.
@@ -66889,13 +66984,28 @@ var init_git_refusals = __esm({
66889
66984
 
66890
66985
  // ../../packages/shared/src/branch-name.ts
66891
66986
  function slugifyBranchName(input2) {
66892
- return input2.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9\-/]/g, "").replace(/-{2,}/g, "-").replace(/\/+/g, "/").replace(/^[-/]+|[-/]+$/g, "");
66987
+ let name5 = input2.trim().replace(/\s+/g, "-").replace(/[\x00-\x1f\x7f~^:?*[\\]/g, "").replace(/@\{/g, "").replace(/^@$/, "");
66988
+ let previous;
66989
+ do {
66990
+ previous = name5;
66991
+ name5 = name5.replace(/\.{2,}/g, ".").replace(/@\{/g, "").replace(/-{2,}/g, "-").replace(/\/+/g, "/").replace(/\/\./g, "/").replace(/\.lock(?=\/|$)/g, "").replace(/\.+(?=\/|$)/g, "").replace(/^[-/.]+|[-/.]+$/g, "");
66992
+ } while (name5 !== previous);
66993
+ return name5;
66893
66994
  }
66894
66995
  var init_branch_name = __esm({
66895
66996
  "../../packages/shared/src/branch-name.ts"() {
66896
66997
  }
66897
66998
  });
66898
66999
 
67000
+ // ../../packages/shared/src/worktree-display.ts
67001
+ var WORKTREE_TITLE_MAX;
67002
+ var init_worktree_display = __esm({
67003
+ "../../packages/shared/src/worktree-display.ts"() {
67004
+ init_worktree_id();
67005
+ WORKTREE_TITLE_MAX = 100;
67006
+ }
67007
+ });
67008
+
66899
67009
  // src/server/infra/db/queries/pending-removals.ts
66900
67010
  var PendingRemovalQueries;
66901
67011
  var init_pending_removals = __esm({
@@ -68286,6 +68396,11 @@ var init_mime_types = __esm({
68286
68396
  ".ico": "image/x-icon",
68287
68397
  ".bmp": "image/bmp",
68288
68398
  ".avif": "image/avif",
68399
+ ".mp4": "video/mp4",
68400
+ ".m4v": "video/mp4",
68401
+ ".mov": "video/quicktime",
68402
+ ".webm": "video/webm",
68403
+ ".ogv": "video/ogg",
68289
68404
  ".pdf": "application/pdf",
68290
68405
  ".json": "application/json",
68291
68406
  ".txt": "text/plain",
@@ -68723,7 +68838,7 @@ var init_channel = __esm({
68723
68838
  });
68724
68839
 
68725
68840
  // ../../packages/link/src/chats.ts
68726
- var CAPABILITY_CHAT_OWNERSHIP, CHAT_ID_BLOCK_SIZE, METHOD_CHAT_OWN, METHOD_CHAT_FORWARD, METHOD_CHAT_RELEASE, METHOD_CHAT_SET_CONFIG, METHOD_CHAT_LIST_SESSIONS, METHOD_CHAT_REPLICATE, METHOD_CHAT_ID_BLOCK, METHOD_CHAT_HISTORY, METHOD_CHAT_LAUNCH, METHOD_CHAT_SESSION_STATE, NOTIFY_CHAT_LIVE, NOTIFY_CHAT_TRANSIENT, NOTIFY_CHAT_ACTIVITY;
68841
+ var CAPABILITY_CHAT_OWNERSHIP, CHAT_ID_BLOCK_SIZE, METHOD_CHAT_OWN, METHOD_CHAT_FORWARD, METHOD_CHAT_RELEASE, METHOD_CHAT_SET_CONFIG, METHOD_CHAT_LIST_SESSIONS, METHOD_CHAT_VIEW_SESSION, METHOD_CHAT_REPLICATE, METHOD_CHAT_ID_BLOCK, METHOD_CHAT_HISTORY, METHOD_CHAT_LAUNCH, METHOD_CHAT_SESSION_STATE, NOTIFY_CHAT_LIVE, NOTIFY_CHAT_TRANSIENT, NOTIFY_CHAT_ACTIVITY;
68727
68842
  var init_chats2 = __esm({
68728
68843
  "../../packages/link/src/chats.ts"() {
68729
68844
  "use strict";
@@ -68734,6 +68849,7 @@ var init_chats2 = __esm({
68734
68849
  METHOD_CHAT_RELEASE = "chat.release";
68735
68850
  METHOD_CHAT_SET_CONFIG = "chat.setConfigOption";
68736
68851
  METHOD_CHAT_LIST_SESSIONS = "chat.listSessions";
68852
+ METHOD_CHAT_VIEW_SESSION = "chat.viewSession";
68737
68853
  METHOD_CHAT_REPLICATE = "chat.replicate";
68738
68854
  METHOD_CHAT_ID_BLOCK = "chat.idBlock";
68739
68855
  METHOD_CHAT_HISTORY = "chat.history";
@@ -69676,7 +69792,7 @@ function forwardedTask(id, options) {
69676
69792
  codingAgentId: options.codingAgentId
69677
69793
  };
69678
69794
  }
69679
- var log23, FORWARD_TIMEOUT_MS, RETRY_MS, HISTORY_MAX_ROWS, ChatOwnershipService, chatOwnershipService;
69795
+ var log23, FORWARD_TIMEOUT_MS, VIEW_SESSION_TIMEOUT_MS, RETRY_MS, HISTORY_MAX_ROWS, ChatOwnershipService, chatOwnershipService;
69680
69796
  var init_chat_ownership_service = __esm({
69681
69797
  "src/server/services/chat-ownership-service.ts"() {
69682
69798
  "use strict";
@@ -69697,6 +69813,7 @@ var init_chat_ownership_service = __esm({
69697
69813
  init_worktree_service();
69698
69814
  log23 = createLogger("chat-ownership");
69699
69815
  FORWARD_TIMEOUT_MS = 6e4;
69816
+ VIEW_SESSION_TIMEOUT_MS = 2e5;
69700
69817
  RETRY_MS = 5e3;
69701
69818
  HISTORY_MAX_ROWS = 1e3;
69702
69819
  ChatOwnershipService = class {
@@ -70127,6 +70244,35 @@ var init_chat_ownership_service = __esm({
70127
70244
  );
70128
70245
  return readPayload(session, enc);
70129
70246
  }
70247
+ /**
70248
+ * The user picked another session for the chat: the owner learns of it from the chat's info,
70249
+ * sent again. A chat with a turn running keeps its session, and the worker's own record wins.
70250
+ */
70251
+ syncActiveSession(chatId) {
70252
+ const hostId2 = this.ownerOf(chatId);
70253
+ if (!hostId2 || taskService.getTask(chatId)?.status === "running") return;
70254
+ this.enqueue(chatId, hostId2, METHOD_CHAT_OWN, { chatId });
70255
+ }
70256
+ /**
70257
+ * Has the owner load a session Band has no log for, and waits until the replayed rows are
70258
+ * here. An offline owner, or an old worker without the method, does nothing, so the replay
70259
+ * arrives with the first prompt as before.
70260
+ */
70261
+ async viewSession(chatId) {
70262
+ const sessionId = chatService.get(chatId)?.activeSessionId;
70263
+ const hostId2 = this.ownerOf(chatId);
70264
+ const session = hostId2 ? this.sessions.get(hostId2) : void 0;
70265
+ if (!session?.attached) return;
70266
+ try {
70267
+ await session.request(
70268
+ METHOD_CHAT_VIEW_SESSION,
70269
+ { chatId, sessionId },
70270
+ { timeoutMs: VIEW_SESSION_TIMEOUT_MS }
70271
+ );
70272
+ } catch (err) {
70273
+ log23.warn({ chatId, err }, "the owner could not load the session history");
70274
+ }
70275
+ }
70130
70276
  /** The agent's own session list on the owner, or null when it can't list or is offline. */
70131
70277
  async listSessions(chatId) {
70132
70278
  const hostId2 = this.ownerOf(chatId);
@@ -70380,7 +70526,7 @@ function toTaskInfo(task) {
70380
70526
  }
70381
70527
  async function promptBlocks(task, firstTurn, sharedDir) {
70382
70528
  const caps = agentSessionService.promptCapabilities(task.chatId);
70383
- const blocks = [{ type: "text", text: task.prompt }];
70529
+ const blocks = task.prompt.trim() ? [{ type: "text", text: task.prompt }] : [];
70384
70530
  for (const file3 of task.attachments) {
70385
70531
  const name5 = file3.filename ?? file3.path.split("/").pop() ?? file3.path;
70386
70532
  if (caps.image && file3.mediaType.startsWith("image/")) {
@@ -70825,10 +70971,15 @@ async function buildSnapshot(host, cwd, wipDir, worktreeId4, label2, seedIndex =
70825
70971
  await host.fs.mkdir(wipDir, { recursive: true });
70826
70972
  const baseSha = await git2(["rev-parse", "HEAD"]);
70827
70973
  const branch = await git2(["rev-parse", "--abbrev-ref", "HEAD"]);
70828
- const indexFile = posix3.join(wipDir, `${worktreeId4}.index`);
70974
+ const slotKey = `${wipDir}\0${worktreeId4}`;
70975
+ const slot = takeIndexSlot(slotKey);
70976
+ const indexFile = posix3.join(
70977
+ wipDir,
70978
+ slot === 0 ? `${worktreeId4}.index` : `${worktreeId4}.${slot}.index`
70979
+ );
70829
70980
  const env = { ...SNAPSHOT_BOT, GIT_INDEX_FILE: indexFile };
70830
- await host.fs.rm(indexFile, { force: true });
70831
70981
  try {
70982
+ await host.fs.rm(indexFile, { force: true });
70832
70983
  let treeSha;
70833
70984
  if (seedIndex) {
70834
70985
  try {
@@ -70850,8 +71001,25 @@ async function buildSnapshot(host, cwd, wipDir, worktreeId4, label2, seedIndex =
70850
71001
  return { baseSha, branch: branch === "HEAD" ? "" : branch, snapshotSha, treeSha };
70851
71002
  } finally {
70852
71003
  await host.fs.rm(indexFile, { force: true }).catch(() => void 0);
71004
+ releaseIndexSlot(slotKey, slot);
70853
71005
  }
70854
71006
  }
71007
+ function takeIndexSlot(key2) {
71008
+ let used = indexSlots.get(key2);
71009
+ if (!used) {
71010
+ used = /* @__PURE__ */ new Set();
71011
+ indexSlots.set(key2, used);
71012
+ }
71013
+ let slot = 0;
71014
+ while (used.has(slot)) slot++;
71015
+ used.add(slot);
71016
+ return slot;
71017
+ }
71018
+ function releaseIndexSlot(key2, slot) {
71019
+ const used = indexSlots.get(key2);
71020
+ used?.delete(slot);
71021
+ if (used?.size === 0) indexSlots.delete(key2);
71022
+ }
70855
71023
  async function checkoutSnapshot(host, repoPath, worktreePath, snapshot, options = {}) {
70856
71024
  const git2 = gitIn(host, repoPath);
70857
71025
  await host.fs.mkdir(posix3.dirname(worktreePath), { recursive: true });
@@ -70897,7 +71065,7 @@ async function resetCheckoutToSnapshot(host, path2, snapshot) {
70897
71065
  await git2(["reset", "-q"]);
70898
71066
  }
70899
71067
  }
70900
- var WIP_DIR, SNAPSHOT_BOT;
71068
+ var WIP_DIR, SNAPSHOT_BOT, indexSlots;
70901
71069
  var init_snapshot_git = __esm({
70902
71070
  "../../packages/host-api/src/snapshot-git.ts"() {
70903
71071
  "use strict";
@@ -70908,6 +71076,7 @@ var init_snapshot_git = __esm({
70908
71076
  GIT_COMMITTER_NAME: "Band",
70909
71077
  GIT_COMMITTER_EMAIL: "band@localhost"
70910
71078
  };
71079
+ indexSlots = /* @__PURE__ */ new Map();
70911
71080
  }
70912
71081
  });
70913
71082
 
@@ -71273,7 +71442,7 @@ var init_remote_host = __esm({
71273
71442
  stat: (path2, options) => this.rpc.call("fs.stat", { path: path2, followSymlinks: options?.followSymlinks }),
71274
71443
  realpath: (path2) => this.rpc.call("fs.realpath", { path: path2 }),
71275
71444
  readFile: (path2) => this.rpc.callBytes("fs.readFile", { path: path2 }),
71276
- readStream: (path2) => this.streamOf("fs.readStream", { path: path2 }),
71445
+ readStream: (path2, range) => this.streamOf("fs.readStream", { path: path2, start: range?.start, end: range?.end }),
71277
71446
  writeFile: (path2, data, options) => this.writeFile(path2, data, options),
71278
71447
  glob: (pattern, cwd) => this.rpc.call("fs.glob", { pattern, cwd }),
71279
71448
  mkdtemp: (prefix) => this.rpc.call("fs.mkdtemp", { prefix }),
@@ -77425,6 +77594,8 @@ __export(worktree_service_exports, {
77425
77594
  worktreeSetGroupInput: () => worktreeSetGroupInput,
77426
77595
  worktreeSetOriginInput: () => worktreeSetOriginInput,
77427
77596
  worktreeSetPinnedInput: () => worktreeSetPinnedInput,
77597
+ worktreeSetTitleInput: () => worktreeSetTitleInput,
77598
+ worktreeSetUnreadInput: () => worktreeSetUnreadInput,
77428
77599
  worktreeVia: () => worktreeVia
77429
77600
  });
77430
77601
  import { randomUUID as randomUUID11 } from "node:crypto";
@@ -77470,7 +77641,7 @@ function expandHome(given, home, hostId2) {
77470
77641
  }
77471
77642
  return path2;
77472
77643
  }
77473
- var TEARDOWN_TIMEOUT_MS, log39, worktreeVia, worktreeCreateInput, worktreeRemoveInput, worktreeSetPinnedInput, worktreeSetOriginInput, worktreeRefInput, worktreeSetGroupInput, worktreeReorderInput, worktreeGitInput, worktreeRunScriptInput, RepoNotFoundError, InvalidOriginError, PlainRepoError, WorktreeService, worktreeService;
77644
+ var TEARDOWN_TIMEOUT_MS, log39, worktreeVia, worktreeCreateInput, worktreeRemoveInput, worktreeSetPinnedInput, worktreeSetOriginInput, worktreeRefInput, worktreeSetGroupInput, worktreeSetUnreadInput, worktreeSetTitleInput, worktreeReorderInput, worktreeGitInput, worktreeRunScriptInput, RepoNotFoundError, InvalidOriginError, PlainRepoError, WorktreeService, worktreeService;
77474
77645
  var init_worktree_service = __esm({
77475
77646
  "src/server/services/worktree-service.ts"() {
77476
77647
  "use strict";
@@ -77481,6 +77652,7 @@ var init_worktree_service = __esm({
77481
77652
  init_path();
77482
77653
  init_src();
77483
77654
  init_branch_name();
77655
+ init_worktree_display();
77484
77656
  init_worktree_id();
77485
77657
  init_zod();
77486
77658
  init_errors6();
@@ -77598,6 +77770,17 @@ var init_worktree_service = __esm({
77598
77770
  // Also moves every worktree started from this one, on any repo and host, and theirs in turn.
77599
77771
  withChildren: external_exports2.boolean().optional()
77600
77772
  });
77773
+ worktreeSetUnreadInput = external_exports2.object({
77774
+ worktreeId: external_exports2.string().min(1),
77775
+ unread: external_exports2.boolean(),
77776
+ // Tells two same-named worktrees on different workers apart.
77777
+ hostId: external_exports2.string().optional()
77778
+ });
77779
+ worktreeSetTitleInput = external_exports2.object({
77780
+ worktreeId: external_exports2.string().min(1),
77781
+ // Blank or null clears the title, so the card shows the name again.
77782
+ title: external_exports2.string().max(WORKTREE_TITLE_MAX).nullable()
77783
+ });
77601
77784
  worktreeReorderInput = external_exports2.object({
77602
77785
  // The worktrees of one list in their new order.
77603
77786
  order: external_exports2.array(worktreeRefInput).min(1).max(500),
@@ -77838,6 +78021,14 @@ var init_worktree_service = __esm({
77838
78021
  (wt) => sameName(wt) && (wt.hostId ?? hostRegistry.local.id) === hostId2
77839
78022
  );
77840
78023
  if (existing) return { ok: true, path: existing.path, hostId: hostId2 };
78024
+ const caseClash = repo.worktrees.find(
78025
+ (wt) => (wt.hostId ?? hostRegistry.local.id) === hostId2 && [wt.name, wt.branch].some((n) => n.toLowerCase() === input2.branch.toLowerCase())
78026
+ );
78027
+ if (caseClash) {
78028
+ throw new Error(
78029
+ `Branch "${input2.branch}" differs only in case from the existing worktree "${caseClash.branch}". Use that name or pick another.`
78030
+ );
78031
+ }
77841
78032
  assertHostAllowed(repo.name, hostId2);
77842
78033
  const elsewhere = listedWorktrees(repo).find(
77843
78034
  (wt) => sameName(wt) && !wt.supersededBy && (wt.hostId ?? hostRegistry.local.id) !== hostId2
@@ -78361,6 +78552,20 @@ var init_worktree_service = __esm({
78361
78552
  emit({ kind: "worktree-origin-changed", worktreeId: worktreeId4 });
78362
78553
  return { ok: true };
78363
78554
  }
78555
+ /** Sets the title shown for a worktree, or clears it (blank or null). */
78556
+ setTitle(input2) {
78557
+ const title = input2.title?.trim() || null;
78558
+ const state2 = loadState();
78559
+ for (const repo of state2.repos) {
78560
+ for (const wt of repo.worktrees) {
78561
+ if (toWorktreeId(repo.name, wt.name, wt.hostId) !== input2.worktreeId) continue;
78562
+ this.repoQueries.setTitle(repo.name, wt.name, title, wt.hostId ?? "local");
78563
+ emit({ kind: "worktree-origin-changed", worktreeId: input2.worktreeId });
78564
+ return { ok: true, title };
78565
+ }
78566
+ }
78567
+ throw new WorktreeNotFoundError(input2.worktreeId);
78568
+ }
78364
78569
  /**
78365
78570
  * Puts a worktree in a group, or in none with `null`. With `withChildren` every worktree
78366
78571
  * started from it, and theirs in turn, moves too.
@@ -78395,6 +78600,42 @@ var init_worktree_service = __esm({
78395
78600
  emit({ kind: "worktree-origin-changed", worktreeId: input2.worktreeId });
78396
78601
  return { ok: true, moved };
78397
78602
  }
78603
+ /**
78604
+ * Marks a worktree unread or read. Reading also clears a `done` or `failed` attention dot, the
78605
+ * way `statuses.clearNeedsAttention` does.
78606
+ */
78607
+ setUnread(input2) {
78608
+ const row = this.findRowById(input2.worktreeId, input2.hostId);
78609
+ if (!row) throw new WorktreeNotFoundError(input2.worktreeId);
78610
+ this.repoQueries.setUnread(row.repoName, row.name, input2.unread, row.hostId);
78611
+ if (!input2.unread) {
78612
+ const status = acknowledgeWorktreeAttention(input2.worktreeId);
78613
+ if (status) emit({ kind: "update", status });
78614
+ }
78615
+ emit({ kind: "worktree-origin-changed", worktreeId: input2.worktreeId });
78616
+ return { ok: true };
78617
+ }
78618
+ /**
78619
+ * Clears the unread flag when the user opens the worktree. Does nothing, and emits nothing,
78620
+ * for a worktree that is not marked unread or is gone.
78621
+ */
78622
+ clearUnreadOnOpen(worktreeId4) {
78623
+ const row = this.findRowById(worktreeId4);
78624
+ if (!row?.unread) return;
78625
+ this.repoQueries.setUnread(row.repoName, row.name, false, row.hostId);
78626
+ emit({ kind: "worktree-origin-changed", worktreeId: worktreeId4 });
78627
+ }
78628
+ findRowById(worktreeId4, hostId2) {
78629
+ for (const repo of loadState().repos) {
78630
+ for (const wt of repo.worktrees) {
78631
+ if (toWorktreeId(repo.name, wt.name, wt.hostId) !== worktreeId4) continue;
78632
+ const rowHost = wt.hostId ?? "local";
78633
+ if (hostId2 !== void 0 && hostId2 !== rowHost) continue;
78634
+ return { repoName: repo.name, name: wt.name, hostId: rowHost, unread: wt.unread === true };
78635
+ }
78636
+ }
78637
+ return void 0;
78638
+ }
78398
78639
  /**
78399
78640
  * Stores the order of one list of worktrees, and optionally the new parent of one of them. The
78400
78641
  * parent change goes first, so a refused parent leaves the order untouched.
@@ -83414,7 +83655,10 @@ var init_agent_session_service = __esm({
83414
83655
  async ensureSession(chatId, purpose, options = {}) {
83415
83656
  const chat = chatService.get(chatId);
83416
83657
  if (!chat) throw new ChatNotFoundError(chatId);
83417
- if (chat.ownerHostId) return null;
83658
+ if (chat.ownerHostId) {
83659
+ if (purpose === "view") await chatOwnershipService.viewSession(chatId);
83660
+ return null;
83661
+ }
83418
83662
  if (purpose === "prompt") await ephemeralLifecycleService.ensureAwake(chat.worktreeId);
83419
83663
  const worktree = worktreeService.resolve(chat.worktreeId);
83420
83664
  if (!worktree) throw new Error(`Worktree not found: ${chat.worktreeId}`);
@@ -94669,6 +94913,8 @@ var RepoService = class {
94669
94913
  head: wt.head,
94670
94914
  pinned: tracked2?.pinned ?? false,
94671
94915
  ...tracked2?.groupId ? { groupId: tracked2.groupId } : {},
94916
+ ...tracked2?.unread ? { unread: true } : {},
94917
+ ...tracked2?.title ? { title: tracked2.title } : {},
94672
94918
  ...tracked2?.sortIndex === void 0 ? {} : { sortIndex: tracked2.sortIndex },
94673
94919
  ...tracked2?.pinSortIndex === void 0 ? {} : { pinSortIndex: tracked2.pinSortIndex }
94674
94920
  };
@@ -94710,6 +94956,8 @@ var RepoService = class {
94710
94956
  lastCheckpointAt: lastCheckpoints.get(worktreeId4) ?? null,
94711
94957
  agent: status?.agent ?? null,
94712
94958
  groupId: wt.groupId ?? null,
94959
+ unread: wt.unread ?? false,
94960
+ title: wt.title ?? null,
94713
94961
  origin: links.origins.get(worktreeId4) ?? null,
94714
94962
  children: links.children.get(worktreeId4) ?? []
94715
94963
  };
@@ -95464,6 +95712,11 @@ var CheckpointService = class {
95464
95712
  log55.warn({ worktreeId: worktreeId4, trigger, err: errorText3(err) }, "could not take a checkpoint");
95465
95713
  }
95466
95714
  }
95715
+ /** Refuses when a restore or move holds any of the worktrees. */
95716
+ assertNotBusy(...worktreeIds) {
95717
+ const held = worktreeIds.find((id) => this.busy.has(id));
95718
+ if (held) throw new CheckpointError("conflict", `${held} is being restored or moved.`);
95719
+ }
95467
95720
  /** Why no automatic checkpoint is taken for the worktree, or null when one may be. */
95468
95721
  ineligible(worktreeId4) {
95469
95722
  const found = worktreeService.resolve(worktreeId4);
@@ -95533,6 +95786,7 @@ var CheckpointService = class {
95533
95786
  "Checkpoints are for worktrees on a worker, not on the hub's own machine."
95534
95787
  );
95535
95788
  }
95789
+ this.assertNotBusy(worktreeId4);
95536
95790
  if (!opts.ignoreSetting && !checkpointsEnabledFor(found.repo.checkpointsEnabled, found.repo.allowedHostIds)) {
95537
95791
  throw new CheckpointError("disabled", `Checkpoints are off for ${found.repo.name}.`);
95538
95792
  }
@@ -95965,9 +96219,8 @@ ${diff}`).digest("hex");
95965
96219
  `${worktreeId4} was already restored as ${worktree.supersededBy}.`
95966
96220
  );
95967
96221
  }
95968
- if (this.busy.has(worktreeId4)) {
95969
- throw new CheckpointError("conflict", `${worktreeId4} is being restored or moved.`);
95970
- }
96222
+ const targetId = toWorktreeId(repo.name, worktree.name, toHostId);
96223
+ this.assertNotBusy(worktreeId4, targetId);
95971
96224
  const row = this.pick(worktreeId4, input2.checkpointId);
95972
96225
  const sourceUp = this.sessions.get(fromHostId)?.attached === true;
95973
96226
  const leaseLeft = sourceUp ? 0 : workerLeaseService.remainingMs(fromHostId);
@@ -95998,7 +96251,6 @@ ${diff}`).digest("hex");
95998
96251
  (w2) => w2.name === worktree.name && (w2.hostId ?? LOCAL_HOST_ID8) === toHostId
95999
96252
  );
96000
96253
  if (onTarget && !onTarget.supersededBy && isRoot) {
96001
- const targetId = toWorktreeId(repo.name, worktree.name, toHostId);
96002
96254
  const busy = chatService.list(targetId).some(
96003
96255
  (chat) => hasRunningTask(chat.id) || agentSessionService.isActive(chat.id) || hasQueuedMessages(chat.id)
96004
96256
  ) || (await terminalService.list(targetId).catch(() => [])).length > 0;
@@ -96015,6 +96267,7 @@ ${diff}`).digest("hex");
96015
96267
  `Worker ${toHostId} already has a worktree ${worktree.name} of ${repo.name}. Remove it first.`
96016
96268
  );
96017
96269
  }
96270
+ this.assertNotBusy(worktreeId4, targetId);
96018
96271
  const previousAllowed = repo.allowedHostIds ? [...repo.allowedHostIds] : null;
96019
96272
  let widened = false;
96020
96273
  let restored = false;
@@ -96025,7 +96278,9 @@ ${diff}`).digest("hex");
96025
96278
  widened = true;
96026
96279
  }
96027
96280
  this.busy.add(worktreeId4);
96281
+ this.busy.add(targetId);
96028
96282
  try {
96283
+ await this.inflight.get(targetId)?.catch(() => void 0);
96029
96284
  const host = hostRegistry.hostById(toHostId);
96030
96285
  const rpc = new RemoteRpc(toHostId, () => target);
96031
96286
  const [root] = (await host.info()).roots;
@@ -96042,7 +96297,7 @@ ${diff}`).digest("hex");
96042
96297
  input2.hostRepoPath
96043
96298
  );
96044
96299
  const git2 = gitIn(host, repoPath);
96045
- const newId = toWorktreeId(repo.name, worktree.name, toHostId);
96300
+ const newId = targetId;
96046
96301
  const worktreePath = isRoot ? repoPath : posix8.join(root, ".band-worktrees", repo.name, worktree.name);
96047
96302
  const backups = [];
96048
96303
  const backupStamp = Date.now();
@@ -96178,6 +96433,10 @@ ${diff}`).digest("hex");
96178
96433
  if (worktree.groupId) {
96179
96434
  this.repos.setGroup(repo.name, worktree.name, worktree.groupId, toHostId);
96180
96435
  }
96436
+ if (worktree.unread) this.repos.setUnread(repo.name, worktree.name, true, toHostId);
96437
+ if (worktree.title) {
96438
+ this.repos.setTitle(repo.name, worktree.name, worktree.title, toHostId);
96439
+ }
96181
96440
  this.repos.setSupersededBy(repo.name, worktree.name, fromHostId, newId);
96182
96441
  await Promise.all([
96183
96442
  workerLeaseService.pushBlockedPaths(fromHostId),
@@ -96214,6 +96473,7 @@ ${diff}`).digest("hex");
96214
96473
  }
96215
96474
  }
96216
96475
  this.busy.delete(worktreeId4);
96476
+ this.busy.delete(targetId);
96217
96477
  }
96218
96478
  }
96219
96479
  async restoreSessions(host, rpc, row, wipDir, fromCwd, toCwd) {
@@ -97375,8 +97635,9 @@ async function handleChatSubmit(req, res, chatId) {
97375
97635
  }
97376
97636
  const { text: text4, sessionId, mode, model, codingAgentId, files } = body;
97377
97637
  let { worktreeId: worktreeId4 } = body;
97378
- if (!worktreeId4 || !text4?.trim()) {
97379
- sendJson2(res, 400, { error: "worktreeId and text are required" });
97638
+ const hasFiles = Array.isArray(files) && files.length > 0;
97639
+ if (!worktreeId4 || !text4?.trim() && !hasFiles) {
97640
+ sendJson2(res, 400, { error: "worktreeId and text or files are required" });
97380
97641
  return;
97381
97642
  }
97382
97643
  if (sessionId !== void 0 && !SESSION_ID_PATTERN2.test(sessionId)) {
@@ -97420,7 +97681,7 @@ async function handleChatSubmit(req, res, chatId) {
97420
97681
  const result = taskService.submitOrQueueTask({
97421
97682
  worktreeId: worktreeId4,
97422
97683
  chatId,
97423
- prompt: text4,
97684
+ prompt: text4 ?? "",
97424
97685
  sessionId,
97425
97686
  attachments,
97426
97687
  mode,
@@ -109943,6 +110204,7 @@ init_zod();
109943
110204
  init_errors6();
109944
110205
  init_agent_login_service();
109945
110206
  init_agent_session_service();
110207
+ init_chat_ownership_service();
109946
110208
  init_chat_service();
109947
110209
  init_settings_service();
109948
110210
  init_task_service();
@@ -110114,6 +110376,7 @@ var chatsRouter = t.router({
110114
110376
  summary,
110115
110377
  lastModified
110116
110378
  });
110379
+ chatOwnershipService.syncActiveSession(input2.chatId);
110117
110380
  return { ok: true };
110118
110381
  }),
110119
110382
  send: publicProcedure.input(
@@ -110275,6 +110538,7 @@ init_zod();
110275
110538
 
110276
110539
  // src/server/services/cli-service.ts
110277
110540
  init_cli_binary();
110541
+ init_local_host_enabled();
110278
110542
  import { execFile as execFile15 } from "node:child_process";
110279
110543
  import {
110280
110544
  accessSync as accessSync3,
@@ -110295,10 +110559,14 @@ function windowsBinDir() {
110295
110559
  return join49(localAppData, "band", "bin");
110296
110560
  }
110297
110561
  var SYMLINK_PATH = platform2() === "win32" ? join49(windowsBinDir(), "band.cmd") : "/usr/local/bin/band";
110562
+ function symlinkPath() {
110563
+ if (platform2() === "win32") return SYMLINK_PATH;
110564
+ return process.env.BAND_CLI_SYMLINK_PATH?.trim() || SYMLINK_PATH;
110565
+ }
110298
110566
  function checkCliWindows() {
110299
110567
  let contents;
110300
110568
  try {
110301
- contents = readFileSync11(SYMLINK_PATH, "utf-8");
110569
+ contents = readFileSync11(symlinkPath(), "utf-8");
110302
110570
  } catch (err) {
110303
110571
  const code = err.code;
110304
110572
  if (code === "EACCES") return "NotWritable";
@@ -110310,18 +110578,22 @@ function checkCliWindows() {
110310
110578
  if (basename3(target).toLowerCase() !== "band.exe") return "ConflictingBinary";
110311
110579
  return existsSync16(target) ? "Installed" : "NotInstalled";
110312
110580
  }
110581
+ function isCliCheckApplicable(admin) {
110582
+ return isLocalHostEnabled() && admin;
110583
+ }
110313
110584
  async function checkCli() {
110314
110585
  if (platform2() === "win32") {
110315
110586
  return checkCliWindows();
110316
110587
  }
110588
+ const linkPath = symlinkPath();
110317
110589
  try {
110318
- const stat7 = lstatSync4(SYMLINK_PATH);
110590
+ const stat7 = lstatSync4(linkPath);
110319
110591
  if (!stat7.isSymbolicLink()) {
110320
110592
  return "ConflictingBinary";
110321
110593
  }
110322
110594
  let target;
110323
110595
  try {
110324
- target = realpathSync5(SYMLINK_PATH);
110596
+ target = realpathSync5(linkPath);
110325
110597
  } catch {
110326
110598
  return "NotInstalled";
110327
110599
  }
@@ -110330,6 +110602,13 @@ async function checkCli() {
110330
110602
  if (isCargoBuild || isElectronSidecar) {
110331
110603
  return "Installed";
110332
110604
  }
110605
+ const resolved = findCliBinary();
110606
+ if (resolved) {
110607
+ try {
110608
+ if (realpathSync5(resolved) === target) return "Installed";
110609
+ } catch {
110610
+ }
110611
+ }
110333
110612
  const isStaleAppLink = target.toLowerCase().includes("/band.app/contents/macos/");
110334
110613
  if (isStaleAppLink) {
110335
110614
  return "NotInstalled";
@@ -110339,7 +110618,7 @@ async function checkCli() {
110339
110618
  const code = err.code;
110340
110619
  if (code === "ENOENT") {
110341
110620
  try {
110342
- lstatSync4("/usr/local/bin");
110621
+ lstatSync4(dirname13(linkPath));
110343
110622
  return "NotInstalled";
110344
110623
  } catch {
110345
110624
  return "DirNotFound";
@@ -110364,7 +110643,7 @@ function noBinaryError(env = process.env) {
110364
110643
  return new Error("Bundled CLI binary missing - try reinstalling Band");
110365
110644
  }
110366
110645
  return new Error(
110367
- "Could not find band CLI binary. Build it first with: cargo build --release -p band-cli"
110646
+ `Could not find band CLI binary. Install ${cliPackageName()} or set BAND_CLI_PATH. In a source checkout, build it with: cargo build --release -p band-cli`
110368
110647
  );
110369
110648
  }
110370
110649
  function runReg(args) {
@@ -110416,39 +110695,43 @@ async function installCli(_opts = {}) {
110416
110695
  await installCliWindows(binaryPath);
110417
110696
  return;
110418
110697
  }
110419
- const dir = dirname13(SYMLINK_PATH);
110698
+ const linkPath = symlinkPath();
110699
+ const dir = dirname13(linkPath);
110420
110700
  if (isDirWritable(dir)) {
110421
110701
  try {
110422
- lstatSync4(SYMLINK_PATH);
110423
- unlinkSync6(SYMLINK_PATH);
110702
+ lstatSync4(linkPath);
110703
+ unlinkSync6(linkPath);
110424
110704
  } catch (err) {
110425
110705
  const code = err.code;
110426
110706
  if (code !== "ENOENT") throw err;
110427
110707
  }
110428
- symlinkSync(binaryPath, SYMLINK_PATH);
110708
+ symlinkSync(binaryPath, linkPath);
110429
110709
  return;
110430
110710
  }
110431
110711
  if (platform2() === "darwin") {
110432
110712
  throw new Error("elevation-required");
110433
110713
  }
110434
- throw new Error(`Run: sudo ln -sf "${binaryPath}" "${SYMLINK_PATH}"`);
110714
+ throw new Error(`On the hub's server, run: sudo ln -sf "${binaryPath}" "${linkPath}"`);
110435
110715
  }
110436
110716
  function resolveCliPaths() {
110437
110717
  const binaryPath = findCliBinary();
110438
110718
  if (!binaryPath) return null;
110439
- return { binaryPath, symlinkPath: SYMLINK_PATH };
110719
+ return { binaryPath, symlinkPath: symlinkPath() };
110440
110720
  }
110441
110721
 
110442
110722
  // src/server/api/cli/router.ts
110443
110723
  var cliRouter = t.router({
110444
- check: publicProcedure.query(async () => {
110445
- const status = await checkCli();
110446
- return { status };
110724
+ // `applicable` is false when nobody can act on the answer: the hub runs no worktrees of its
110725
+ // own (workers carry their own CLI), or the caller is not an admin.
110726
+ check: publicProcedure.query(async ({ ctx }) => {
110727
+ if (!isCliCheckApplicable(ctx.admin))
110728
+ return { status: "Installed", applicable: false };
110729
+ return { status: await checkCli(), applicable: true };
110447
110730
  }),
110448
- resolve: publicProcedure.query(() => {
110731
+ resolve: adminProcedure.query(() => {
110449
110732
  return resolveCliPaths();
110450
110733
  }),
110451
- install: publicProcedure.input(external_exports2.object({ allowPrompt: external_exports2.boolean().optional() }).optional()).mutation(async ({ input: input2 }) => {
110734
+ install: adminProcedure.input(external_exports2.object({ allowPrompt: external_exports2.boolean().optional() }).optional()).mutation(async ({ input: input2 }) => {
110452
110735
  try {
110453
110736
  await installCli({ allowPrompt: input2?.allowPrompt });
110454
110737
  return { ok: true };
@@ -111754,7 +112037,7 @@ init_registry();
111754
112037
  // package.json
111755
112038
  var package_default = {
111756
112039
  name: "@band-app/server",
111757
- version: "0.54.2",
112040
+ version: "0.54.4",
111758
112041
  type: "module",
111759
112042
  types: "src/index.ts",
111760
112043
  exports: {
@@ -112089,6 +112372,7 @@ var RELAY_PROCEDURES = /* @__PURE__ */ new Set([
112089
112372
  "repos.remove",
112090
112373
  "groups.list",
112091
112374
  "worktrees.setGroup",
112375
+ "worktrees.setTitle",
112092
112376
  "hosts.list",
112093
112377
  "hosts.forAgents",
112094
112378
  "workers.list",
@@ -118475,6 +118759,7 @@ init_zod();
118475
118759
  init_branch_status_poller();
118476
118760
  init_state();
118477
118761
  init_watcher_service();
118762
+ init_worktree_service();
118478
118763
  var statusesRouter = t.router({
118479
118764
  get: publicProcedure.input(external_exports2.object({ worktreeId: external_exports2.string() })).query(({ input: input2 }) => {
118480
118765
  return getWorktreeStatus(input2.worktreeId);
@@ -118501,7 +118786,15 @@ var statusesRouter = t.router({
118501
118786
  refreshBranchStatus: publicProcedure.input(external_exports2.object({ worktreeId: external_exports2.string() })).mutation(async ({ input: input2 }) => ({
118502
118787
  refreshed: await branchStatusPoller.refreshWorktree(input2.worktreeId)
118503
118788
  })),
118504
- clearNeedsAttention: publicProcedure.input(external_exports2.object({ worktreeId: external_exports2.string() })).mutation(({ input: input2 }) => {
118789
+ clearNeedsAttention: publicProcedure.input(
118790
+ external_exports2.object({
118791
+ worktreeId: external_exports2.string(),
118792
+ // Opening a worktree also drops a manual unread mark. The UI leaves it out for a
118793
+ // worktree it just marked while that worktree was open.
118794
+ clearUnread: external_exports2.boolean().optional()
118795
+ })
118796
+ ).mutation(({ input: input2 }) => {
118797
+ if (input2.clearUnread) worktreeService.clearUnreadOnOpen(input2.worktreeId);
118505
118798
  const status = acknowledgeWorktreeAttention(input2.worktreeId);
118506
118799
  if (status) emit({ kind: "update", status });
118507
118800
  return { ok: true };
@@ -122250,6 +122543,23 @@ var worktreesRouter = t.router({
122250
122543
  throwAsTrpcError3(err);
122251
122544
  }
122252
122545
  }),
122546
+ setUnread: publicProcedure.input(worktreeSetUnreadInput).mutation(({ input: input2 }) => {
122547
+ try {
122548
+ return worktreeService.setUnread(input2);
122549
+ } catch (err) {
122550
+ throwAsTrpcError3(err);
122551
+ }
122552
+ }),
122553
+ setTitle: publicProcedure.input(worktreeSetTitleInput).mutation(({ input: input2 }) => {
122554
+ try {
122555
+ return worktreeService.setTitle(input2);
122556
+ } catch (err) {
122557
+ if (err instanceof WorktreeNotFoundError) {
122558
+ throw new TRPCError({ code: "NOT_FOUND", message: err.message, cause: err });
122559
+ }
122560
+ throwAsTrpcError3(err);
122561
+ }
122562
+ }),
122253
122563
  gitPull: publicProcedure.input(worktreeGitInput).mutation(async ({ input: input2 }) => {
122254
122564
  try {
122255
122565
  return await worktreeService.gitPull(input2);
@@ -122364,6 +122674,7 @@ function discoverProcedures() {
122364
122674
  if (path2 === "repos.addByUrl") continue;
122365
122675
  if (path2 === "repos.inspectFolder" || path2 === "repos.resolveRemote") continue;
122366
122676
  if (path2 === "worktrees.setOrigin") continue;
122677
+ if (path2 === "worktrees.setUnread") continue;
122367
122678
  if (path2.startsWith("groups.") && path2 !== "groups.list") continue;
122368
122679
  if (path2 === "worktrees.reorder") continue;
122369
122680
  if (path2 === "environment.build") continue;
@@ -124642,6 +124953,37 @@ async function handleLspConnection(ws, req) {
124642
124953
  // start-server.ts
124643
124954
  init_webhook();
124644
124955
 
124956
+ // src/server/services/_utils/byte-range.ts
124957
+ function parseByteRange(header, size) {
124958
+ const match = /^bytes=(\d*)-(\d*)$/.exec(header?.trim() ?? "");
124959
+ if (!match || match[1] === "" && match[2] === "") return null;
124960
+ if (match[1] !== "" && match[2] !== "" && Number(match[2]) < Number(match[1])) return null;
124961
+ let start;
124962
+ let end;
124963
+ if (match[1] === "") {
124964
+ const length = Number(match[2]);
124965
+ if (length === 0) return "unsatisfiable";
124966
+ start = Math.max(0, size - length);
124967
+ end = size - 1;
124968
+ } else {
124969
+ start = Number(match[1]);
124970
+ end = match[2] === "" ? size - 1 : Math.min(Number(match[2]), size - 1);
124971
+ }
124972
+ if (start >= size || start > end) return "unsatisfiable";
124973
+ return { start, end };
124974
+ }
124975
+
124976
+ // src/server/services/_utils/content-disposition.ts
124977
+ function attachmentDisposition(filename) {
124978
+ const clean = filename.replace(/[/\\]/g, "_") || "download";
124979
+ const ascii = clean.replace(/[^\x20-\x7e]/g, "_").replace(/["%;]/g, "_");
124980
+ const encoded = encodeURIComponent(clean).replace(
124981
+ /['()*]/g,
124982
+ (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
124983
+ );
124984
+ return `attachment; filename="${ascii}"; filename*=UTF-8''${encoded}`;
124985
+ }
124986
+
124645
124987
  // src/server/services/_utils/hosted-file.ts
124646
124988
  init_registry();
124647
124989
  import { basename as basename6, dirname as dirname17, join as join57 } from "node:path";
@@ -133203,7 +133545,7 @@ async function serveHostedFile(res, kind, worktreeId4, rawName, chatId) {
133203
133545
  );
133204
133546
  return true;
133205
133547
  }
133206
- async function serveWorktreeFile(res, worktreeId4, rawPath, extraHeaders = {}, visualEditNonce) {
133548
+ async function serveWorktreeFile(res, worktreeId4, rawPath, extraHeaders = {}, visualEditNonce, download = false, rangeHeader) {
133207
133549
  const worktree = worktreeService.resolve(worktreeId4);
133208
133550
  if (!worktree) {
133209
133551
  res.writeHead(404);
@@ -133226,6 +133568,11 @@ async function serveWorktreeFile(res, worktreeId4, rawPath, extraHeaders = {}, v
133226
133568
  return;
133227
133569
  }
133228
133570
  const fileStat = await fs3.stat(target, { followSymlinks: true });
133571
+ if (download && fileStat.kind !== "file") {
133572
+ res.writeHead(400);
133573
+ res.end("Bad request");
133574
+ return;
133575
+ }
133229
133576
  const contentType = mimeTypeFromFilename(basename7(target));
133230
133577
  const isHtml = contentType.startsWith("text/html");
133231
133578
  const editHtml = isHtml && visualEditNonce !== void 0 && fileStat.kind === "file";
@@ -133241,28 +133588,37 @@ async function serveWorktreeFile(res, worktreeId4, rawPath, extraHeaders = {}, v
133241
133588
  body = Buffer.from(annotateHtmlForEdit(source, visualEditNonce), "utf8");
133242
133589
  }
133243
133590
  }
133244
- res.writeHead(200, {
133591
+ const range = !body && !trailer && fileStat.kind === "file" ? parseByteRange(rangeHeader, fileStat.size) : null;
133592
+ if (range === "unsatisfiable") {
133593
+ res.writeHead(416, { "Content-Range": `bytes */${fileStat.size}` });
133594
+ res.end();
133595
+ return;
133596
+ }
133597
+ const partial3 = range !== null;
133598
+ res.writeHead(partial3 ? 206 : 200, {
133245
133599
  "Content-Type": contentType,
133246
- "Content-Length": (body?.byteLength ?? fileStat.size + (trailer?.byteLength ?? 0)).toString(),
133600
+ "Content-Length": (partial3 ? range.end - range.start + 1 : body?.byteLength ?? fileStat.size + (trailer?.byteLength ?? 0)).toString(),
133601
+ ...partial3 ? { "Content-Range": `bytes ${range.start}-${range.end}/${fileStat.size}` } : {},
133602
+ "Accept-Ranges": "bytes",
133247
133603
  "Cache-Control": "private, no-cache",
133248
- // HTML from a repo must not run with the hub's origin.
133249
- ...isHtml ? {
133250
- "Content-Security-Policy": "sandbox allow-scripts",
133251
- "X-Content-Type-Options": "nosniff"
133252
- } : {},
133604
+ // HTML and SVG from a repo must not run with the hub's origin, and no
133605
+ // type may be sniffed into one.
133606
+ ...hostedFileHeaders(contentType),
133607
+ ...download ? { "Content-Disposition": attachmentDisposition(basename7(target)) } : {},
133253
133608
  ...extraHeaders
133254
133609
  });
133255
133610
  if (body) {
133256
133611
  res.end(body);
133257
133612
  return;
133258
133613
  }
133259
- const stream = Readable2.from(fs3.readStream(target), {
133614
+ const stream = Readable2.from(fs3.readStream(target, range ?? void 0), {
133260
133615
  objectMode: false,
133261
133616
  highWaterMark: 64 * 1024
133262
133617
  });
133263
133618
  if (trailer) {
133264
133619
  stream.on("end", () => res.end(trailer));
133265
133620
  stream.on("error", () => res.destroy());
133621
+ res.once("close", () => stream.destroy());
133266
133622
  stream.pipe(res, { end: false });
133267
133623
  } else {
133268
133624
  pipeline2(stream, res, () => {
@@ -133640,6 +133996,7 @@ async function main() {
133640
133996
  "/api/worktree-file/".length,
133641
133997
  queryIdx === -1 ? void 0 : queryIdx
133642
133998
  );
133999
+ const download = queryIdx !== -1 && new URLSearchParams(req.url.slice(queryIdx)).get("download") === "1";
133643
134000
  const slashIdx = rest.indexOf("/");
133644
134001
  if (slashIdx === -1) {
133645
134002
  res.writeHead(400);
@@ -133661,7 +134018,7 @@ async function main() {
133661
134018
  res.end("Bad request");
133662
134019
  return;
133663
134020
  }
133664
- await serveWorktreeFile(res, wId, filePath);
134021
+ await serveWorktreeFile(res, wId, filePath, {}, void 0, download, req.headers.range);
133665
134022
  return;
133666
134023
  }
133667
134024
  if (req.url?.startsWith("/api/cdp/tabs")) {