@band-app/server 0.54.3 → 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-Xl6oGaup.js → TerminalPanel-CxzXrgZc.js} +1 -1
  3. package/dist/client/assets/{_basePickBy-CPkUkuQZ.js → _basePickBy-ESHGoJr1.js} +1 -1
  4. package/dist/client/assets/{_baseUniq-Cwz3JodT.js → _baseUniq-Caza5c4E.js} +1 -1
  5. package/dist/client/assets/{arc-DwsdIXPp.js → arc-DaZonhJY.js} +1 -1
  6. package/dist/client/assets/{architectureDiagram-VXUJARFQ-SXYkL9tA.js → architectureDiagram-VXUJARFQ-Cq7YbAnJ.js} +1 -1
  7. package/dist/client/assets/{blockDiagram-VD42YOAC-BJA-EF6e.js → blockDiagram-VD42YOAC-C8s0ghb3.js} +1 -1
  8. package/dist/client/assets/{c4Diagram-YG6GDRKO-Dx-xhS0w.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-hIHAP7Cm.js → chunk-4BX2VUAB-D3SZ3P8V.js} +1 -1
  11. package/dist/client/assets/{chunk-55IACEB6-B819cB7q.js → chunk-55IACEB6-BfA-9jtI.js} +1 -1
  12. package/dist/client/assets/{chunk-B4BG7PRW-B1YMhrD2.js → chunk-B4BG7PRW-DAygCOyX.js} +1 -1
  13. package/dist/client/assets/{chunk-DI55MBZ5-B6E7V5JJ.js → chunk-DI55MBZ5-CuikvpNq.js} +1 -1
  14. package/dist/client/assets/{chunk-FMBD7UC4-Cto0C9v0.js → chunk-FMBD7UC4-JCmcJDsw.js} +1 -1
  15. package/dist/client/assets/{chunk-QN33PNHL-FlpvaPrY.js → chunk-QN33PNHL-3o6_7ioT.js} +1 -1
  16. package/dist/client/assets/{chunk-QZHKN3VN-hfVUofCn.js → chunk-QZHKN3VN-5B8nia1f.js} +1 -1
  17. package/dist/client/assets/{chunk-TZMSLE5B-B6Uwm_RZ.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-N9jnwEcj.js → cose-bilkent-S5V4N54A-CVQzgyKs.js} +1 -1
  22. package/dist/client/assets/{dagre-6UL2VRFP-Ckg49tw0.js → dagre-6UL2VRFP-C95ZZlB4.js} +1 -1
  23. package/dist/client/assets/{diagram-PSM6KHXK-Dgp_MGHT.js → diagram-PSM6KHXK-Sb4x5piz.js} +1 -1
  24. package/dist/client/assets/{diagram-QEK2KX5R-HqtNbPpi.js → diagram-QEK2KX5R-TTDYZins.js} +1 -1
  25. package/dist/client/assets/{diagram-S2PKOQOG-B9kmoM2C.js → diagram-S2PKOQOG-C8joLdiY.js} +1 -1
  26. package/dist/client/assets/{erDiagram-Q2GNP2WA-CX0bAjbe.js → erDiagram-Q2GNP2WA-HXkzVcZE.js} +1 -1
  27. package/dist/client/assets/{flowDiagram-NV44I4VS-BlNstfD-.js → flowDiagram-NV44I4VS-BPzPMXG7.js} +1 -1
  28. package/dist/client/assets/{ganttDiagram-JELNMOA3-Bngv2UfV.js → ganttDiagram-JELNMOA3-5JGkIYiO.js} +1 -1
  29. package/dist/client/assets/{gitGraphDiagram-V2S2FVAM-DaVgpKKU.js → gitGraphDiagram-V2S2FVAM-CDCkC2dq.js} +1 -1
  30. package/dist/client/assets/{graph-DJibyMhY.js → graph-CQ3VI3Kb.js} +1 -1
  31. package/dist/client/assets/{highlighted-body-B3W2YXNL-8sRLcOTz.js → highlighted-body-B3W2YXNL-jA7b7ztt.js} +1 -1
  32. package/dist/client/assets/{index-ROMcQtWt.js → index-0DuoTbku.js} +1 -1
  33. package/dist/client/assets/{index-hBffkV04.js → index-B6FlIvZu.js} +1 -1
  34. package/dist/client/assets/{index-BsY88S9G.js → index-Bcr3D2nL.js} +1 -1
  35. package/dist/client/assets/{index-D_Hnyp0H.js → index-BedjQBXV.js} +1 -1
  36. package/dist/client/assets/{index-QiwXMA-_.js → index-C7Xj47iO.js} +1 -1
  37. package/dist/client/assets/{index-BdPmfxoL.js → index-CZqDBAUL.js} +1 -1
  38. package/dist/client/assets/{index-SOcZR9zA.js → index-Ce8SticC.js} +1 -1
  39. package/dist/client/assets/{index-Y9ds9lfC.js → index-DWepK3T1.js} +1 -1
  40. package/dist/client/assets/{index-BYWdCHQD.js → index-Dt3jdJaQ.js} +1 -1
  41. package/dist/client/assets/{index-C8jptsrq.js → index-F2GbFYex.js} +1 -1
  42. package/dist/client/assets/{index-CZkHIWnu.js → index-SjDPUTbU.js} +1 -1
  43. package/dist/client/assets/{index-BWbHkoFf.js → index-sTT9joMB.js} +1 -1
  44. package/dist/client/assets/{index-CSlSUYm9.js → index-xEgVFQfI.js} +1 -1
  45. package/dist/client/assets/{infoDiagram-HS3SLOUP-BaXQhmZS.js → infoDiagram-HS3SLOUP-DXlzs7_8.js} +1 -1
  46. package/dist/client/assets/{journeyDiagram-XKPGCS4Q-Bn4XhLKd.js → journeyDiagram-XKPGCS4Q-B7mdzrEf.js} +1 -1
  47. package/dist/client/assets/{kanban-definition-3W4ZIXB7-Dbz9IJS9.js → kanban-definition-3W4ZIXB7-C3yAE4ky.js} +1 -1
  48. package/dist/client/assets/{layout-KhM6N15I.js → layout-BpyKMBbb.js} +1 -1
  49. package/dist/client/assets/{main-CqSslSJI.js → main-B169v3Mr.js} +245 -245
  50. package/dist/client/assets/main-DWmz5QXO.css +1 -0
  51. package/dist/client/assets/{mindmap-definition-VGOIOE7T-G-zFfV9G.js → mindmap-definition-VGOIOE7T-BV1ylQ_o.js} +1 -1
  52. package/dist/client/assets/{pieDiagram-ADFJNKIX-BhPxI2x7.js → pieDiagram-ADFJNKIX-C68Mdt_x.js} +1 -1
  53. package/dist/client/assets/{quadrantDiagram-AYHSOK5B-B-GNhRJU.js → quadrantDiagram-AYHSOK5B-5wmdYa_y.js} +1 -1
  54. package/dist/client/assets/{requirementDiagram-UZGBJVZJ-DQJdQFIw.js → requirementDiagram-UZGBJVZJ-CXCtBgk5.js} +1 -1
  55. package/dist/client/assets/{sankeyDiagram-TZEHDZUN-DrPdLIfN.js → sankeyDiagram-TZEHDZUN-CwuKYu6l.js} +1 -1
  56. package/dist/client/assets/{sequenceDiagram-WL72ISMW-0iaye3zO.js → sequenceDiagram-WL72ISMW-BwFU-IAs.js} +1 -1
  57. package/dist/client/assets/{stateDiagram-FKZM4ZOC-B2-QB2W0.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-M883VntJ.js → timeline-definition-IT6M3QCI-DHJrxfmL.js} +1 -1
  60. package/dist/client/assets/{treemap-GDKQZRPO-NQBhY7Vk.js → treemap-GDKQZRPO-Bn3yodwG.js} +1 -1
  61. package/dist/client/assets/{worktree._worktreeId-D-GKIuuU.js → worktree._worktreeId-7oupLNxr.js} +1 -1
  62. package/dist/client/assets/{worktree._worktreeId-D3DbNkyK.js → worktree._worktreeId-MU4bksud.js} +1 -1
  63. package/dist/client/assets/{xychartDiagram-PRI3JC2R-l_IPT77x.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 +355 -42
  70. package/package.json +11 -6
  71. package/dist/client/assets/channel-2mY-lkL6.js +0 -1
  72. package/dist/client/assets/classDiagram-2ON5EDUG-CxKjlsUh.js +0 -1
  73. package/dist/client/assets/classDiagram-v2-WZHVMYZB-CxKjlsUh.js +0 -1
  74. package/dist/client/assets/clone-CTVr6Med.js +0 -1
  75. package/dist/client/assets/main-BYKQWpTo.css +0 -1
  76. package/dist/client/assets/stateDiagram-v2-4FDKWEC3-pfnkktuh.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
  }
@@ -61453,7 +61505,9 @@ function reconcileKindForRepo(repo) {
61453
61505
  path: repo.path,
61454
61506
  pinned: false,
61455
61507
  ...main2?.origin ? { origin: main2.origin } : {},
61456
- ...main2?.groupId ? { groupId: main2.groupId } : {}
61508
+ ...main2?.groupId ? { groupId: main2.groupId } : {},
61509
+ ...main2?.unread ? { unread: true } : {},
61510
+ ...main2?.title ? { title: main2.title } : {}
61457
61511
  }
61458
61512
  ];
61459
61513
  }
@@ -61504,6 +61558,8 @@ var init_repos = __esm({
61504
61558
  }
61505
61559
  } : {},
61506
61560
  ...row.groupId ? { groupId: row.groupId } : {},
61561
+ ...row.unread ? { unread: true } : {},
61562
+ ...row.title ? { title: row.title } : {},
61507
61563
  ...row.supersededBy ? { supersededBy: row.supersededBy } : {},
61508
61564
  ...row.continuedFrom ? { continuedFrom: row.continuedFrom } : {},
61509
61565
  ...row.divergence ? { divergence: row.divergence } : {}
@@ -61557,7 +61613,9 @@ var init_repos = __esm({
61557
61613
  originWorktreeId: worktrees.originWorktreeId,
61558
61614
  originChatId: worktrees.originChatId,
61559
61615
  originTerminalId: worktrees.originTerminalId,
61560
- groupId: worktrees.groupId
61616
+ groupId: worktrees.groupId,
61617
+ unread: worktrees.unread,
61618
+ title: worktrees.title
61561
61619
  }).from(worktrees).all().map((w2) => [`${w2.repoName}\0${w2.name}`, w2])
61562
61620
  );
61563
61621
  const continuation = new Map(
@@ -61599,6 +61657,8 @@ var init_repos = __esm({
61599
61657
  originChatId: origin?.originChatId ?? null,
61600
61658
  originTerminalId: origin?.originTerminalId ?? null,
61601
61659
  groupId: origin?.groupId ?? null,
61660
+ unread: origin?.unread ?? false,
61661
+ title: origin?.title ?? null,
61602
61662
  repoName: repo.name,
61603
61663
  name: wt.name,
61604
61664
  branch: wt.branch,
@@ -61807,6 +61867,16 @@ var init_repos = __esm({
61807
61867
  }).from(repos).where(eq2(repos.name, name5)).get();
61808
61868
  return row ? { allowedHostIds: row.allowedHostIds ?? null, primaryHostId: row.primaryHostId ?? null } : void 0;
61809
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
+ }
61810
61880
  /**
61811
61881
  * Puts a worktree in a group, or in none with `null`. Focused UPDATE, because `saveAll` keeps
61812
61882
  * the stored group and never writes the one in its snapshot.
@@ -61820,6 +61890,19 @@ var init_repos = __esm({
61820
61890
  )
61821
61891
  ).run();
61822
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
+ }
61823
61906
  /**
61824
61907
  * Records where a worktree was started from, or clears it with `null`. Focused UPDATE, because
61825
61908
  * `saveAll` keeps the stored origin and never writes the one in its snapshot.
@@ -66901,13 +66984,28 @@ var init_git_refusals = __esm({
66901
66984
 
66902
66985
  // ../../packages/shared/src/branch-name.ts
66903
66986
  function slugifyBranchName(input2) {
66904
- 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;
66905
66994
  }
66906
66995
  var init_branch_name = __esm({
66907
66996
  "../../packages/shared/src/branch-name.ts"() {
66908
66997
  }
66909
66998
  });
66910
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
+
66911
67009
  // src/server/infra/db/queries/pending-removals.ts
66912
67010
  var PendingRemovalQueries;
66913
67011
  var init_pending_removals = __esm({
@@ -68298,6 +68396,11 @@ var init_mime_types = __esm({
68298
68396
  ".ico": "image/x-icon",
68299
68397
  ".bmp": "image/bmp",
68300
68398
  ".avif": "image/avif",
68399
+ ".mp4": "video/mp4",
68400
+ ".m4v": "video/mp4",
68401
+ ".mov": "video/quicktime",
68402
+ ".webm": "video/webm",
68403
+ ".ogv": "video/ogg",
68301
68404
  ".pdf": "application/pdf",
68302
68405
  ".json": "application/json",
68303
68406
  ".txt": "text/plain",
@@ -68735,7 +68838,7 @@ var init_channel = __esm({
68735
68838
  });
68736
68839
 
68737
68840
  // ../../packages/link/src/chats.ts
68738
- 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;
68739
68842
  var init_chats2 = __esm({
68740
68843
  "../../packages/link/src/chats.ts"() {
68741
68844
  "use strict";
@@ -68746,6 +68849,7 @@ var init_chats2 = __esm({
68746
68849
  METHOD_CHAT_RELEASE = "chat.release";
68747
68850
  METHOD_CHAT_SET_CONFIG = "chat.setConfigOption";
68748
68851
  METHOD_CHAT_LIST_SESSIONS = "chat.listSessions";
68852
+ METHOD_CHAT_VIEW_SESSION = "chat.viewSession";
68749
68853
  METHOD_CHAT_REPLICATE = "chat.replicate";
68750
68854
  METHOD_CHAT_ID_BLOCK = "chat.idBlock";
68751
68855
  METHOD_CHAT_HISTORY = "chat.history";
@@ -69688,7 +69792,7 @@ function forwardedTask(id, options) {
69688
69792
  codingAgentId: options.codingAgentId
69689
69793
  };
69690
69794
  }
69691
- 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;
69692
69796
  var init_chat_ownership_service = __esm({
69693
69797
  "src/server/services/chat-ownership-service.ts"() {
69694
69798
  "use strict";
@@ -69709,6 +69813,7 @@ var init_chat_ownership_service = __esm({
69709
69813
  init_worktree_service();
69710
69814
  log23 = createLogger("chat-ownership");
69711
69815
  FORWARD_TIMEOUT_MS = 6e4;
69816
+ VIEW_SESSION_TIMEOUT_MS = 2e5;
69712
69817
  RETRY_MS = 5e3;
69713
69818
  HISTORY_MAX_ROWS = 1e3;
69714
69819
  ChatOwnershipService = class {
@@ -70139,6 +70244,35 @@ var init_chat_ownership_service = __esm({
70139
70244
  );
70140
70245
  return readPayload(session, enc);
70141
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
+ }
70142
70276
  /** The agent's own session list on the owner, or null when it can't list or is offline. */
70143
70277
  async listSessions(chatId) {
70144
70278
  const hostId2 = this.ownerOf(chatId);
@@ -71308,7 +71442,7 @@ var init_remote_host = __esm({
71308
71442
  stat: (path2, options) => this.rpc.call("fs.stat", { path: path2, followSymlinks: options?.followSymlinks }),
71309
71443
  realpath: (path2) => this.rpc.call("fs.realpath", { path: path2 }),
71310
71444
  readFile: (path2) => this.rpc.callBytes("fs.readFile", { path: path2 }),
71311
- readStream: (path2) => this.streamOf("fs.readStream", { path: path2 }),
71445
+ readStream: (path2, range) => this.streamOf("fs.readStream", { path: path2, start: range?.start, end: range?.end }),
71312
71446
  writeFile: (path2, data, options) => this.writeFile(path2, data, options),
71313
71447
  glob: (pattern, cwd) => this.rpc.call("fs.glob", { pattern, cwd }),
71314
71448
  mkdtemp: (prefix) => this.rpc.call("fs.mkdtemp", { prefix }),
@@ -77460,6 +77594,8 @@ __export(worktree_service_exports, {
77460
77594
  worktreeSetGroupInput: () => worktreeSetGroupInput,
77461
77595
  worktreeSetOriginInput: () => worktreeSetOriginInput,
77462
77596
  worktreeSetPinnedInput: () => worktreeSetPinnedInput,
77597
+ worktreeSetTitleInput: () => worktreeSetTitleInput,
77598
+ worktreeSetUnreadInput: () => worktreeSetUnreadInput,
77463
77599
  worktreeVia: () => worktreeVia
77464
77600
  });
77465
77601
  import { randomUUID as randomUUID11 } from "node:crypto";
@@ -77505,7 +77641,7 @@ function expandHome(given, home, hostId2) {
77505
77641
  }
77506
77642
  return path2;
77507
77643
  }
77508
- 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;
77509
77645
  var init_worktree_service = __esm({
77510
77646
  "src/server/services/worktree-service.ts"() {
77511
77647
  "use strict";
@@ -77516,6 +77652,7 @@ var init_worktree_service = __esm({
77516
77652
  init_path();
77517
77653
  init_src();
77518
77654
  init_branch_name();
77655
+ init_worktree_display();
77519
77656
  init_worktree_id();
77520
77657
  init_zod();
77521
77658
  init_errors6();
@@ -77633,6 +77770,17 @@ var init_worktree_service = __esm({
77633
77770
  // Also moves every worktree started from this one, on any repo and host, and theirs in turn.
77634
77771
  withChildren: external_exports2.boolean().optional()
77635
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
+ });
77636
77784
  worktreeReorderInput = external_exports2.object({
77637
77785
  // The worktrees of one list in their new order.
77638
77786
  order: external_exports2.array(worktreeRefInput).min(1).max(500),
@@ -77873,6 +78021,14 @@ var init_worktree_service = __esm({
77873
78021
  (wt) => sameName(wt) && (wt.hostId ?? hostRegistry.local.id) === hostId2
77874
78022
  );
77875
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
+ }
77876
78032
  assertHostAllowed(repo.name, hostId2);
77877
78033
  const elsewhere = listedWorktrees(repo).find(
77878
78034
  (wt) => sameName(wt) && !wt.supersededBy && (wt.hostId ?? hostRegistry.local.id) !== hostId2
@@ -78396,6 +78552,20 @@ var init_worktree_service = __esm({
78396
78552
  emit({ kind: "worktree-origin-changed", worktreeId: worktreeId4 });
78397
78553
  return { ok: true };
78398
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
+ }
78399
78569
  /**
78400
78570
  * Puts a worktree in a group, or in none with `null`. With `withChildren` every worktree
78401
78571
  * started from it, and theirs in turn, moves too.
@@ -78430,6 +78600,42 @@ var init_worktree_service = __esm({
78430
78600
  emit({ kind: "worktree-origin-changed", worktreeId: input2.worktreeId });
78431
78601
  return { ok: true, moved };
78432
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
+ }
78433
78639
  /**
78434
78640
  * Stores the order of one list of worktrees, and optionally the new parent of one of them. The
78435
78641
  * parent change goes first, so a refused parent leaves the order untouched.
@@ -83449,7 +83655,10 @@ var init_agent_session_service = __esm({
83449
83655
  async ensureSession(chatId, purpose, options = {}) {
83450
83656
  const chat = chatService.get(chatId);
83451
83657
  if (!chat) throw new ChatNotFoundError(chatId);
83452
- if (chat.ownerHostId) return null;
83658
+ if (chat.ownerHostId) {
83659
+ if (purpose === "view") await chatOwnershipService.viewSession(chatId);
83660
+ return null;
83661
+ }
83453
83662
  if (purpose === "prompt") await ephemeralLifecycleService.ensureAwake(chat.worktreeId);
83454
83663
  const worktree = worktreeService.resolve(chat.worktreeId);
83455
83664
  if (!worktree) throw new Error(`Worktree not found: ${chat.worktreeId}`);
@@ -94704,6 +94913,8 @@ var RepoService = class {
94704
94913
  head: wt.head,
94705
94914
  pinned: tracked2?.pinned ?? false,
94706
94915
  ...tracked2?.groupId ? { groupId: tracked2.groupId } : {},
94916
+ ...tracked2?.unread ? { unread: true } : {},
94917
+ ...tracked2?.title ? { title: tracked2.title } : {},
94707
94918
  ...tracked2?.sortIndex === void 0 ? {} : { sortIndex: tracked2.sortIndex },
94708
94919
  ...tracked2?.pinSortIndex === void 0 ? {} : { pinSortIndex: tracked2.pinSortIndex }
94709
94920
  };
@@ -94745,6 +94956,8 @@ var RepoService = class {
94745
94956
  lastCheckpointAt: lastCheckpoints.get(worktreeId4) ?? null,
94746
94957
  agent: status?.agent ?? null,
94747
94958
  groupId: wt.groupId ?? null,
94959
+ unread: wt.unread ?? false,
94960
+ title: wt.title ?? null,
94748
94961
  origin: links.origins.get(worktreeId4) ?? null,
94749
94962
  children: links.children.get(worktreeId4) ?? []
94750
94963
  };
@@ -96220,6 +96433,10 @@ ${diff}`).digest("hex");
96220
96433
  if (worktree.groupId) {
96221
96434
  this.repos.setGroup(repo.name, worktree.name, worktree.groupId, toHostId);
96222
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
+ }
96223
96440
  this.repos.setSupersededBy(repo.name, worktree.name, fromHostId, newId);
96224
96441
  await Promise.all([
96225
96442
  workerLeaseService.pushBlockedPaths(fromHostId),
@@ -109987,6 +110204,7 @@ init_zod();
109987
110204
  init_errors6();
109988
110205
  init_agent_login_service();
109989
110206
  init_agent_session_service();
110207
+ init_chat_ownership_service();
109990
110208
  init_chat_service();
109991
110209
  init_settings_service();
109992
110210
  init_task_service();
@@ -110158,6 +110376,7 @@ var chatsRouter = t.router({
110158
110376
  summary,
110159
110377
  lastModified
110160
110378
  });
110379
+ chatOwnershipService.syncActiveSession(input2.chatId);
110161
110380
  return { ok: true };
110162
110381
  }),
110163
110382
  send: publicProcedure.input(
@@ -110319,6 +110538,7 @@ init_zod();
110319
110538
 
110320
110539
  // src/server/services/cli-service.ts
110321
110540
  init_cli_binary();
110541
+ init_local_host_enabled();
110322
110542
  import { execFile as execFile15 } from "node:child_process";
110323
110543
  import {
110324
110544
  accessSync as accessSync3,
@@ -110339,10 +110559,14 @@ function windowsBinDir() {
110339
110559
  return join49(localAppData, "band", "bin");
110340
110560
  }
110341
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
+ }
110342
110566
  function checkCliWindows() {
110343
110567
  let contents;
110344
110568
  try {
110345
- contents = readFileSync11(SYMLINK_PATH, "utf-8");
110569
+ contents = readFileSync11(symlinkPath(), "utf-8");
110346
110570
  } catch (err) {
110347
110571
  const code = err.code;
110348
110572
  if (code === "EACCES") return "NotWritable";
@@ -110354,18 +110578,22 @@ function checkCliWindows() {
110354
110578
  if (basename3(target).toLowerCase() !== "band.exe") return "ConflictingBinary";
110355
110579
  return existsSync16(target) ? "Installed" : "NotInstalled";
110356
110580
  }
110581
+ function isCliCheckApplicable(admin) {
110582
+ return isLocalHostEnabled() && admin;
110583
+ }
110357
110584
  async function checkCli() {
110358
110585
  if (platform2() === "win32") {
110359
110586
  return checkCliWindows();
110360
110587
  }
110588
+ const linkPath = symlinkPath();
110361
110589
  try {
110362
- const stat7 = lstatSync4(SYMLINK_PATH);
110590
+ const stat7 = lstatSync4(linkPath);
110363
110591
  if (!stat7.isSymbolicLink()) {
110364
110592
  return "ConflictingBinary";
110365
110593
  }
110366
110594
  let target;
110367
110595
  try {
110368
- target = realpathSync5(SYMLINK_PATH);
110596
+ target = realpathSync5(linkPath);
110369
110597
  } catch {
110370
110598
  return "NotInstalled";
110371
110599
  }
@@ -110374,6 +110602,13 @@ async function checkCli() {
110374
110602
  if (isCargoBuild || isElectronSidecar) {
110375
110603
  return "Installed";
110376
110604
  }
110605
+ const resolved = findCliBinary();
110606
+ if (resolved) {
110607
+ try {
110608
+ if (realpathSync5(resolved) === target) return "Installed";
110609
+ } catch {
110610
+ }
110611
+ }
110377
110612
  const isStaleAppLink = target.toLowerCase().includes("/band.app/contents/macos/");
110378
110613
  if (isStaleAppLink) {
110379
110614
  return "NotInstalled";
@@ -110383,7 +110618,7 @@ async function checkCli() {
110383
110618
  const code = err.code;
110384
110619
  if (code === "ENOENT") {
110385
110620
  try {
110386
- lstatSync4("/usr/local/bin");
110621
+ lstatSync4(dirname13(linkPath));
110387
110622
  return "NotInstalled";
110388
110623
  } catch {
110389
110624
  return "DirNotFound";
@@ -110408,7 +110643,7 @@ function noBinaryError(env = process.env) {
110408
110643
  return new Error("Bundled CLI binary missing - try reinstalling Band");
110409
110644
  }
110410
110645
  return new Error(
110411
- "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`
110412
110647
  );
110413
110648
  }
110414
110649
  function runReg(args) {
@@ -110460,39 +110695,43 @@ async function installCli(_opts = {}) {
110460
110695
  await installCliWindows(binaryPath);
110461
110696
  return;
110462
110697
  }
110463
- const dir = dirname13(SYMLINK_PATH);
110698
+ const linkPath = symlinkPath();
110699
+ const dir = dirname13(linkPath);
110464
110700
  if (isDirWritable(dir)) {
110465
110701
  try {
110466
- lstatSync4(SYMLINK_PATH);
110467
- unlinkSync6(SYMLINK_PATH);
110702
+ lstatSync4(linkPath);
110703
+ unlinkSync6(linkPath);
110468
110704
  } catch (err) {
110469
110705
  const code = err.code;
110470
110706
  if (code !== "ENOENT") throw err;
110471
110707
  }
110472
- symlinkSync(binaryPath, SYMLINK_PATH);
110708
+ symlinkSync(binaryPath, linkPath);
110473
110709
  return;
110474
110710
  }
110475
110711
  if (platform2() === "darwin") {
110476
110712
  throw new Error("elevation-required");
110477
110713
  }
110478
- 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}"`);
110479
110715
  }
110480
110716
  function resolveCliPaths() {
110481
110717
  const binaryPath = findCliBinary();
110482
110718
  if (!binaryPath) return null;
110483
- return { binaryPath, symlinkPath: SYMLINK_PATH };
110719
+ return { binaryPath, symlinkPath: symlinkPath() };
110484
110720
  }
110485
110721
 
110486
110722
  // src/server/api/cli/router.ts
110487
110723
  var cliRouter = t.router({
110488
- check: publicProcedure.query(async () => {
110489
- const status = await checkCli();
110490
- 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 };
110491
110730
  }),
110492
- resolve: publicProcedure.query(() => {
110731
+ resolve: adminProcedure.query(() => {
110493
110732
  return resolveCliPaths();
110494
110733
  }),
110495
- 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 }) => {
110496
110735
  try {
110497
110736
  await installCli({ allowPrompt: input2?.allowPrompt });
110498
110737
  return { ok: true };
@@ -111798,7 +112037,7 @@ init_registry();
111798
112037
  // package.json
111799
112038
  var package_default = {
111800
112039
  name: "@band-app/server",
111801
- version: "0.54.3",
112040
+ version: "0.54.4",
111802
112041
  type: "module",
111803
112042
  types: "src/index.ts",
111804
112043
  exports: {
@@ -112133,6 +112372,7 @@ var RELAY_PROCEDURES = /* @__PURE__ */ new Set([
112133
112372
  "repos.remove",
112134
112373
  "groups.list",
112135
112374
  "worktrees.setGroup",
112375
+ "worktrees.setTitle",
112136
112376
  "hosts.list",
112137
112377
  "hosts.forAgents",
112138
112378
  "workers.list",
@@ -118519,6 +118759,7 @@ init_zod();
118519
118759
  init_branch_status_poller();
118520
118760
  init_state();
118521
118761
  init_watcher_service();
118762
+ init_worktree_service();
118522
118763
  var statusesRouter = t.router({
118523
118764
  get: publicProcedure.input(external_exports2.object({ worktreeId: external_exports2.string() })).query(({ input: input2 }) => {
118524
118765
  return getWorktreeStatus(input2.worktreeId);
@@ -118545,7 +118786,15 @@ var statusesRouter = t.router({
118545
118786
  refreshBranchStatus: publicProcedure.input(external_exports2.object({ worktreeId: external_exports2.string() })).mutation(async ({ input: input2 }) => ({
118546
118787
  refreshed: await branchStatusPoller.refreshWorktree(input2.worktreeId)
118547
118788
  })),
118548
- 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);
118549
118798
  const status = acknowledgeWorktreeAttention(input2.worktreeId);
118550
118799
  if (status) emit({ kind: "update", status });
118551
118800
  return { ok: true };
@@ -122294,6 +122543,23 @@ var worktreesRouter = t.router({
122294
122543
  throwAsTrpcError3(err);
122295
122544
  }
122296
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
+ }),
122297
122563
  gitPull: publicProcedure.input(worktreeGitInput).mutation(async ({ input: input2 }) => {
122298
122564
  try {
122299
122565
  return await worktreeService.gitPull(input2);
@@ -122408,6 +122674,7 @@ function discoverProcedures() {
122408
122674
  if (path2 === "repos.addByUrl") continue;
122409
122675
  if (path2 === "repos.inspectFolder" || path2 === "repos.resolveRemote") continue;
122410
122676
  if (path2 === "worktrees.setOrigin") continue;
122677
+ if (path2 === "worktrees.setUnread") continue;
122411
122678
  if (path2.startsWith("groups.") && path2 !== "groups.list") continue;
122412
122679
  if (path2 === "worktrees.reorder") continue;
122413
122680
  if (path2 === "environment.build") continue;
@@ -124686,6 +124953,37 @@ async function handleLspConnection(ws, req) {
124686
124953
  // start-server.ts
124687
124954
  init_webhook();
124688
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
+
124689
124987
  // src/server/services/_utils/hosted-file.ts
124690
124988
  init_registry();
124691
124989
  import { basename as basename6, dirname as dirname17, join as join57 } from "node:path";
@@ -133247,7 +133545,7 @@ async function serveHostedFile(res, kind, worktreeId4, rawName, chatId) {
133247
133545
  );
133248
133546
  return true;
133249
133547
  }
133250
- async function serveWorktreeFile(res, worktreeId4, rawPath, extraHeaders = {}, visualEditNonce) {
133548
+ async function serveWorktreeFile(res, worktreeId4, rawPath, extraHeaders = {}, visualEditNonce, download = false, rangeHeader) {
133251
133549
  const worktree = worktreeService.resolve(worktreeId4);
133252
133550
  if (!worktree) {
133253
133551
  res.writeHead(404);
@@ -133270,6 +133568,11 @@ async function serveWorktreeFile(res, worktreeId4, rawPath, extraHeaders = {}, v
133270
133568
  return;
133271
133569
  }
133272
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
+ }
133273
133576
  const contentType = mimeTypeFromFilename(basename7(target));
133274
133577
  const isHtml = contentType.startsWith("text/html");
133275
133578
  const editHtml = isHtml && visualEditNonce !== void 0 && fileStat.kind === "file";
@@ -133285,28 +133588,37 @@ async function serveWorktreeFile(res, worktreeId4, rawPath, extraHeaders = {}, v
133285
133588
  body = Buffer.from(annotateHtmlForEdit(source, visualEditNonce), "utf8");
133286
133589
  }
133287
133590
  }
133288
- 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, {
133289
133599
  "Content-Type": contentType,
133290
- "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",
133291
133603
  "Cache-Control": "private, no-cache",
133292
- // HTML from a repo must not run with the hub's origin.
133293
- ...isHtml ? {
133294
- "Content-Security-Policy": "sandbox allow-scripts",
133295
- "X-Content-Type-Options": "nosniff"
133296
- } : {},
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)) } : {},
133297
133608
  ...extraHeaders
133298
133609
  });
133299
133610
  if (body) {
133300
133611
  res.end(body);
133301
133612
  return;
133302
133613
  }
133303
- const stream = Readable2.from(fs3.readStream(target), {
133614
+ const stream = Readable2.from(fs3.readStream(target, range ?? void 0), {
133304
133615
  objectMode: false,
133305
133616
  highWaterMark: 64 * 1024
133306
133617
  });
133307
133618
  if (trailer) {
133308
133619
  stream.on("end", () => res.end(trailer));
133309
133620
  stream.on("error", () => res.destroy());
133621
+ res.once("close", () => stream.destroy());
133310
133622
  stream.pipe(res, { end: false });
133311
133623
  } else {
133312
133624
  pipeline2(stream, res, () => {
@@ -133684,6 +133996,7 @@ async function main() {
133684
133996
  "/api/worktree-file/".length,
133685
133997
  queryIdx === -1 ? void 0 : queryIdx
133686
133998
  );
133999
+ const download = queryIdx !== -1 && new URLSearchParams(req.url.slice(queryIdx)).get("download") === "1";
133687
134000
  const slashIdx = rest.indexOf("/");
133688
134001
  if (slashIdx === -1) {
133689
134002
  res.writeHead(400);
@@ -133705,7 +134018,7 @@ async function main() {
133705
134018
  res.end("Bad request");
133706
134019
  return;
133707
134020
  }
133708
- await serveWorktreeFile(res, wId, filePath);
134021
+ await serveWorktreeFile(res, wId, filePath, {}, void 0, download, req.headers.range);
133709
134022
  return;
133710
134023
  }
133711
134024
  if (req.url?.startsWith("/api/cdp/tabs")) {