@hydra-acp/cli 0.1.52 → 0.1.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -168,8 +168,77 @@ var init_service_token = __esm({
168
168
  }
169
169
  });
170
170
 
171
- // src/core/config.ts
171
+ // src/core/json-store.ts
172
172
  import * as fs2 from "fs/promises";
173
+ import * as fsSync from "fs";
174
+ import { randomBytes } from "crypto";
175
+ async function writeJsonAtomic(filePath, data, opts = {}) {
176
+ const pretty = opts.pretty ?? true;
177
+ const body = (pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data)) + "\n";
178
+ await writeFileAtomic(filePath, body, opts);
179
+ }
180
+ async function writeFileAtomic(filePath, body, opts = {}) {
181
+ const dir = dirname(filePath);
182
+ await fs2.mkdir(dir, { recursive: true });
183
+ const tmp = `${filePath}.tmp-${process.pid}-${randSuffix()}`;
184
+ try {
185
+ const writeOpts = {
186
+ encoding: "utf8"
187
+ };
188
+ if (opts.mode !== void 0) {
189
+ writeOpts.mode = opts.mode;
190
+ }
191
+ await fs2.writeFile(tmp, body, writeOpts);
192
+ await fs2.rename(tmp, filePath);
193
+ } catch (err) {
194
+ await fs2.unlink(tmp).catch(() => void 0);
195
+ throw err;
196
+ }
197
+ if (opts.mode !== void 0) {
198
+ try {
199
+ fsSync.chmodSync(filePath, opts.mode);
200
+ } catch {
201
+ }
202
+ }
203
+ }
204
+ async function readJsonSafe(filePath) {
205
+ let raw;
206
+ try {
207
+ raw = await fs2.readFile(filePath, "utf8");
208
+ } catch (err) {
209
+ const e = err;
210
+ if (e.code === "ENOENT") {
211
+ return void 0;
212
+ }
213
+ throw err;
214
+ }
215
+ if (raw.trim().length === 0) {
216
+ return void 0;
217
+ }
218
+ try {
219
+ return JSON.parse(raw);
220
+ } catch {
221
+ return void 0;
222
+ }
223
+ }
224
+ function dirname(p) {
225
+ const slash = p.lastIndexOf("/");
226
+ if (slash <= 0) {
227
+ return ".";
228
+ }
229
+ return p.slice(0, slash);
230
+ }
231
+ function randSuffix() {
232
+ return randomBytes(4).toString("hex");
233
+ }
234
+ var init_json_store = __esm({
235
+ "src/core/json-store.ts"() {
236
+ "use strict";
237
+ }
238
+ });
239
+
240
+ // src/core/config.ts
241
+ import * as fs3 from "fs/promises";
173
242
  import { homedir as homedir2 } from "os";
174
243
  import { z } from "zod";
175
244
  function extensionList(config) {
@@ -185,17 +254,8 @@ function transformerList(config) {
185
254
  }));
186
255
  }
187
256
  async function readConfigFile() {
188
- let raw;
189
- try {
190
- raw = await fs2.readFile(paths.config(), "utf8");
191
- } catch (err) {
192
- const e = err;
193
- if (e.code === "ENOENT") {
194
- return {};
195
- }
196
- throw err;
197
- }
198
- return JSON.parse(raw);
257
+ const parsed = await readJsonSafe(paths.config());
258
+ return parsed ?? {};
199
259
  }
200
260
  async function migrateLegacyAuthToken() {
201
261
  const raw = await readConfigFile();
@@ -206,7 +266,7 @@ async function migrateLegacyAuthToken() {
206
266
  }
207
267
  let tokenFileExists = false;
208
268
  try {
209
- await fs2.access(paths.authToken());
269
+ await fs3.access(paths.authToken());
210
270
  tokenFileExists = true;
211
271
  } catch (err) {
212
272
  const e = err;
@@ -224,10 +284,7 @@ async function migrateLegacyAuthToken() {
224
284
  if (Object.keys(daemon).length === 0) {
225
285
  delete raw.daemon;
226
286
  }
227
- await fs2.writeFile(paths.config(), JSON.stringify(raw, null, 2) + "\n", {
228
- encoding: "utf8",
229
- mode: 384
230
- });
287
+ await writeJsonAtomic(paths.config(), raw, { mode: 384 });
231
288
  process.stderr.write(
232
289
  `hydra-acp: migrated auth token from ${paths.config()} to ${paths.authToken()}.
233
290
  `
@@ -255,6 +312,7 @@ var init_config = __esm({
255
312
  "use strict";
256
313
  init_paths();
257
314
  init_service_token();
315
+ init_json_store();
258
316
  REGISTRY_URL_DEFAULT = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
259
317
  TlsConfig = z.object({
260
318
  cert: z.string(),
@@ -343,7 +401,22 @@ var init_config = __esm({
343
401
  // shared across all sessions; it's append-only on disk, so long-lived
344
402
  // installs can grow past this — it's enforced at load time and per
345
403
  // append in memory.
346
- promptHistoryMaxEntries: z.number().int().positive().default(2e3)
404
+ promptHistoryMaxEntries: z.number().int().positive().default(2e3),
405
+ // How edit-style tool calls (Edit, Write, str_replace) render in
406
+ // scrollback, *in addition to* the normal tool row inside the tools
407
+ // block.
408
+ // "none" — nothing extra; the collapsed tool row is the only signal.
409
+ // "edit" (default) — a one-line scrollback mark naming the file
410
+ // that was touched, so the user can scroll back and see which
411
+ // files moved without expanding the tools block. Suppressed on
412
+ // tool-only turns (no agent prose) since the marks would only
413
+ // duplicate the still-visible tool rows.
414
+ // "diff" — same mark plus a syntax-highlighted unified diff body,
415
+ // Claude Code's Update(file) look.
416
+ // The diff payload is extracted from the ACP wire (content[]
417
+ // type:"diff" entries, falling back to rawInput shapes), so any agent
418
+ // that emits one of those shapes gets the treatment.
419
+ showFileUpdates: z.enum(["none", "edit", "diff"]).default("edit")
347
420
  });
348
421
  ExtensionName = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/, "extension name must be filename-safe");
349
422
  ExtensionBody = z.object({
@@ -398,7 +471,8 @@ var init_config = __esm({
398
471
  progressIndicator: true,
399
472
  defaultEnterAction: "amend",
400
473
  showThoughts: true,
401
- promptHistoryMaxEntries: 2e3
474
+ promptHistoryMaxEntries: 2e3,
475
+ showFileUpdates: "edit"
402
476
  })
403
477
  });
404
478
  }
@@ -480,8 +554,6 @@ var init_remote_url = __esm({
480
554
  });
481
555
 
482
556
  // src/core/remotes-store.ts
483
- import * as fs3 from "fs/promises";
484
- import * as fsSync from "fs";
485
557
  function hostKey(host, port) {
486
558
  return `${host}:${port}`;
487
559
  }
@@ -504,21 +576,9 @@ function splitKey(key) {
504
576
  }
505
577
  return { host, port };
506
578
  }
507
- async function readFile4() {
508
- let raw;
509
- try {
510
- raw = await fs3.readFile(paths.remotes(), "utf8");
511
- } catch (err) {
512
- const e = err;
513
- if (e.code === "ENOENT") {
514
- return { version: 1, entries: {} };
515
- }
516
- throw err;
517
- }
518
- let parsed;
519
- try {
520
- parsed = JSON.parse(raw);
521
- } catch {
579
+ async function readFile3() {
580
+ const parsed = await readJsonSafe(paths.remotes());
581
+ if (parsed === void 0) {
522
582
  return { version: 1, entries: {} };
523
583
  }
524
584
  return normalise(parsed);
@@ -549,32 +609,22 @@ function normalise(raw) {
549
609
  }
550
610
  return { version: 1, entries: out };
551
611
  }
552
- async function writeFile4(data) {
553
- const dir = paths.home();
554
- await fs3.mkdir(dir, { recursive: true });
555
- const tmp = paths.remotes() + ".tmp";
556
- await fs3.writeFile(tmp, JSON.stringify(data, null, 2) + "\n", {
557
- encoding: "utf8",
558
- mode: 384
559
- });
560
- await fs3.rename(tmp, paths.remotes());
561
- try {
562
- fsSync.chmodSync(paths.remotes(), 384);
563
- } catch {
564
- }
612
+ async function writeFile3(data) {
613
+ await writeJsonAtomic(paths.remotes(), data, { mode: 384 });
565
614
  }
566
615
  var RemotesStore;
567
616
  var init_remotes_store = __esm({
568
617
  "src/core/remotes-store.ts"() {
569
618
  "use strict";
570
619
  init_paths();
620
+ init_json_store();
571
621
  RemotesStore = class _RemotesStore {
572
622
  data;
573
623
  constructor(data) {
574
624
  this.data = data;
575
625
  }
576
626
  static async load() {
577
- const data = await readFile4();
627
+ const data = await readFile3();
578
628
  const now = Date.now();
579
629
  const filtered = {};
580
630
  let dropped = false;
@@ -587,7 +637,7 @@ var init_remotes_store = __esm({
587
637
  }
588
638
  const final = { version: 1, entries: filtered };
589
639
  if (dropped) {
590
- await writeFile4(final);
640
+ await writeFile3(final);
591
641
  }
592
642
  return new _RemotesStore(final);
593
643
  }
@@ -603,7 +653,7 @@ var init_remotes_store = __esm({
603
653
  }
604
654
  async set(host, port, credential) {
605
655
  this.data.entries[hostKey(host, port)] = credential;
606
- await writeFile4(this.data);
656
+ await writeFile3(this.data);
607
657
  }
608
658
  async delete(host, port) {
609
659
  const key = hostKey(host, port);
@@ -611,7 +661,7 @@ var init_remotes_store = __esm({
611
661
  return false;
612
662
  }
613
663
  delete this.data.entries[key];
614
- await writeFile4(this.data);
664
+ await writeFile3(this.data);
615
665
  return true;
616
666
  }
617
667
  list() {
@@ -1172,6 +1222,11 @@ var init_types = __esm({
1172
1222
  importedFromUpstreamSessionId: z3.string().optional(),
1173
1223
  // Set when this session was spawned as a child by a transformer.
1174
1224
  parentSessionId: z3.string().optional(),
1225
+ // Local-fork breadcrumbs set by hydra-acp/fork_session. Distinct from
1226
+ // the imported* family above: a fork is a local branch off another
1227
+ // local session, an import is a cross-machine takeover.
1228
+ forkedFromSessionId: z3.string().optional(),
1229
+ forkedFromMessageId: z3.string().optional(),
1175
1230
  // clientInfo from the process that issued session/new. Lets list views
1176
1231
  // hide cat-style ancillary sessions by default while letting an
1177
1232
  // override flag surface them.
@@ -2386,6 +2441,8 @@ var init_session = __esm({
2386
2441
  agentCapabilities;
2387
2442
  agentArgs;
2388
2443
  parentSessionId;
2444
+ forkedFromSessionId;
2445
+ forkedFromMessageId;
2389
2446
  originatingClient;
2390
2447
  title;
2391
2448
  // Snapshot state delivered to attaching clients via the attach
@@ -2414,6 +2471,13 @@ var init_session = __esm({
2414
2471
  // enqueue) and leave the file out of sync with in-memory state.
2415
2472
  queueWriteChain = Promise.resolve();
2416
2473
  closed = false;
2474
+ // Set true at the start of close() / markClosed before any await yields.
2475
+ // drainQueue checks this between iterations and bails out, so a queued
2476
+ // entry can't be promoted to currentEntry (with its prompt_received and
2477
+ // synthesized turn_complete(interrupted)) while the session is tearing
2478
+ // down. markClosed sweeps the remaining queue with the normal abandoned
2479
+ // / cancelled handling.
2480
+ closing = false;
2417
2481
  closeHandlers = [];
2418
2482
  titleHandlers = [];
2419
2483
  // Subscribers notified after every entry that's actually persisted to
@@ -2541,6 +2605,8 @@ var init_session = __esm({
2541
2605
  this.agentCapabilities = init.agentCapabilities;
2542
2606
  this.agentArgs = init.agentArgs;
2543
2607
  this.parentSessionId = init.parentSessionId;
2608
+ this.forkedFromSessionId = init.forkedFromSessionId;
2609
+ this.forkedFromMessageId = init.forkedFromMessageId;
2544
2610
  this.originatingClient = init.originatingClient;
2545
2611
  this.title = init.title;
2546
2612
  this.currentModel = init.currentModel;
@@ -3649,6 +3715,7 @@ var init_session = __esm({
3649
3715
  if (this.closed) {
3650
3716
  return;
3651
3717
  }
3718
+ this.closing = true;
3652
3719
  this.logger?.info(
3653
3720
  `session ${this.sessionId} closing deleteRecord=${opts.deleteRecord ?? false} regenTitle=${opts.regenTitle ?? false}`
3654
3721
  );
@@ -4780,21 +4847,22 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
4780
4847
  if (this.closed) {
4781
4848
  return;
4782
4849
  }
4850
+ this.closing = true;
4783
4851
  this.closed = true;
4784
4852
  this.cancelIdleTimer();
4785
4853
  if (this.extensionCommandsUnsub) {
4786
4854
  this.extensionCommandsUnsub();
4787
4855
  this.extensionCommandsUnsub = void 0;
4788
4856
  }
4789
- if (this.currentEntry?.kind === "user") {
4857
+ if (this.currentEntry?.kind === "user" && !this.recentlyTerminal.has(this.currentEntry.messageId)) {
4790
4858
  this.broadcastTurnComplete(
4791
4859
  this.currentEntry.clientId,
4792
4860
  { stopReason: "interrupted" },
4793
4861
  this.currentEntry.messageId,
4794
4862
  this.currentEntry.wasAmend
4795
4863
  );
4796
- this.currentEntry = void 0;
4797
4864
  }
4865
+ this.currentEntry = void 0;
4798
4866
  const stranded = this.promptQueue;
4799
4867
  this.promptQueue = [];
4800
4868
  for (const entry of stranded) {
@@ -5123,6 +5191,9 @@ _(switched from \`${oldAgentId}\` to \`${newAgentId}\`)_
5123
5191
  await new Promise((r) => setImmediate(r));
5124
5192
  try {
5125
5193
  while (this.promptQueue.length > 0) {
5194
+ if (this.closing) {
5195
+ break;
5196
+ }
5126
5197
  const next = this.promptQueue.shift();
5127
5198
  if (!next) {
5128
5199
  break;
@@ -5557,6 +5628,51 @@ function isExitPlanModeTool(name) {
5557
5628
  const normalised = name.toLowerCase().replace(/[_\s-]/g, "");
5558
5629
  return normalised === "exitplanmode";
5559
5630
  }
5631
+ function extractEditDiff(u) {
5632
+ const content = u.content;
5633
+ if (Array.isArray(content)) {
5634
+ for (const block of content) {
5635
+ if (!block || typeof block !== "object") {
5636
+ continue;
5637
+ }
5638
+ const b = block;
5639
+ if (b.type !== "diff") {
5640
+ continue;
5641
+ }
5642
+ const oldText = typeof b.oldText === "string" ? b.oldText : void 0;
5643
+ const newText = typeof b.newText === "string" ? b.newText : void 0;
5644
+ if (oldText === void 0 && newText === void 0) {
5645
+ continue;
5646
+ }
5647
+ const path20 = typeof b.path === "string" ? b.path : void 0;
5648
+ return {
5649
+ ...path20 !== void 0 ? { path: path20 } : {},
5650
+ oldText: oldText ?? "",
5651
+ newText: newText ?? ""
5652
+ };
5653
+ }
5654
+ }
5655
+ const rawInput = u.rawInput;
5656
+ if (rawInput && typeof rawInput === "object" && !Array.isArray(rawInput)) {
5657
+ const r = rawInput;
5658
+ const filePath = typeof r.file_path === "string" ? r.file_path : typeof r.path === "string" ? r.path : void 0;
5659
+ if (typeof r.old_string === "string" && typeof r.new_string === "string") {
5660
+ return {
5661
+ ...filePath !== void 0 ? { path: filePath } : {},
5662
+ oldText: r.old_string,
5663
+ newText: r.new_string
5664
+ };
5665
+ }
5666
+ if (typeof r.content === "string") {
5667
+ return {
5668
+ ...filePath !== void 0 ? { path: filePath } : {},
5669
+ oldText: "",
5670
+ newText: r.content
5671
+ };
5672
+ }
5673
+ }
5674
+ return null;
5675
+ }
5560
5676
  function readExitPlanMarkdown(u) {
5561
5677
  const rawInput = u.rawInput;
5562
5678
  if (!rawInput || typeof rawInput !== "object" || Array.isArray(rawInput)) {
@@ -5596,6 +5712,10 @@ function mapToolCall(u) {
5596
5712
  if (rawKind !== void 0) {
5597
5713
  event.rawKind = rawKind;
5598
5714
  }
5715
+ const diff = extractEditDiff(u);
5716
+ if (diff !== null) {
5717
+ event.editDiff = diff;
5718
+ }
5599
5719
  return event;
5600
5720
  }
5601
5721
  function mapToolCallUpdate(u) {
@@ -5606,7 +5726,8 @@ function mapToolCallUpdate(u) {
5606
5726
  const rawTitle = readString(u, "title");
5607
5727
  const title = rawTitle !== void 0 ? sanitizeSingleLine(rawTitle) : void 0;
5608
5728
  const status = readString(u, "status");
5609
- const meaningful = title !== void 0 || status === "completed" || status === "failed" || status === "rejected" || status === "cancelled";
5729
+ const diff = extractEditDiff(u);
5730
+ const meaningful = title !== void 0 || diff !== null || status === "completed" || status === "failed" || status === "rejected" || status === "cancelled";
5610
5731
  if (!meaningful) {
5611
5732
  return null;
5612
5733
  }
@@ -5629,6 +5750,9 @@ function mapToolCallUpdate(u) {
5629
5750
  if (status !== void 0) {
5630
5751
  event.status = status;
5631
5752
  }
5753
+ if (diff !== null) {
5754
+ event.editDiff = diff;
5755
+ }
5632
5756
  if (status === "failed") {
5633
5757
  const errorText = extractToolFailureText(u);
5634
5758
  if (errorText !== null) {
@@ -5949,10 +6073,37 @@ async function listSessions(target, opts = {}, fetchImpl = fetch) {
5949
6073
  title: s.title,
5950
6074
  importedFromMachine: s.importedFromMachine,
5951
6075
  importedFromUpstreamSessionId: s.importedFromUpstreamSessionId,
6076
+ forkedFromSessionId: s.forkedFromSessionId,
6077
+ forkedFromMessageId: s.forkedFromMessageId,
5952
6078
  busy: s.busy,
5953
6079
  originatingClient: s.originatingClient
5954
6080
  }));
5955
6081
  }
6082
+ async function forkSession(target, id, opts = {}, fetchImpl = fetch) {
6083
+ const response = await fetchImpl(
6084
+ `${target.baseUrl}/v1/sessions/${id}/fork`,
6085
+ {
6086
+ method: "POST",
6087
+ headers: {
6088
+ Authorization: `Bearer ${target.token}`,
6089
+ "Content-Type": "application/json"
6090
+ },
6091
+ body: JSON.stringify(opts)
6092
+ }
6093
+ );
6094
+ if (!response.ok) {
6095
+ let detail = "";
6096
+ try {
6097
+ const body = await response.json();
6098
+ if (typeof body.error === "string") {
6099
+ detail = `: ${body.error}`;
6100
+ }
6101
+ } catch {
6102
+ }
6103
+ throw new Error(`fork failed (HTTP ${response.status})${detail}`);
6104
+ }
6105
+ return await response.json();
6106
+ }
5956
6107
  async function killSession(target, id, fetchImpl = fetch) {
5957
6108
  const response = await fetchImpl(`${target.baseUrl}/v1/sessions/${id}/kill`, {
5958
6109
  method: "POST",
@@ -6584,13 +6735,25 @@ function applyInlineMarkup(text, opts) {
6584
6735
  s = s.replace(/`([^`]+)`/g, `${codeOpen}$1${codeReset}`);
6585
6736
  return s;
6586
6737
  }
6738
+ function headingInlineOptsFor(style) {
6739
+ switch (style) {
6740
+ case "heading-1":
6741
+ return { codeOpen: "^C", boldReset: "^+^Y", codeReset: "^+^Y" };
6742
+ case "heading-2":
6743
+ return { codeOpen: "^Y", boldReset: "^+^C", codeReset: "^+^C" };
6744
+ case "heading-3":
6745
+ default:
6746
+ return { codeOpen: "^C", boldReset: "^:^+", codeReset: "^:^+" };
6747
+ }
6748
+ }
6587
6749
  function parseMarkdown(text, opts) {
6588
6750
  const {
6589
6751
  proseStyle,
6590
6752
  highlightCode,
6591
6753
  prefixStyle,
6592
6754
  firstPrefix = " ",
6593
- inlineOpts
6755
+ inlineOpts,
6756
+ maxWidth
6594
6757
  } = opts;
6595
6758
  const out = [];
6596
6759
  const lines = text.split("\n");
@@ -6657,7 +6820,12 @@ function parseMarkdown(text, opts) {
6657
6820
  const level = heading[1].length;
6658
6821
  const headingText = heading[2] ?? "";
6659
6822
  const headingStyle = highlightCode ? level === 1 ? "heading-1" : level === 2 ? "heading-2" : "heading-3" : proseStyle;
6660
- line(headingText, headingStyle, nextPrefix());
6823
+ const headingInlineOpts = highlightCode ? headingInlineOptsFor(headingStyle) : inlineOpts;
6824
+ line(
6825
+ applyInlineMarkup(headingText, headingInlineOpts),
6826
+ headingStyle,
6827
+ nextPrefix()
6828
+ );
6661
6829
  continue;
6662
6830
  }
6663
6831
  const next = lines[i + 1];
@@ -6669,7 +6837,7 @@ function parseMarkdown(text, opts) {
6669
6837
  body.push(parseTableRow(lines[j]));
6670
6838
  j++;
6671
6839
  }
6672
- const tableLines = formatTable(header, body);
6840
+ const tableLines = formatTable(header, body, maxWidth);
6673
6841
  for (const tl of tableLines) {
6674
6842
  if (prefixStyle !== void 0)
6675
6843
  tl.prefixStyle = prefixStyle;
@@ -6712,8 +6880,12 @@ function parseMarkdown(text, opts) {
6712
6880
  flushCode();
6713
6881
  return out;
6714
6882
  }
6715
- function parseAgentMarkdown(text) {
6716
- return parseMarkdown(text, { proseStyle: "agent", highlightCode: true });
6883
+ function parseAgentMarkdown(text, opts) {
6884
+ return parseMarkdown(text, {
6885
+ proseStyle: "agent",
6886
+ highlightCode: true,
6887
+ maxWidth: opts?.maxWidth
6888
+ });
6717
6889
  }
6718
6890
  function parseThoughtMarkdown(text) {
6719
6891
  return parseMarkdown(text, {
@@ -6744,45 +6916,238 @@ function isTableSeparatorLine(line) {
6744
6916
  }
6745
6917
  return cells.every((c) => /^:?-+:?$/.test(c));
6746
6918
  }
6747
- function cellVisibleWidth(cell, asLiteral) {
6748
- if (asLiteral) {
6749
- return stringWidth(cell);
6750
- }
6919
+ function cellVisibleWidth(cell) {
6751
6920
  const visible = cell.replace(/\*\*(.+?)\*\*/g, "$1").replace(/`([^`]+)`/g, "$1");
6752
6921
  return stringWidth(visible);
6753
6922
  }
6754
- function formatTable(header, body) {
6923
+ function tokenizeCell(cell) {
6924
+ const atoms = [];
6925
+ let i = 0;
6926
+ while (i < cell.length) {
6927
+ const ch = cell[i];
6928
+ if (ch === " " || ch === " ") {
6929
+ let j2 = i;
6930
+ while (j2 < cell.length && (cell[j2] === " " || cell[j2] === " ")) {
6931
+ j2++;
6932
+ }
6933
+ const text = cell.slice(i, j2);
6934
+ atoms.push({ text, isWS: true, width: stringWidth(text) });
6935
+ i = j2;
6936
+ continue;
6937
+ }
6938
+ let word = "";
6939
+ let j = i;
6940
+ while (j < cell.length) {
6941
+ const c = cell[j];
6942
+ if (c === " " || c === " ") {
6943
+ break;
6944
+ }
6945
+ if (cell[j] === "*" && cell[j + 1] === "*") {
6946
+ const close = cell.indexOf("**", j + 2);
6947
+ if (close === -1) {
6948
+ word += "**";
6949
+ j += 2;
6950
+ } else {
6951
+ word += cell.slice(j, close + 2);
6952
+ j = close + 2;
6953
+ }
6954
+ continue;
6955
+ }
6956
+ if (c === "`") {
6957
+ const close = cell.indexOf("`", j + 1);
6958
+ if (close === -1) {
6959
+ word += "`";
6960
+ j += 1;
6961
+ } else {
6962
+ word += cell.slice(j, close + 1);
6963
+ j = close + 1;
6964
+ }
6965
+ continue;
6966
+ }
6967
+ word += c;
6968
+ j += 1;
6969
+ }
6970
+ atoms.push({ text: word, isWS: false, width: cellVisibleWidth(word) });
6971
+ i = j;
6972
+ }
6973
+ return atoms;
6974
+ }
6975
+ function hardBreak(text, width) {
6976
+ const out = [];
6977
+ let current = "";
6978
+ let currentWidth = 0;
6979
+ for (const ch of text) {
6980
+ const w = stringWidth(ch);
6981
+ if (currentWidth > 0 && currentWidth + w > width) {
6982
+ out.push(current);
6983
+ current = ch;
6984
+ currentWidth = w;
6985
+ } else {
6986
+ current += ch;
6987
+ currentWidth += w;
6988
+ }
6989
+ }
6990
+ if (current.length > 0) {
6991
+ out.push(current);
6992
+ }
6993
+ return out;
6994
+ }
6995
+ function wrapCellAtoms(atoms, width) {
6996
+ if (width <= 0) {
6997
+ return atoms.length === 0 ? [""] : [atoms.map((a) => a.text).join("")];
6998
+ }
6999
+ const lines = [];
7000
+ let current = "";
7001
+ let currentWidth = 0;
7002
+ const flush = () => {
7003
+ lines.push(current.replace(/[ \t]+$/, ""));
7004
+ current = "";
7005
+ currentWidth = 0;
7006
+ };
7007
+ for (const atom of atoms) {
7008
+ if (atom.isWS) {
7009
+ if (currentWidth === 0) {
7010
+ continue;
7011
+ }
7012
+ current += atom.text;
7013
+ currentWidth += atom.width;
7014
+ continue;
7015
+ }
7016
+ if (atom.width > width) {
7017
+ if (currentWidth > 0) {
7018
+ flush();
7019
+ }
7020
+ const hasMarkup = atom.text.includes("**") || atom.text.includes("`");
7021
+ if (hasMarkup) {
7022
+ lines.push(atom.text);
7023
+ } else {
7024
+ const fragments = hardBreak(atom.text, width);
7025
+ for (let k = 0; k < fragments.length - 1; k++) {
7026
+ lines.push(fragments[k]);
7027
+ }
7028
+ const last = fragments[fragments.length - 1] ?? "";
7029
+ current = last;
7030
+ currentWidth = stringWidth(last);
7031
+ }
7032
+ continue;
7033
+ }
7034
+ if (currentWidth === 0) {
7035
+ current = atom.text;
7036
+ currentWidth = atom.width;
7037
+ continue;
7038
+ }
7039
+ if (currentWidth + atom.width > width) {
7040
+ flush();
7041
+ current = atom.text;
7042
+ currentWidth = atom.width;
7043
+ } else {
7044
+ current += atom.text;
7045
+ currentWidth += atom.width;
7046
+ }
7047
+ }
7048
+ if (current.length > 0 || lines.length === 0) {
7049
+ flush();
7050
+ }
7051
+ return lines;
7052
+ }
7053
+ function distributeColumnWidths(natural, budget) {
7054
+ const cols = natural.length;
7055
+ const total = natural.reduce((a, b) => a + b, 0);
7056
+ if (total <= budget) {
7057
+ return natural.slice();
7058
+ }
7059
+ const widths = natural.map((n) => Math.min(n, TABLE_MIN_COL));
7060
+ let used = widths.reduce((a, b) => a + b, 0);
7061
+ if (used >= budget) {
7062
+ return widths;
7063
+ }
7064
+ const remaining = budget - used;
7065
+ const shrinkable = natural.map((n, i) => ({ i, slack: Math.max(0, n - widths[i]) })).filter((e) => e.slack > 0);
7066
+ const shrinkableTotal = shrinkable.reduce((a, b) => a + b.slack, 0);
7067
+ if (shrinkableTotal === 0) {
7068
+ return widths;
7069
+ }
7070
+ for (const e of shrinkable) {
7071
+ const add = Math.floor(remaining * e.slack / shrinkableTotal);
7072
+ widths[e.i] = widths[e.i] + Math.min(add, e.slack);
7073
+ }
7074
+ used = widths.reduce((a, b) => a + b, 0);
7075
+ let leftover = budget - used;
7076
+ while (leftover > 0) {
7077
+ let bestIdx = -1;
7078
+ let bestDeficit = 0;
7079
+ for (let i = 0; i < cols; i++) {
7080
+ const deficit = natural[i] - widths[i];
7081
+ if (deficit > bestDeficit) {
7082
+ bestDeficit = deficit;
7083
+ bestIdx = i;
7084
+ }
7085
+ }
7086
+ if (bestIdx < 0) {
7087
+ break;
7088
+ }
7089
+ widths[bestIdx] = widths[bestIdx] + 1;
7090
+ leftover--;
7091
+ }
7092
+ return widths;
7093
+ }
7094
+ function formatTable(header, body, maxWidth) {
6755
7095
  const cols = header.length;
6756
- const widths = new Array(cols).fill(0);
7096
+ const natural = new Array(cols).fill(0);
6757
7097
  for (let c = 0; c < cols; c++) {
6758
- widths[c] = cellVisibleWidth(header[c] ?? "", true);
7098
+ natural[c] = cellVisibleWidth(header[c] ?? "");
6759
7099
  }
6760
7100
  for (const row of body) {
6761
7101
  for (let c = 0; c < cols; c++) {
6762
7102
  const cell = row[c] ?? "";
6763
- const w = cellVisibleWidth(cell, false);
6764
- if (w > widths[c]) {
6765
- widths[c] = w;
7103
+ const w = cellVisibleWidth(cell);
7104
+ if (w > natural[c]) {
7105
+ natural[c] = w;
6766
7106
  }
6767
7107
  }
6768
7108
  }
6769
- const renderRow = (cells, style, applyMarkup) => {
6770
- const padded = [];
7109
+ let widths = natural.slice();
7110
+ if (maxWidth !== void 0) {
7111
+ const budget = Math.max(
7112
+ cols * TABLE_MIN_COL,
7113
+ maxWidth - TABLE_PREFIX_WIDTH - (cols - 1) * TABLE_SEP_WIDTH
7114
+ );
7115
+ widths = distributeColumnWidths(natural, budget);
7116
+ }
7117
+ const renderRow = (cells, style, inlineOpts) => {
7118
+ const wrapped = [];
7119
+ let rowHeight = 1;
6771
7120
  for (let c = 0; c < cols; c++) {
6772
7121
  const cell = cells[c] ?? "";
6773
7122
  const w = widths[c];
6774
- const visible = cellVisibleWidth(cell, !applyMarkup);
6775
- const rendered = applyMarkup ? applyInlineMarkup(cell) : cell;
6776
- padded.push(rendered + " ".repeat(Math.max(0, w - visible)));
7123
+ const lines = wrapCellAtoms(tokenizeCell(cell), w);
7124
+ wrapped.push(lines);
7125
+ if (lines.length > rowHeight) {
7126
+ rowHeight = lines.length;
7127
+ }
7128
+ }
7129
+ const out2 = [];
7130
+ for (let r = 0; r < rowHeight; r++) {
7131
+ const padded = [];
7132
+ for (let c = 0; c < cols; c++) {
7133
+ const cellLine = wrapped[c][r] ?? "";
7134
+ const w = widths[c];
7135
+ const visible = cellVisibleWidth(cellLine);
7136
+ const rendered = applyInlineMarkup(cellLine, inlineOpts);
7137
+ padded.push(rendered + " ".repeat(Math.max(0, w - visible)));
7138
+ }
7139
+ out2.push({
7140
+ prefix: " ",
7141
+ body: padded.join(" \u2502 "),
7142
+ bodyStyle: style
7143
+ });
6777
7144
  }
6778
- return {
6779
- prefix: " ",
6780
- body: padded.join(" \u2502 "),
6781
- bodyStyle: style
6782
- };
7145
+ return out2;
6783
7146
  };
6784
7147
  const out = [];
6785
- out.push(renderRow(header, "heading-3", false));
7148
+ out.push(
7149
+ ...renderRow(header, "heading-3", headingInlineOptsFor("heading-3"))
7150
+ );
6786
7151
  const rules = [];
6787
7152
  for (let c = 0; c < cols; c++) {
6788
7153
  rules.push("\u2500".repeat(widths[c]));
@@ -6793,7 +7158,7 @@ function formatTable(header, body) {
6793
7158
  bodyStyle: "dim"
6794
7159
  });
6795
7160
  for (const row of body) {
6796
- out.push(renderRow(row, "agent", true));
7161
+ out.push(...renderRow(row, "agent"));
6797
7162
  }
6798
7163
  return out;
6799
7164
  }
@@ -6811,6 +7176,7 @@ function highlightFencedBlock(lang, lines) {
6811
7176
  } catch {
6812
7177
  return lines.map((body) => ({ body, ansi: false }));
6813
7178
  }
7179
+ highlighted = highlighted.replace(/\x1b\[39m/g, "\x1B[37m");
6814
7180
  const out = highlighted.split("\n");
6815
7181
  if (out.length !== lines.length) {
6816
7182
  return lines.map((body) => ({ body, ansi: false }));
@@ -6875,6 +7241,97 @@ function formatToolLine2(state) {
6875
7241
  }
6876
7242
  return lines;
6877
7243
  }
7244
+ function formatEditDiffBlock(diff, mode) {
7245
+ const lines = [];
7246
+ if (diff.path) {
7247
+ lines.push({
7248
+ prefix: " ",
7249
+ body: `\u25B8 Edited ${sanitizeSingleLine(shortenHomePath(diff.path))}`,
7250
+ bodyStyle: "dim"
7251
+ });
7252
+ }
7253
+ if (mode === "edit") {
7254
+ return lines;
7255
+ }
7256
+ const body = buildUnifiedDiff(diff);
7257
+ if (body.length === 0) {
7258
+ return lines;
7259
+ }
7260
+ const fenced = "```diff\n" + body + "\n```";
7261
+ lines.push(...parseAgentMarkdown(fenced));
7262
+ return lines;
7263
+ }
7264
+ function buildUnifiedDiff(diff) {
7265
+ const oldLines = sanitizeWireText(diff.oldText).split("\n");
7266
+ const newLines = sanitizeWireText(diff.newText).split("\n");
7267
+ if (oldLines.length > 0 && oldLines[oldLines.length - 1] === "") {
7268
+ oldLines.pop();
7269
+ }
7270
+ if (newLines.length > 0 && newLines[newLines.length - 1] === "") {
7271
+ newLines.pop();
7272
+ }
7273
+ const ops = diffLines(oldLines, newLines);
7274
+ const rendered = [];
7275
+ for (let idx = 0; idx < ops.length; idx++) {
7276
+ const op = ops[idx];
7277
+ const wouldTruncate = rendered.length >= EDIT_DIFF_MAX_LINES - 1 && idx < ops.length - 1;
7278
+ if (wouldTruncate) {
7279
+ const remaining = ops.length - idx;
7280
+ rendered.push(`\u2026 ${remaining} more line${remaining === 1 ? "" : "s"}`);
7281
+ break;
7282
+ }
7283
+ if (op.op === "=") {
7284
+ rendered.push(` ${op.text}`);
7285
+ } else if (op.op === "-") {
7286
+ rendered.push(`- ${op.text}`);
7287
+ } else {
7288
+ rendered.push(`+ ${op.text}`);
7289
+ }
7290
+ }
7291
+ return rendered.join("\n");
7292
+ }
7293
+ function diffLines(a, b) {
7294
+ const m = a.length;
7295
+ const n = b.length;
7296
+ const dp = Array.from(
7297
+ { length: m + 1 },
7298
+ () => new Array(n + 1).fill(0)
7299
+ );
7300
+ for (let i2 = m - 1; i2 >= 0; i2--) {
7301
+ for (let j2 = n - 1; j2 >= 0; j2--) {
7302
+ if (a[i2] === b[j2]) {
7303
+ dp[i2][j2] = dp[i2 + 1][j2 + 1] + 1;
7304
+ } else {
7305
+ dp[i2][j2] = Math.max(dp[i2 + 1][j2], dp[i2][j2 + 1]);
7306
+ }
7307
+ }
7308
+ }
7309
+ const out = [];
7310
+ let i = 0;
7311
+ let j = 0;
7312
+ while (i < m && j < n) {
7313
+ if (a[i] === b[j]) {
7314
+ out.push({ op: "=", text: a[i] });
7315
+ i++;
7316
+ j++;
7317
+ } else if (dp[i + 1][j] >= dp[i][j + 1]) {
7318
+ out.push({ op: "-", text: a[i] });
7319
+ i++;
7320
+ } else {
7321
+ out.push({ op: "+", text: b[j] });
7322
+ j++;
7323
+ }
7324
+ }
7325
+ while (i < m) {
7326
+ out.push({ op: "-", text: a[i] });
7327
+ i++;
7328
+ }
7329
+ while (j < n) {
7330
+ out.push({ op: "+", text: b[j] });
7331
+ j++;
7332
+ }
7333
+ return out;
7334
+ }
6878
7335
  function toolStatusIcon(status) {
6879
7336
  switch (status) {
6880
7337
  case "completed":
@@ -7016,11 +7473,15 @@ function toolStatusStyle(status) {
7016
7473
  return "tool-status-pending";
7017
7474
  }
7018
7475
  }
7019
- var highlightChalk, HIGHLIGHT_THEME;
7476
+ var TABLE_MIN_COL, TABLE_PREFIX_WIDTH, TABLE_SEP_WIDTH, highlightChalk, HIGHLIGHT_THEME, EDIT_DIFF_MAX_LINES;
7020
7477
  var init_format = __esm({
7021
7478
  "src/tui/format.ts"() {
7022
7479
  "use strict";
7480
+ init_paths();
7023
7481
  init_render_update();
7482
+ TABLE_MIN_COL = 6;
7483
+ TABLE_PREFIX_WIDTH = 2;
7484
+ TABLE_SEP_WIDTH = 3;
7024
7485
  highlightChalk = new chalk2.Instance({ level: 3 });
7025
7486
  HIGHLIGHT_THEME = {
7026
7487
  keyword: highlightChalk.blueBright,
@@ -7046,6 +7507,7 @@ var init_format = __esm({
7046
7507
  tag: highlightChalk.cyan,
7047
7508
  name: highlightChalk.cyanBright
7048
7509
  };
7510
+ EDIT_DIFF_MAX_LINES = 40;
7049
7511
  }
7050
7512
  });
7051
7513
 
@@ -7140,10 +7602,17 @@ var init_update_check = __esm({
7140
7602
  });
7141
7603
 
7142
7604
  // src/tui/input.ts
7143
- var InputDispatcher;
7605
+ function formatPasteToken(id, lineCount) {
7606
+ return `[pasted #${id} +${lineCount} lines]`;
7607
+ }
7608
+ var PASTE_LINE_THRESHOLD, PASTE_TOKEN_RE, PASTE_TOKEN_LEFT_RE, PASTE_TOKEN_RIGHT_RE, InputDispatcher;
7144
7609
  var init_input = __esm({
7145
7610
  "src/tui/input.ts"() {
7146
7611
  "use strict";
7612
+ PASTE_LINE_THRESHOLD = 10;
7613
+ PASTE_TOKEN_RE = /\[pasted #(\d+) \+\d+ lines\]/g;
7614
+ PASTE_TOKEN_LEFT_RE = /\[pasted #(\d+) \+\d+ lines\]$/;
7615
+ PASTE_TOKEN_RIGHT_RE = /^\[pasted #(\d+) \+\d+ lines\]/;
7147
7616
  InputDispatcher = class {
7148
7617
  buffer = [""];
7149
7618
  row = 0;
@@ -7185,9 +7654,23 @@ var init_input = __esm({
7185
7654
  // queue slots (which may carry their own attachments — though we
7186
7655
  // don't surface that yet) shouldn't blend with the current draft's.
7187
7656
  savedAttachments = null;
7657
+ // Map of paste id → original text for placeholder tokens currently in
7658
+ // the buffer (or recoverable via history walks within this session).
7659
+ // Persists across sends — never cleared by clearBuffer/setBuffer, so
7660
+ // up-arrow recall of a placeholder can still reanimate on resubmit.
7661
+ pastes = /* @__PURE__ */ new Map();
7662
+ nextPasteId = 1;
7663
+ collapsePastes;
7188
7664
  constructor(opts = {}) {
7189
7665
  this.history = [...opts.history ?? []];
7190
7666
  this.planMode = opts.planMode ?? false;
7667
+ this.collapsePastes = opts.collapsePastes ?? true;
7668
+ }
7669
+ // Buffer text with paste placeholders expanded back to their original
7670
+ // content. Used by callers that bypass the send/amend effects (e.g.
7671
+ // picker.ts reads composer text directly).
7672
+ expandedText() {
7673
+ return this.expandPastes(this.bufferText());
7191
7674
  }
7192
7675
  state() {
7193
7676
  return {
@@ -7296,7 +7779,14 @@ var init_input = __esm({
7296
7779
  return [];
7297
7780
  }
7298
7781
  if (event.type === "paste") {
7299
- this.insertText(event.text);
7782
+ const lineCount = event.text.split("\n").length;
7783
+ if (this.collapsePastes && lineCount > PASTE_LINE_THRESHOLD) {
7784
+ const id = this.nextPasteId++;
7785
+ this.pastes.set(id, event.text);
7786
+ this.insertText(formatPasteToken(id, lineCount));
7787
+ } else {
7788
+ this.insertText(event.text);
7789
+ }
7300
7790
  return [];
7301
7791
  }
7302
7792
  if (event.type === "attachment-paths") {
@@ -7420,6 +7910,16 @@ var init_input = __esm({
7420
7910
  bufferText() {
7421
7911
  return this.buffer.join("\n");
7422
7912
  }
7913
+ // Substitute every [pasted #N +M lines] token with its stored original
7914
+ // text. Unknown ids (orphaned placeholders from outside this process)
7915
+ // are left as the literal token string — the safe fallback.
7916
+ expandPastes(text) {
7917
+ return text.replace(PASTE_TOKEN_RE, (match, idStr) => {
7918
+ const id = parseInt(idStr, 10);
7919
+ const stored = this.pastes.get(id);
7920
+ return stored !== void 0 ? stored : match;
7921
+ });
7922
+ }
7423
7923
  bufferIsEmpty() {
7424
7924
  return this.buffer.length === 1 && this.buffer[0] === "";
7425
7925
  }
@@ -7476,6 +7976,16 @@ var init_input = __esm({
7476
7976
  backspace() {
7477
7977
  if (this.col > 0) {
7478
7978
  const line = this.currentLine();
7979
+ const before = line.slice(0, this.col);
7980
+ const m = before.match(PASTE_TOKEN_LEFT_RE);
7981
+ if (m !== null) {
7982
+ this.pastes.delete(parseInt(m[1], 10));
7983
+ this.setCurrentLine(
7984
+ line.slice(0, this.col - m[0].length) + line.slice(this.col)
7985
+ );
7986
+ this.col -= m[0].length;
7987
+ return;
7988
+ }
7479
7989
  this.setCurrentLine(line.slice(0, this.col - 1) + line.slice(this.col));
7480
7990
  this.col -= 1;
7481
7991
  return;
@@ -7493,6 +8003,15 @@ var init_input = __esm({
7493
8003
  deleteForward() {
7494
8004
  const line = this.currentLine();
7495
8005
  if (this.col < line.length) {
8006
+ const after = line.slice(this.col);
8007
+ const m = after.match(PASTE_TOKEN_RIGHT_RE);
8008
+ if (m !== null) {
8009
+ this.pastes.delete(parseInt(m[1], 10));
8010
+ this.setCurrentLine(
8011
+ line.slice(0, this.col) + line.slice(this.col + m[0].length)
8012
+ );
8013
+ return;
8014
+ }
7496
8015
  this.setCurrentLine(line.slice(0, this.col) + line.slice(this.col + 1));
7497
8016
  return;
7498
8017
  }
@@ -7567,6 +8086,15 @@ var init_input = __esm({
7567
8086
  this.backspace();
7568
8087
  return;
7569
8088
  }
8089
+ const before = line.slice(0, this.col);
8090
+ const m = before.match(PASTE_TOKEN_LEFT_RE);
8091
+ if (m !== null) {
8092
+ this.killBuffer = m[0];
8093
+ const i2 = this.col - m[0].length;
8094
+ this.setCurrentLine(line.slice(0, i2) + line.slice(this.col));
8095
+ this.col = i2;
8096
+ return;
8097
+ }
7570
8098
  let i = this.col;
7571
8099
  while (i > 0 && /\s/.test(line[i - 1] ?? "")) {
7572
8100
  i -= 1;
@@ -7589,6 +8117,12 @@ var init_input = __esm({
7589
8117
  }
7590
8118
  moveLeft() {
7591
8119
  if (this.col > 0) {
8120
+ const before = this.currentLine().slice(0, this.col);
8121
+ const m = before.match(PASTE_TOKEN_LEFT_RE);
8122
+ if (m !== null) {
8123
+ this.col -= m[0].length;
8124
+ return;
8125
+ }
7592
8126
  this.col -= 1;
7593
8127
  return;
7594
8128
  }
@@ -7598,7 +8132,14 @@ var init_input = __esm({
7598
8132
  }
7599
8133
  }
7600
8134
  moveRight() {
7601
- if (this.col < this.currentLine().length) {
8135
+ const line = this.currentLine();
8136
+ if (this.col < line.length) {
8137
+ const after = line.slice(this.col);
8138
+ const m = after.match(PASTE_TOKEN_RIGHT_RE);
8139
+ if (m !== null) {
8140
+ this.col += m[0].length;
8141
+ return;
8142
+ }
7602
8143
  this.col += 1;
7603
8144
  return;
7604
8145
  }
@@ -7621,6 +8162,12 @@ var init_input = __esm({
7621
8162
  while (i > 0 && /\s/.test(line[i - 1] ?? "")) {
7622
8163
  i -= 1;
7623
8164
  }
8165
+ const before = line.slice(0, i);
8166
+ const m = before.match(PASTE_TOKEN_LEFT_RE);
8167
+ if (m !== null) {
8168
+ this.col = i - m[0].length;
8169
+ return;
8170
+ }
7624
8171
  while (i > 0 && !/\s/.test(line[i - 1] ?? "")) {
7625
8172
  i -= 1;
7626
8173
  }
@@ -7640,6 +8187,12 @@ var init_input = __esm({
7640
8187
  while (i < line.length && /\s/.test(line[i] ?? "")) {
7641
8188
  i += 1;
7642
8189
  }
8190
+ const after = line.slice(i);
8191
+ const m = after.match(PASTE_TOKEN_RIGHT_RE);
8192
+ if (m !== null) {
8193
+ this.col = i + m[0].length;
8194
+ return;
8195
+ }
7643
8196
  while (i < line.length && !/\s/.test(line[i] ?? "")) {
7644
8197
  i += 1;
7645
8198
  }
@@ -7890,7 +8443,8 @@ var init_input = __esm({
7890
8443
  this.col = (this.buffer[this.row] ?? "").length;
7891
8444
  }
7892
8445
  send() {
7893
- const text = this.bufferText();
8446
+ const displayText = this.bufferText();
8447
+ const text = this.expandPastes(displayText);
7894
8448
  if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
7895
8449
  const index = this.queueIndex;
7896
8450
  const attachments2 = [...this.attachments];
@@ -7898,7 +8452,7 @@ var init_input = __esm({
7898
8452
  if (text.trim().length === 0) {
7899
8453
  return [{ type: "queue-remove", index }];
7900
8454
  }
7901
- return [{ type: "queue-edit", index, text, attachments: attachments2 }];
8455
+ return [{ type: "queue-edit", index, text, displayText, attachments: attachments2 }];
7902
8456
  }
7903
8457
  if (text.trim().length === 0 && this.attachments.length === 0) {
7904
8458
  return [];
@@ -7906,7 +8460,7 @@ var init_input = __esm({
7906
8460
  const planMode = this.planMode;
7907
8461
  const attachments = [...this.attachments];
7908
8462
  this.clearBuffer();
7909
- return [{ type: "send", text, planMode, attachments }];
8463
+ return [{ type: "send", text, displayText, planMode, attachments }];
7910
8464
  }
7911
8465
  // Shift+Enter (also Ctrl+Enter / ^S): amend the in-flight turn.
7912
8466
  // While editing a queued slot, this is the "drop and amend" chord:
@@ -7918,7 +8472,8 @@ var init_input = __esm({
7918
8472
  // whether to route the amend through amend_prompt or fall through to
7919
8473
  // a regular send when no turn is in flight.
7920
8474
  amend() {
7921
- const text = this.bufferText();
8475
+ const displayText = this.bufferText();
8476
+ const text = this.expandPastes(displayText);
7922
8477
  if (this.queueIndex >= 0 && this.queueIndex < this.queue.length) {
7923
8478
  const index = this.queueIndex;
7924
8479
  const planMode2 = this.planMode;
@@ -7930,7 +8485,7 @@ var init_input = __esm({
7930
8485
  }
7931
8486
  return [
7932
8487
  { type: "queue-remove", index },
7933
- { type: "amend", text, planMode: planMode2, attachments: attachments2 }
8488
+ { type: "amend", text, displayText, planMode: planMode2, attachments: attachments2 }
7934
8489
  ];
7935
8490
  }
7936
8491
  if (text.trim().length === 0 && this.attachments.length === 0) {
@@ -7939,7 +8494,7 @@ var init_input = __esm({
7939
8494
  const planMode = this.planMode;
7940
8495
  const attachments = [...this.attachments];
7941
8496
  this.clearBuffer();
7942
- return [{ type: "amend", text, planMode, attachments }];
8497
+ return [{ type: "amend", text, displayText, planMode, attachments }];
7943
8498
  }
7944
8499
  // Home: jump to the very start of the prompt buffer. If we're already
7945
8500
  // there, fall through to scrolling the scrollback to its top.
@@ -8248,6 +8803,9 @@ function writeBodyWithHighlight(termObj, text, style, term, activeCol = null, _a
8248
8803
  i = next + term.length;
8249
8804
  }
8250
8805
  }
8806
+ function bodyStyleUsesMarkup(style) {
8807
+ return style === "agent" || style === "heading-1" || style === "heading-2" || style === "heading-3";
8808
+ }
8251
8809
  function writeStyled(term, text, style) {
8252
8810
  if (text.length === 0) {
8253
8811
  return;
@@ -8299,16 +8857,16 @@ function writeStyled(term, text, style) {
8299
8857
  term.dim.noFormat(text);
8300
8858
  return;
8301
8859
  case "code":
8302
- term.bgColorGrayscale.brightCyan.noFormat(28, text);
8860
+ term.bgColorGrayscale.white.noFormat(28, text);
8303
8861
  return;
8304
8862
  case "heading-1":
8305
- term.bold.brightYellow.noFormat(text);
8863
+ term.bold.brightYellow(text);
8306
8864
  return;
8307
8865
  case "heading-2":
8308
- term.bold.brightCyan.noFormat(text);
8866
+ term.bold.brightCyan(text);
8309
8867
  return;
8310
8868
  case "heading-3":
8311
- term.bold.noFormat(text);
8869
+ term.bold(text);
8312
8870
  return;
8313
8871
  case "search-highlight":
8314
8872
  term.bgBrightYellow.black.noFormat(text);
@@ -9343,6 +9901,14 @@ uncaught: ${err.stack ?? err.message}
9343
9901
  this.pasteActive = true;
9344
9902
  }
9345
9903
  }
9904
+ // Current terminal column count. Markdown rendering (parseAgentMarkdown,
9905
+ // tables in particular) consults this so a too-wide block lays out
9906
+ // narrowly enough that the screen-layer wrap is a no-op. Returns 0 if the
9907
+ // terminal hasn't reported a width yet, in which case callers should fall
9908
+ // back to natural-width formatting.
9909
+ width() {
9910
+ return this.term.width || 0;
9911
+ }
9346
9912
  appendLines(lines) {
9347
9913
  if (lines.length === 0) {
9348
9914
  return;
@@ -10880,7 +11446,7 @@ uncaught: ${err.stack ?? err.message}
10880
11446
  }
10881
11447
  const prefix = line.prefix ?? "";
10882
11448
  const room = Math.max(1, width - prefix.length);
10883
- const stripMarkup = line.bodyStyle === "agent";
11449
+ const stripMarkup = bodyStyleUsesMarkup(line.bodyStyle);
10884
11450
  const chunks = line.ansi ? wrapAnsiBody(line.body, room) : wrap(line.body, room, { stripMarkup });
10885
11451
  const wrapped = [];
10886
11452
  let scanPos = 0;
@@ -10926,7 +11492,7 @@ uncaught: ${err.stack ?? err.message}
10926
11492
  writeStyled(this.term, line.prefix, line.prefixStyle ?? line.bodyStyle);
10927
11493
  }
10928
11494
  const remaining = Math.max(0, width - (line.prefix?.length ?? 0));
10929
- const stripMarkup = line.bodyStyle === "agent";
11495
+ const stripMarkup = bodyStyleUsesMarkup(line.bodyStyle);
10930
11496
  const bodyText = line.ansi ? line.body : truncate(line.body, remaining, { stripMarkup });
10931
11497
  if (this.scrollbackHighlight !== null && !line.ansi) {
10932
11498
  writeBodyWithHighlight(
@@ -11466,7 +12032,10 @@ async function pickSession(term, opts) {
11466
12032
  let mode = "normal";
11467
12033
  let pendingAction = null;
11468
12034
  let findSubMode = "input";
11469
- let findComposer = new InputDispatcher({ history: [] });
12035
+ let findComposer = new InputDispatcher({
12036
+ history: [],
12037
+ collapsePastes: false
12038
+ });
11470
12039
  let findResults = [];
11471
12040
  let findTruncated = false;
11472
12041
  let findSelectedIdx = 0;
@@ -12214,7 +12783,10 @@ async function pickSession(term, opts) {
12214
12783
  };
12215
12784
  const focus = { push: pushLayer, pop: popLayer };
12216
12785
  exitFind = () => {
12217
- findComposer = new InputDispatcher({ history: [] });
12786
+ findComposer = new InputDispatcher({
12787
+ history: [],
12788
+ collapsePastes: false
12789
+ });
12218
12790
  findResults = [];
12219
12791
  findTruncated = false;
12220
12792
  findSelectedIdx = 0;
@@ -12699,7 +13271,7 @@ ${cells}`;
12699
13271
  }
12700
13272
  if (name === "ENTER" || name === "KP_ENTER") {
12701
13273
  cleanup();
12702
- const text = composer.state().buffer.join("\n");
13274
+ const text = composer.expandedText();
12703
13275
  if (text.trim().length === 0) {
12704
13276
  resolve7({ kind: "new" });
12705
13277
  } else {
@@ -12867,12 +13439,35 @@ ${cells}`;
12867
13439
  resolve7(result);
12868
13440
  return;
12869
13441
  }
12870
- if ((name === "k" || name === "K") && selectedIdx > 0) {
13442
+ if ((name === "f" || name === "F") && selectedIdx > 0) {
12871
13443
  const session = visible[selectedIdx - 1];
12872
13444
  if (!session) {
12873
13445
  return;
12874
13446
  }
12875
- pendingAction = {
13447
+ cleanup();
13448
+ const result = {
13449
+ kind: "fork",
13450
+ sourceSessionId: session.sessionId,
13451
+ sourceCwd: session.cwd
13452
+ };
13453
+ if (session.agentId !== void 0) {
13454
+ result.sourceAgentId = session.agentId;
13455
+ }
13456
+ if (session.importedFromMachine !== void 0) {
13457
+ result.sourceImportedFromMachine = session.importedFromMachine;
13458
+ }
13459
+ if (session.upstreamSessionId !== void 0) {
13460
+ result.sourceUpstreamSessionId = session.upstreamSessionId;
13461
+ }
13462
+ resolve7(result);
13463
+ return;
13464
+ }
13465
+ if ((name === "k" || name === "K") && selectedIdx > 0) {
13466
+ const session = visible[selectedIdx - 1];
13467
+ if (!session) {
13468
+ return;
13469
+ }
13470
+ pendingAction = {
12876
13471
  sessionId: session.sessionId,
12877
13472
  cwd: session.cwd,
12878
13473
  status: session.status
@@ -13111,7 +13706,7 @@ var init_picker = __esm({
13111
13706
  });
13112
13707
 
13113
13708
  // src/core/cwd.ts
13114
- import * as fs20 from "fs/promises";
13709
+ import * as fs19 from "fs/promises";
13115
13710
  import * as path17 from "path";
13116
13711
  async function validateLocalCwd(input) {
13117
13712
  const trimmed = input.trim();
@@ -13121,7 +13716,7 @@ async function validateLocalCwd(input) {
13121
13716
  const resolved = path17.resolve(expandHome(trimmed));
13122
13717
  let stat5;
13123
13718
  try {
13124
- stat5 = await fs20.stat(resolved);
13719
+ stat5 = await fs19.stat(resolved);
13125
13720
  } catch {
13126
13721
  return { ok: false, reason: `${resolved} does not exist` };
13127
13722
  }
@@ -13130,6 +13725,58 @@ async function validateLocalCwd(input) {
13130
13725
  }
13131
13726
  return { ok: true, path: resolved };
13132
13727
  }
13728
+ async function pickInitialLocalCwd(sessionCwd) {
13729
+ const candidates = [];
13730
+ const seen = /* @__PURE__ */ new Set();
13731
+ const push = (p) => {
13732
+ if (!seen.has(p)) {
13733
+ seen.add(p);
13734
+ candidates.push(p);
13735
+ }
13736
+ };
13737
+ push(sessionCwd);
13738
+ if (sessionCwd.startsWith("/Users/")) {
13739
+ push("/home/" + sessionCwd.slice("/Users/".length));
13740
+ } else if (sessionCwd.startsWith("/home/")) {
13741
+ push("/Users/" + sessionCwd.slice("/home/".length));
13742
+ }
13743
+ for (const candidate of candidates) {
13744
+ try {
13745
+ const stat5 = await fs19.stat(candidate);
13746
+ if (stat5.isDirectory()) {
13747
+ return candidate;
13748
+ }
13749
+ } catch {
13750
+ }
13751
+ }
13752
+ return null;
13753
+ }
13754
+ async function completeLocalPath(input) {
13755
+ const lastSlash = input.lastIndexOf("/");
13756
+ let prefix;
13757
+ let basePrefix;
13758
+ let dirForRead;
13759
+ if (lastSlash === -1) {
13760
+ prefix = "";
13761
+ basePrefix = input;
13762
+ dirForRead = ".";
13763
+ } else {
13764
+ prefix = input.slice(0, lastSlash + 1);
13765
+ basePrefix = input.slice(lastSlash + 1);
13766
+ dirForRead = lastSlash === 0 ? "/" : prefix;
13767
+ }
13768
+ const resolvedDir = path17.resolve(expandHome(dirForRead));
13769
+ let entries;
13770
+ try {
13771
+ const list = await fs19.readdir(resolvedDir, { withFileTypes: true });
13772
+ entries = list.map((e) => ({ name: e.name, isDir: e.isDirectory() }));
13773
+ } catch {
13774
+ return { prefix, basePrefix, matches: [] };
13775
+ }
13776
+ const showHidden = basePrefix.startsWith(".");
13777
+ const matches = entries.filter((e) => e.name.startsWith(basePrefix)).filter((e) => showHidden || !e.name.startsWith(".")).map((e) => e.isDir ? `${e.name}/` : e.name).sort();
13778
+ return { prefix, basePrefix, matches };
13779
+ }
13133
13780
  var init_cwd = __esm({
13134
13781
  "src/core/cwd.ts"() {
13135
13782
  "use strict";
@@ -13137,10 +13784,54 @@ var init_cwd = __esm({
13137
13784
  }
13138
13785
  });
13139
13786
 
13787
+ // src/tui/completion.ts
13788
+ function longestCommonPrefix(names) {
13789
+ if (names.length === 0) {
13790
+ return "";
13791
+ }
13792
+ let prefix = names[0] ?? "";
13793
+ for (let i = 1; i < names.length; i++) {
13794
+ const n = names[i] ?? "";
13795
+ let j = 0;
13796
+ while (j < prefix.length && j < n.length && prefix[j] === n[j]) {
13797
+ j += 1;
13798
+ }
13799
+ prefix = prefix.slice(0, j);
13800
+ if (prefix.length === 0) {
13801
+ break;
13802
+ }
13803
+ }
13804
+ return prefix;
13805
+ }
13806
+ function computeTabCompletion(args) {
13807
+ const { matches, firstLine: firstLine3 } = args;
13808
+ if (matches.length === 0) {
13809
+ return null;
13810
+ }
13811
+ const space = firstLine3.indexOf(" ");
13812
+ const typedPrefix = space === -1 ? firstLine3 : firstLine3.slice(0, space);
13813
+ const tail = space === -1 ? "" : firstLine3.slice(space);
13814
+ if (matches.length === 1) {
13815
+ const name = matches[0] ?? "";
13816
+ const suffix = tail.startsWith(" ") ? "" : " ";
13817
+ return name + suffix + tail;
13818
+ }
13819
+ const commonPrefix = longestCommonPrefix(matches);
13820
+ if (commonPrefix.length <= typedPrefix.length) {
13821
+ return null;
13822
+ }
13823
+ return commonPrefix + tail;
13824
+ }
13825
+ var init_completion = __esm({
13826
+ "src/tui/completion.ts"() {
13827
+ "use strict";
13828
+ }
13829
+ });
13830
+
13140
13831
  // src/tui/import-cwd-prompt.ts
13141
13832
  import * as os6 from "os";
13142
13833
  async function promptForImportCwd(term, session, opts = {}) {
13143
- const defaultCwd = opts.defaultCwd ?? os6.homedir();
13834
+ const defaultCwd = opts.defaultCwd ?? await pickInitialLocalCwd(session.cwd) ?? os6.homedir();
13144
13835
  resetTerminalModes();
13145
13836
  const shortId2 = stripHydraSessionPrefix(session.sessionId);
13146
13837
  const fromMachine = session.importedFromMachine ?? "another machine";
@@ -13180,11 +13871,11 @@ async function promptForImportCwd(term, session, opts = {}) {
13180
13871
  } else {
13181
13872
  term.moveTo(layout.contentX, layout.contentY + row);
13182
13873
  term.dim.noFormat(
13183
- " Enter accept \xB7 Esc back \xB7 ^U clear \xB7 ^W kill word"
13874
+ " Enter accept \xB7 Tab complete \xB7 Esc back \xB7 ^U clear"
13184
13875
  );
13185
13876
  }
13186
13877
  };
13187
- const inputRow = () => 7;
13878
+ const inputRow = () => 6;
13188
13879
  const paintInputRow = (rowOffset) => {
13189
13880
  if (!layout) {
13190
13881
  return;
@@ -13215,7 +13906,7 @@ async function promptForImportCwd(term, session, opts = {}) {
13215
13906
  term.red.noFormat(` ${truncate3(errorLine, layout.contentW - 2)}`);
13216
13907
  } else {
13217
13908
  term.dim.noFormat(
13218
- " Enter accept \xB7 Esc back \xB7 ^U clear \xB7 ^W kill word"
13909
+ " Enter accept \xB7 Tab complete \xB7 Esc back \xB7 ^U clear"
13219
13910
  );
13220
13911
  }
13221
13912
  };
@@ -13271,6 +13962,32 @@ async function promptForImportCwd(term, session, opts = {}) {
13271
13962
  finish({ kind: "cancel" });
13272
13963
  return;
13273
13964
  }
13965
+ if (name === "TAB") {
13966
+ busy = true;
13967
+ void completeLocalPath(buffer).then((result) => {
13968
+ busy = false;
13969
+ if (result.matches.length === 0) {
13970
+ return;
13971
+ }
13972
+ let next;
13973
+ if (result.matches.length === 1) {
13974
+ next = result.prefix + result.matches[0];
13975
+ } else {
13976
+ const lcp = longestCommonPrefix(result.matches);
13977
+ if (lcp.length <= result.basePrefix.length) {
13978
+ return;
13979
+ }
13980
+ next = result.prefix + lcp;
13981
+ }
13982
+ if (next === buffer) {
13983
+ return;
13984
+ }
13985
+ buffer = next;
13986
+ errorLine = null;
13987
+ repaintInput();
13988
+ });
13989
+ return;
13990
+ }
13274
13991
  if (name === "BACKSPACE") {
13275
13992
  if (buffer.length > 0) {
13276
13993
  buffer = buffer.slice(0, -1);
@@ -13332,13 +14049,14 @@ var init_import_cwd_prompt = __esm({
13332
14049
  init_paths();
13333
14050
  init_session();
13334
14051
  init_cwd();
14052
+ init_completion();
13335
14053
  init_prompt_utils();
13336
14054
  }
13337
14055
  });
13338
14056
 
13339
14057
  // src/tui/clipboard.ts
13340
14058
  import { spawn as nodeSpawn } from "child_process";
13341
- import fs21 from "fs/promises";
14059
+ import fs20 from "fs/promises";
13342
14060
  import os7 from "os";
13343
14061
  import path18 from "path";
13344
14062
  async function readClipboard(envIn = {}) {
@@ -13379,7 +14097,7 @@ async function readMacOS(env) {
13379
14097
  return img;
13380
14098
  }
13381
14099
  } catch {
13382
- await fs21.unlink(tmpPath).catch(() => void 0);
14100
+ await fs20.unlink(tmpPath).catch(() => void 0);
13383
14101
  }
13384
14102
  try {
13385
14103
  const buf = await runCapture(env.spawn, "pbpaste", []);
@@ -13494,9 +14212,9 @@ async function which(env, cmd) {
13494
14212
  }
13495
14213
  async function readFileAsAttachment(p, unlinkAfter) {
13496
14214
  try {
13497
- const buf = await fs21.readFile(p);
14215
+ const buf = await fs20.readFile(p);
13498
14216
  if (unlinkAfter) {
13499
- await fs21.unlink(p).catch(() => void 0);
14217
+ await fs20.unlink(p).catch(() => void 0);
13500
14218
  }
13501
14219
  if (buf.length === 0) {
13502
14220
  return { ok: false, reason: "no image on clipboard" };
@@ -13595,50 +14313,6 @@ var init_clipboard = __esm({
13595
14313
  }
13596
14314
  });
13597
14315
 
13598
- // src/tui/completion.ts
13599
- function longestCommonPrefix(names) {
13600
- if (names.length === 0) {
13601
- return "";
13602
- }
13603
- let prefix = names[0] ?? "";
13604
- for (let i = 1; i < names.length; i++) {
13605
- const n = names[i] ?? "";
13606
- let j = 0;
13607
- while (j < prefix.length && j < n.length && prefix[j] === n[j]) {
13608
- j += 1;
13609
- }
13610
- prefix = prefix.slice(0, j);
13611
- if (prefix.length === 0) {
13612
- break;
13613
- }
13614
- }
13615
- return prefix;
13616
- }
13617
- function computeTabCompletion(args) {
13618
- const { matches, firstLine: firstLine3 } = args;
13619
- if (matches.length === 0) {
13620
- return null;
13621
- }
13622
- const space = firstLine3.indexOf(" ");
13623
- const typedPrefix = space === -1 ? firstLine3 : firstLine3.slice(0, space);
13624
- const tail = space === -1 ? "" : firstLine3.slice(space);
13625
- if (matches.length === 1) {
13626
- const name = matches[0] ?? "";
13627
- const suffix = tail.startsWith(" ") ? "" : " ";
13628
- return name + suffix + tail;
13629
- }
13630
- const commonPrefix = longestCommonPrefix(matches);
13631
- if (commonPrefix.length <= typedPrefix.length) {
13632
- return null;
13633
- }
13634
- return commonPrefix + tail;
13635
- }
13636
- var init_completion = __esm({
13637
- "src/tui/completion.ts"() {
13638
- "use strict";
13639
- }
13640
- });
13641
-
13642
14316
  // src/tui/reconnect-state.ts
13643
14317
  function parseReattachResponse(result) {
13644
14318
  const out = {};
@@ -13664,6 +14338,21 @@ function parseReattachResponse(result) {
13664
14338
  }
13665
14339
  return out;
13666
14340
  }
14341
+ function shouldDriftSnap(args) {
14342
+ return !args.replayDraining && args.pendingTurns > 0 && args.queueSize === 0 && !args.ownTurnInFlight && !args.hasInFlightHead;
14343
+ }
14344
+ function computeAttachReconcile(args) {
14345
+ if (args.daemonTurnStartedAt !== void 0) {
14346
+ const delta2 = args.pendingTurns === 0 ? 1 : 0;
14347
+ return {
14348
+ pendingTurnsDelta: delta2,
14349
+ banner: "busy",
14350
+ busySince: args.daemonTurnStartedAt
14351
+ };
14352
+ }
14353
+ const delta = args.pendingTurns > 0 ? -args.pendingTurns : 0;
14354
+ return { pendingTurnsDelta: delta, banner: "ready" };
14355
+ }
13667
14356
  var init_reconnect_state = __esm({
13668
14357
  "src/tui/reconnect-state.ts"() {
13669
14358
  "use strict";
@@ -13674,7 +14363,7 @@ var init_reconnect_state = __esm({
13674
14363
  import { appendFileSync, statSync, renameSync } from "fs";
13675
14364
  import { nanoid as nanoid3 } from "nanoid";
13676
14365
  import termkit from "terminal-kit";
13677
- import fs22 from "fs/promises";
14366
+ import fs21 from "fs/promises";
13678
14367
  import path19 from "path";
13679
14368
  function isReadonlyForbiddenEffect(effect) {
13680
14369
  switch (effect.type) {
@@ -13800,6 +14489,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
13800
14489
  let bufferedEvents = [];
13801
14490
  let applyRenderEvent = null;
13802
14491
  let teardownStarted = false;
14492
+ let replayDraining = false;
13803
14493
  const appendRender = (event) => {
13804
14494
  if (!event) {
13805
14495
  return;
@@ -14002,7 +14692,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14002
14692
  echo.flushed = true;
14003
14693
  appendRender({
14004
14694
  kind: "user-text",
14005
- text: echo.text,
14695
+ text: echo.displayText,
14006
14696
  attachments: echo.attachments
14007
14697
  });
14008
14698
  currentTurnEcho = echo;
@@ -14266,23 +14956,26 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14266
14956
  if (globalHistory.length > config.tui.promptHistoryMaxEntries) {
14267
14957
  globalHistory = globalHistory.slice(globalHistory.length - config.tui.promptHistoryMaxEntries);
14268
14958
  }
14959
+ let displayHistory = [...history];
14269
14960
  const dispatcher = new InputDispatcher({
14270
- history: buildCombinedHistory(globalHistory, history)
14961
+ history: buildCombinedHistory(globalHistory, displayHistory)
14271
14962
  });
14272
14963
  dispatcherRef = dispatcher;
14273
14964
  let livePeerHistoryRecording = false;
14274
- const recordHistoryEntry = (entry) => {
14965
+ const recordHistoryEntry = (entry, displayEntry) => {
14275
14966
  const trimmed = entry.replace(/\n+$/, "");
14276
14967
  if (trimmed.length === 0) {
14277
14968
  return;
14278
14969
  }
14970
+ const trimmedDisplay = (displayEntry ?? entry).replace(/\n+$/, "");
14279
14971
  const nextSession = appendEntry(history, trimmed);
14280
14972
  const sessionChanged = nextSession !== history;
14281
14973
  history = nextSession;
14974
+ displayHistory = appendEntry(displayHistory, trimmedDisplay);
14282
14975
  const nextGlobal = appendEntry(globalHistory, trimmed, config.tui.promptHistoryMaxEntries);
14283
14976
  const globalChanged = nextGlobal !== globalHistory;
14284
14977
  globalHistory = nextGlobal;
14285
- dispatcher.setHistory(buildCombinedHistory(globalHistory, history));
14978
+ dispatcher.setHistory(buildCombinedHistory(globalHistory, displayHistory));
14286
14979
  if (sessionChanged) {
14287
14980
  saveHistory(historyFile, history).catch(() => void 0);
14288
14981
  }
@@ -14692,6 +15385,31 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14692
15385
  resolvedChoice = { choice: choice2, sessions };
14693
15386
  break;
14694
15387
  }
15388
+ if (choice2.kind === "fork") {
15389
+ const decided2 = await runForkFlow(term, target, choice2, sessions);
15390
+ if (decided2.kind === "cancel") {
15391
+ screen.start({ skipFullscreen: true });
15392
+ screen.resumeRepaint();
15393
+ return;
15394
+ }
15395
+ if (decided2.kind === "back") {
15396
+ continue;
15397
+ }
15398
+ const synthetic = {
15399
+ kind: "attach",
15400
+ sessionId: decided2.ctx.sessionId,
15401
+ ...decided2.ctx.agentId ? { agentId: decided2.ctx.agentId } : {}
15402
+ };
15403
+ resolvedChoice = { choice: synthetic, sessions };
15404
+ attachOverrides = {
15405
+ readonly: false,
15406
+ cwd: decided2.ctx.cwd
15407
+ };
15408
+ if (decided2.ctx.importAttachHint !== void 0) {
15409
+ attachOverrides.importAttachHint = decided2.ctx.importAttachHint;
15410
+ }
15411
+ break;
15412
+ }
14695
15413
  const chosen = sessions.find((s) => s.sessionId === choice2.sessionId);
14696
15414
  const isImportedFirstLaunch = chosen !== void 0 && !!chosen.importedFromMachine && !chosen.upstreamSessionId && choice2.readonly !== true;
14697
15415
  if (!isImportedFirstLaunch) {
@@ -14783,16 +15501,16 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14783
15501
  switch (effect.type) {
14784
15502
  case "send":
14785
15503
  if (config.tui.defaultEnterAction === "amend") {
14786
- amendPrompt(effect.text, effect.attachments);
15504
+ amendPrompt(effect.text, effect.attachments, effect.displayText);
14787
15505
  } else {
14788
- enqueuePrompt(effect.text, effect.attachments);
15506
+ enqueuePrompt(effect.text, effect.attachments, effect.displayText);
14789
15507
  }
14790
15508
  return;
14791
15509
  case "amend":
14792
15510
  if (config.tui.defaultEnterAction === "amend") {
14793
- enqueuePrompt(effect.text, effect.attachments);
15511
+ enqueuePrompt(effect.text, effect.attachments, effect.displayText);
14794
15512
  } else {
14795
- amendPrompt(effect.text, effect.attachments);
15513
+ amendPrompt(effect.text, effect.attachments, effect.displayText);
14796
15514
  }
14797
15515
  return;
14798
15516
  case "queue-edit": {
@@ -14946,7 +15664,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
14946
15664
  continue;
14947
15665
  }
14948
15666
  try {
14949
- const buf = await fs22.readFile(token);
15667
+ const buf = await fs21.readFile(token);
14950
15668
  if (buf.length > MAX_ATTACHMENT_BYTES) {
14951
15669
  screen.notify(
14952
15670
  `image too large (${formatSize(buf.length)}, max ${formatSize(MAX_ATTACHMENT_BYTES)})`
@@ -15044,22 +15762,22 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15044
15762
  refreshQueueDisplay();
15045
15763
  }
15046
15764
  }
15047
- const enqueuePrompt = (text, attachments) => {
15765
+ const enqueuePrompt = (text, attachments, displayText) => {
15048
15766
  screen.scrollToBottom();
15049
15767
  if (handleBuiltinCommand(text)) {
15050
15768
  return;
15051
15769
  }
15052
- recordHistoryEntry(text);
15053
- void runPrompt(text, attachments);
15770
+ recordHistoryEntry(text, displayText);
15771
+ void runPrompt(text, attachments, displayText);
15054
15772
  };
15055
- const amendPrompt = (text, attachments) => {
15773
+ const amendPrompt = (text, attachments, displayText) => {
15056
15774
  screen.scrollToBottom();
15057
15775
  if (handleBuiltinCommand(text)) {
15058
15776
  return;
15059
15777
  }
15060
- recordHistoryEntry(text);
15778
+ recordHistoryEntry(text, displayText);
15061
15779
  if (!daemonSupportsAmend || currentHeadMessageId === void 0) {
15062
- void runPrompt(text, attachments);
15780
+ void runPrompt(text, attachments, displayText);
15063
15781
  return;
15064
15782
  }
15065
15783
  const target2 = currentHeadMessageId;
@@ -15070,7 +15788,12 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15070
15788
  for (const a of attachments) {
15071
15789
  blocks.push({ type: "image", data: a.data, mimeType: a.mimeType });
15072
15790
  }
15073
- const echo = { text, attachments, flushed: false };
15791
+ const echo = {
15792
+ text,
15793
+ displayText: displayText ?? text,
15794
+ attachments,
15795
+ flushed: false
15796
+ };
15074
15797
  pendingEchoes.push(echo);
15075
15798
  const popEcho = () => {
15076
15799
  const idx = pendingEchoes.indexOf(echo);
@@ -15161,6 +15884,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15161
15884
  toolsBlockEndedAt = null;
15162
15885
  toolsBlockStopReason = null;
15163
15886
  toolsExpanded = false;
15887
+ pendingEditMarks = [];
15164
15888
  screen.clearScrollback();
15165
15889
  return true;
15166
15890
  case "/demo-plan": {
@@ -15256,7 +15980,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15256
15980
  return false;
15257
15981
  }
15258
15982
  };
15259
- const runPrompt = async (text, attachments) => {
15983
+ const runPrompt = async (text, attachments, displayText) => {
15260
15984
  const userBlocks = [];
15261
15985
  if (text.length > 0) {
15262
15986
  userBlocks.push({ type: "text", text });
@@ -15265,7 +15989,12 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15265
15989
  userBlocks.push({ type: "image", data: a.data, mimeType: a.mimeType });
15266
15990
  }
15267
15991
  adjustPendingTurns(1);
15268
- const echo = { text, attachments, flushed: false };
15992
+ const echo = {
15993
+ text,
15994
+ displayText: displayText ?? text,
15995
+ attachments,
15996
+ flushed: false
15997
+ };
15269
15998
  pendingEchoes.push(echo);
15270
15999
  let cancelled = false;
15271
16000
  turnInFlight = {
@@ -15351,7 +16080,11 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15351
16080
  if (agentKey === null) {
15352
16081
  return;
15353
16082
  }
15354
- const lines = parseAgentMarkdown(agentBuffer);
16083
+ const w = screen.width();
16084
+ const lines = parseAgentMarkdown(
16085
+ agentBuffer,
16086
+ w > 0 ? { maxWidth: w } : void 0
16087
+ );
15355
16088
  if (lines.length === 0) {
15356
16089
  return;
15357
16090
  }
@@ -15460,7 +16193,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15460
16193
  toolsBlockStopReason = null;
15461
16194
  renderToolsBlock();
15462
16195
  };
15463
- const recordToolCall = (id, title, status, errorText) => {
16196
+ const recordToolCall = (id, title, status, errorText, editDiff) => {
15464
16197
  const wasNew = !toolStates.has(id);
15465
16198
  const existing = toolStates.get(id);
15466
16199
  const state = existing ?? {
@@ -15480,6 +16213,9 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15480
16213
  if (errorText !== void 0) {
15481
16214
  state.errorText = errorText;
15482
16215
  }
16216
+ if (editDiff !== void 0) {
16217
+ state.editDiff = editDiff;
16218
+ }
15483
16219
  toolStates.set(id, state);
15484
16220
  if (wasNew) {
15485
16221
  if (toolsBlockStartedAt === null) {
@@ -15490,6 +16226,45 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15490
16226
  toolCallOrder.push(id);
15491
16227
  }
15492
16228
  };
16229
+ let pendingEditMarks = [];
16230
+ const maybeRenderEditDiff = (toolCallId) => {
16231
+ const mode = config.tui.showFileUpdates;
16232
+ if (mode === "none") {
16233
+ return;
16234
+ }
16235
+ const state = toolStates.get(toolCallId);
16236
+ if (!state?.editDiff || state.status !== "completed") {
16237
+ return;
16238
+ }
16239
+ if (mode === "diff") {
16240
+ const lines = formatEditDiffBlock(state.editDiff, "diff");
16241
+ if (lines.length > 0) {
16242
+ screen.upsertLines(`editdiff:${toolCallId}`, lines);
16243
+ }
16244
+ return;
16245
+ }
16246
+ pendingEditMarks.push({ toolCallId, diff: state.editDiff });
16247
+ };
16248
+ const flushPendingEditMarks = () => {
16249
+ if (pendingEditMarks.length === 0) {
16250
+ return;
16251
+ }
16252
+ let lastPath = null;
16253
+ for (const { toolCallId, diff } of pendingEditMarks) {
16254
+ if (diff.path && diff.path === lastPath) {
16255
+ continue;
16256
+ }
16257
+ const lines = formatEditDiffBlock(diff, "edit");
16258
+ if (lines.length === 0) {
16259
+ continue;
16260
+ }
16261
+ screen.upsertLines(`editdiff:${toolCallId}`, lines);
16262
+ if (diff.path) {
16263
+ lastPath = diff.path;
16264
+ }
16265
+ }
16266
+ pendingEditMarks = [];
16267
+ };
15493
16268
  applyRenderEvent = (event) => {
15494
16269
  if (event.kind === "available-commands") {
15495
16270
  agentCommands = event.commands;
@@ -15561,17 +16336,22 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15561
16336
  toolCallOrder.length = 0;
15562
16337
  toolsExpanded = false;
15563
16338
  toolsBlockEndedAt = null;
16339
+ pendingEditMarks = [];
15564
16340
  startToolsBlock();
15565
16341
  screen.redraw();
15566
16342
  return;
15567
16343
  }
15568
16344
  if (event.kind === "agent-text") {
15569
16345
  closeThought();
16346
+ flushPendingEditMarks();
15570
16347
  appendAgentText(event.text);
15571
16348
  return;
15572
16349
  }
15573
16350
  if (event.kind === "agent-thought") {
15574
16351
  closeAgentText();
16352
+ if (viewPrefs.showThoughts) {
16353
+ flushPendingEditMarks();
16354
+ }
15575
16355
  appendThought(event.text);
15576
16356
  return;
15577
16357
  }
@@ -15596,8 +16376,15 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15596
16376
  if (event.kind === "tool-call") {
15597
16377
  closeAgentText();
15598
16378
  closeThought();
15599
- recordToolCall(event.toolCallId, event.title, event.status, void 0);
16379
+ recordToolCall(
16380
+ event.toolCallId,
16381
+ event.title,
16382
+ event.status,
16383
+ void 0,
16384
+ event.editDiff
16385
+ );
15600
16386
  renderToolsBlock();
16387
+ maybeRenderEditDiff(event.toolCallId);
15601
16388
  return;
15602
16389
  }
15603
16390
  if (event.kind === "plan") {
@@ -15617,12 +16404,14 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15617
16404
  event.toolCallId,
15618
16405
  event.title,
15619
16406
  event.status,
15620
- event.errorText
16407
+ event.errorText,
16408
+ event.editDiff
15621
16409
  );
15622
16410
  if (event.upstreamInterrupted) {
15623
16411
  upstreamInterruptedSeen = true;
15624
16412
  }
15625
16413
  renderToolsBlock();
16414
+ maybeRenderEditDiff(event.toolCallId);
15626
16415
  return;
15627
16416
  }
15628
16417
  if (event.kind === "model-changed") {
@@ -15675,7 +16464,17 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15675
16464
  toolsBlockStopReason = null;
15676
16465
  toolsExpanded = false;
15677
16466
  upstreamInterruptedSeen = false;
16467
+ pendingEditMarks = [];
15678
16468
  screen.ensureSeparator();
16469
+ if (shouldDriftSnap({
16470
+ pendingTurns,
16471
+ queueSize: queueCache.size,
16472
+ ownTurnInFlight: turnInFlight !== null,
16473
+ hasInFlightHead: currentHeadMessageId !== void 0,
16474
+ replayDraining
16475
+ })) {
16476
+ adjustPendingTurns(-pendingTurns);
16477
+ }
15679
16478
  }
15680
16479
  };
15681
16480
  const buffered = bufferedEvents;
@@ -15687,18 +16486,21 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15687
16486
  }
15688
16487
  }
15689
16488
  screen.pauseRepaint();
16489
+ replayDraining = true;
15690
16490
  try {
15691
16491
  for (const event of buffered) {
15692
16492
  applyRenderEvent(event);
15693
16493
  }
15694
16494
  } finally {
16495
+ replayDraining = false;
15695
16496
  screen.resumeRepaint();
15696
16497
  }
15697
16498
  if (replayedPromptTexts.length > 0) {
15698
16499
  const merged = mergeReplayedEntries(history, replayedPromptTexts);
15699
16500
  if (merged !== history) {
15700
16501
  history = merged;
15701
- dispatcher.setHistory(buildCombinedHistory(globalHistory, history));
16502
+ displayHistory = mergeReplayedEntries(displayHistory, replayedPromptTexts);
16503
+ dispatcher.setHistory(buildCombinedHistory(globalHistory, displayHistory));
15702
16504
  saveHistory(historyFile, history).catch(() => void 0);
15703
16505
  }
15704
16506
  }
@@ -15747,6 +16549,7 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15747
16549
  toolsBlockEndedAt = null;
15748
16550
  toolsBlockStopReason = null;
15749
16551
  toolsExpanded = false;
16552
+ pendingEditMarks = [];
15750
16553
  };
15751
16554
  onDisconnectHook = () => {
15752
16555
  screen.setBanner({ status: "disconnected", elapsedMs: void 0 });
@@ -15832,17 +16635,47 @@ async function runSession(term, config, target, opts, exitHint, viewPrefs, picke
15832
16635
  }
15833
16636
  ]);
15834
16637
  } else {
15835
- for (const params of buffered2) {
15836
- handleSessionUpdate(params);
16638
+ replayDraining = true;
16639
+ try {
16640
+ for (const params of buffered2) {
16641
+ handleSessionUpdate(params);
16642
+ }
16643
+ } finally {
16644
+ replayDraining = false;
15837
16645
  }
15838
16646
  }
15839
- if (fields && fields.turnStartedAt === void 0 && pendingTurns > 0) {
15840
- adjustPendingTurns(-pendingTurns);
16647
+ if (fields) {
16648
+ const reconcile = computeAttachReconcile({
16649
+ daemonTurnStartedAt: fields.turnStartedAt,
16650
+ pendingTurns
16651
+ });
16652
+ if (reconcile.pendingTurnsDelta !== 0) {
16653
+ adjustPendingTurns(reconcile.pendingTurnsDelta);
16654
+ }
16655
+ if (reconcile.banner === "busy" && reconcile.busySince !== void 0) {
16656
+ sessionBusySince = reconcile.busySince;
16657
+ screen.setBanner({
16658
+ status: "busy",
16659
+ elapsedMs: Date.now() - reconcile.busySince
16660
+ });
16661
+ if (sessionElapsedTimer === null) {
16662
+ sessionElapsedTimer = setInterval(() => {
16663
+ if (sessionBusySince === null || screenRef === null) {
16664
+ return;
16665
+ }
16666
+ screenRef.setBanner({ elapsedMs: Date.now() - sessionBusySince });
16667
+ renderToolsBlock();
16668
+ }, 1e3);
16669
+ }
16670
+ } else {
16671
+ screen.setBanner({ status: "ready", elapsedMs: void 0 });
16672
+ }
16673
+ } else {
16674
+ screen.setBanner({
16675
+ status: pendingTurns > 0 ? "busy" : "ready",
16676
+ elapsedMs: pendingTurns > 0 ? 0 : void 0
16677
+ });
15841
16678
  }
15842
- screen.setBanner({
15843
- status: pendingTurns > 0 ? "busy" : "ready",
15844
- elapsedMs: pendingTurns > 0 ? 0 : void 0
15845
- });
15846
16679
  };
15847
16680
  conn.onClose((err) => {
15848
16681
  if (err) {
@@ -15906,6 +16739,16 @@ async function resolveSession(term, config, target, opts, pickerPrefs) {
15906
16739
  }
15907
16740
  return newCtx(opts, cwd, config);
15908
16741
  }
16742
+ if (choice.kind === "fork") {
16743
+ const decided = await runForkFlow(term, target, choice, sessions);
16744
+ if (decided.kind === "cancel") {
16745
+ return null;
16746
+ }
16747
+ if (decided.kind === "back") {
16748
+ continue;
16749
+ }
16750
+ return decided.ctx;
16751
+ }
15909
16752
  opts.readonly = choice.readonly === true;
15910
16753
  const chosen = sessions.find((s) => s.sessionId === choice.sessionId);
15911
16754
  const isImportedFirstLaunch = chosen !== void 0 && !!chosen.importedFromMachine && !chosen.upstreamSessionId && !opts.readonly;
@@ -15966,6 +16809,51 @@ async function runImportedFirstLaunchFlow(term, chosen, choice, opts) {
15966
16809
  };
15967
16810
  }
15968
16811
  }
16812
+ async function runForkFlow(term, target, choice, sessions) {
16813
+ const source = sessions.find((s) => s.sessionId === choice.sourceSessionId);
16814
+ const isForeignNeverLaunched = !!choice.sourceImportedFromMachine && !choice.sourceUpstreamSessionId;
16815
+ let cwd = choice.sourceCwd;
16816
+ if (isForeignNeverLaunched) {
16817
+ if (!source) {
16818
+ return { kind: "back" };
16819
+ }
16820
+ const cwdResult = await promptForImportCwd(term, source);
16821
+ if (cwdResult.kind === "cancel") {
16822
+ return { kind: "cancel" };
16823
+ }
16824
+ if (cwdResult.kind === "back") {
16825
+ return { kind: "back" };
16826
+ }
16827
+ cwd = cwdResult.path;
16828
+ }
16829
+ let result;
16830
+ try {
16831
+ result = await forkSession(
16832
+ target,
16833
+ choice.sourceSessionId,
16834
+ isForeignNeverLaunched ? { cwd } : {}
16835
+ );
16836
+ } catch (err) {
16837
+ term.red(`
16838
+ fork failed: ${err.message}
16839
+ `);
16840
+ return { kind: "cancel" };
16841
+ }
16842
+ return {
16843
+ kind: "ctx",
16844
+ ctx: {
16845
+ sessionId: result.sessionId,
16846
+ agentId: choice.sourceAgentId ?? "",
16847
+ cwd,
16848
+ // For foreign-never-launched forks, the daemon stamped the chosen
16849
+ // cwd onto meta.json via the POST body, but the very first attach
16850
+ // still goes through the import-reseed path (upstreamSessionId=""),
16851
+ // and importAttachHint is what makes attachManagerHooks persist
16852
+ // the local cwd over the bundle's recorded one.
16853
+ ...isForeignNeverLaunched ? { importAttachHint: { agentId: choice.sourceAgentId ?? "", cwd } } : {}
16854
+ }
16855
+ };
16856
+ }
15969
16857
  function newCtx(opts, cwd, config) {
15970
16858
  return {
15971
16859
  sessionId: "__new__",
@@ -16174,7 +17062,7 @@ var init_tui = __esm({
16174
17062
  // src/cli.ts
16175
17063
  import { readFileSync as readFileSync2 } from "fs";
16176
17064
  import { fileURLToPath as fileURLToPath2 } from "url";
16177
- import { dirname as dirname6, resolve as resolve6 } from "path";
17065
+ import { dirname as dirname7, resolve as resolve6 } from "path";
16178
17066
 
16179
17067
  // src/cli/parse-args.ts
16180
17068
  var KNOWN_BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
@@ -16363,7 +17251,7 @@ import { setTimeout as sleep2 } from "timers/promises";
16363
17251
  import chalk from "chalk";
16364
17252
 
16365
17253
  // src/daemon/server.ts
16366
- import * as fs17 from "fs";
17254
+ import * as fs16 from "fs";
16367
17255
  import * as fsp8 from "fs/promises";
16368
17256
  import Fastify from "fastify";
16369
17257
  import websocketPlugin from "@fastify/websocket";
@@ -16384,6 +17272,7 @@ import createPinoRoll from "pino-roll";
16384
17272
 
16385
17273
  // src/core/registry.ts
16386
17274
  init_paths();
17275
+ init_json_store();
16387
17276
  import * as fs6 from "fs/promises";
16388
17277
  import * as path5 from "path";
16389
17278
  import { z as z2 } from "zod";
@@ -17024,54 +17913,26 @@ var Registry = class {
17024
17913
  return cached2;
17025
17914
  }
17026
17915
  async readDiskCache() {
17027
- let text;
17028
- try {
17029
- text = await fs6.readFile(paths.registryCache(), "utf8");
17030
- } catch (err) {
17031
- const e = err;
17032
- if (e.code === "ENOENT") {
17033
- return void 0;
17034
- }
17035
- throw err;
17916
+ const parsed = await readJsonSafe(
17917
+ paths.registryCache()
17918
+ );
17919
+ if (!parsed || typeof parsed.fetchedAt !== "number" || parsed.data === void 0) {
17920
+ return void 0;
17036
17921
  }
17037
17922
  try {
17038
- const parsed = JSON.parse(text);
17039
- if (typeof parsed.fetchedAt !== "number" || parsed.data === void 0) {
17040
- return void 0;
17041
- }
17042
17923
  const data = RegistryDocument.parse(parsed.data);
17043
17924
  return { fetchedAt: parsed.fetchedAt, raw: parsed.data, data };
17044
17925
  } catch {
17045
17926
  return void 0;
17046
17927
  }
17047
17928
  }
17048
- // Atomic write: dump to a sibling temp path, then rename onto the
17049
- // target. POSIX rename is atomic within a filesystem, so readers
17050
- // either see the old file or the fully-written new file — never a
17051
- // truncated middle. This also makes simultaneous writers safe
17052
- // without a lock file: the loser of the rename race just gets its
17053
- // version replaced by the winner's.
17054
17929
  async writeDiskCache(cache) {
17055
- await fs6.mkdir(paths.home(), { recursive: true });
17056
- const final = paths.registryCache();
17057
- const tmp = `${final}.tmp-${process.pid}-${randSuffix()}`;
17058
- const body = JSON.stringify(
17059
- { fetchedAt: cache.fetchedAt, data: cache.raw },
17060
- null,
17061
- 2
17062
- ) + "\n";
17063
- try {
17064
- await fs6.writeFile(tmp, body, "utf8");
17065
- await fs6.rename(tmp, final);
17066
- } catch (err) {
17067
- await fs6.unlink(tmp).catch(() => void 0);
17068
- throw err;
17069
- }
17930
+ await writeJsonAtomic(paths.registryCache(), {
17931
+ fetchedAt: cache.fetchedAt,
17932
+ data: cache.raw
17933
+ });
17070
17934
  }
17071
17935
  };
17072
- function randSuffix() {
17073
- return Math.random().toString(36).slice(2, 10);
17074
- }
17075
17936
  function npxPackageBasename(agent) {
17076
17937
  const pkg = agent.distribution.npx?.package;
17077
17938
  if (!pkg) {
@@ -17384,6 +18245,7 @@ init_session();
17384
18245
 
17385
18246
  // src/core/session-store.ts
17386
18247
  init_paths();
18248
+ init_json_store();
17387
18249
  import * as fs8 from "fs/promises";
17388
18250
  import * as path6 from "path";
17389
18251
  import { customAlphabet as customAlphabet2 } from "nanoid";
@@ -17469,6 +18331,12 @@ var SessionRecord = z4.object({
17469
18331
  // Set when this session was spawned as a child by a transformer via
17470
18332
  // hydra-acp/spawn_child_session. Points to the spawning session's id.
17471
18333
  parentSessionId: z4.string().optional(),
18334
+ // Set when this session was created by hydra-acp/fork_session.
18335
+ // forkedFromSessionId points to the local source session; forkedFromMessageId
18336
+ // is the resolved forkAt — the messageId of the turn_complete the slice
18337
+ // ended at. Kept so future UI can show "branched from turn N of session X".
18338
+ forkedFromSessionId: z4.string().optional(),
18339
+ forkedFromMessageId: z4.string().optional(),
17472
18340
  // clientInfo from the process that issued session/new. Picker and
17473
18341
  // `sessions list` use this to hide cat-style ancillary sessions by
17474
18342
  // default; carried in meta.json so cold sessions filter the same way.
@@ -17485,30 +18353,21 @@ function assertSafeId(id) {
17485
18353
  var SessionStore = class {
17486
18354
  async write(record) {
17487
18355
  assertSafeId(record.sessionId);
17488
- await fs8.mkdir(paths.sessionDir(record.sessionId), { recursive: true });
17489
18356
  const full = { version: 1, ...record };
17490
- await fs8.writeFile(
17491
- paths.sessionFile(record.sessionId),
17492
- JSON.stringify(full, null, 2) + "\n",
17493
- { encoding: "utf8", mode: 384 }
17494
- );
18357
+ await writeJsonAtomic(paths.sessionFile(record.sessionId), full, {
18358
+ mode: 384
18359
+ });
17495
18360
  }
17496
18361
  async read(sessionId) {
17497
18362
  if (!SESSION_ID_PATTERN.test(sessionId)) {
17498
18363
  return void 0;
17499
18364
  }
17500
- let raw;
17501
- try {
17502
- raw = await fs8.readFile(paths.sessionFile(sessionId), "utf8");
17503
- } catch (err) {
17504
- const e = err;
17505
- if (e.code === "ENOENT") {
17506
- return void 0;
17507
- }
17508
- throw err;
18365
+ const parsed = await readJsonSafe(paths.sessionFile(sessionId));
18366
+ if (parsed === void 0) {
18367
+ return void 0;
17509
18368
  }
17510
18369
  try {
17511
- return SessionRecord.parse(JSON.parse(raw));
18370
+ return SessionRecord.parse(parsed);
17512
18371
  } catch {
17513
18372
  return void 0;
17514
18373
  }
@@ -17595,6 +18454,8 @@ function recordFromMemorySession(args) {
17595
18454
  agentModels: args.agentModels,
17596
18455
  pendingHistorySync: args.pendingHistorySync,
17597
18456
  parentSessionId: args.parentSessionId,
18457
+ forkedFromSessionId: args.forkedFromSessionId,
18458
+ forkedFromMessageId: args.forkedFromMessageId,
17598
18459
  originatingClient: args.originatingClient,
17599
18460
  createdAt: args.createdAt ?? now,
17600
18461
  updatedAt: args.updatedAt ?? now
@@ -17721,6 +18582,19 @@ var HistoryStore = class {
17721
18582
  }
17722
18583
  return out;
17723
18584
  }
18585
+ // Wait for every pending append/rewrite/compact across all sessions to
18586
+ // settle. Daemon shutdown calls this after closing sessions so the final
18587
+ // turn_complete(interrupted) emitted by markClosed reaches disk before
18588
+ // the process exits — without this, history-replay attaches after a
18589
+ // restart see an unmatched prompt_received and leak pendingTurns on
18590
+ // every client.
18591
+ async flushAll() {
18592
+ const pending = [...this.writeQueues.values()];
18593
+ if (pending.length === 0) {
18594
+ return;
18595
+ }
18596
+ await Promise.allSettled(pending);
18597
+ }
17724
18598
  async delete(sessionId) {
17725
18599
  if (!SESSION_ID_PATTERN2.test(sessionId)) {
17726
18600
  return;
@@ -17756,11 +18630,96 @@ var HistoryStore = class {
17756
18630
  });
17757
18631
  return task$;
17758
18632
  }
17759
- };
18633
+ };
18634
+
18635
+ // src/core/session-manager.ts
18636
+ init_paths();
18637
+ init_history();
18638
+
18639
+ // src/core/bundle.ts
18640
+ import { z as z5 } from "zod";
18641
+ var HistoryEntrySchema = z5.object({
18642
+ method: z5.string(),
18643
+ params: z5.unknown(),
18644
+ recordedAt: z5.number()
18645
+ });
18646
+ var BundleSession = z5.object({
18647
+ // The exporter's local id. Regenerated fresh on import (sessionId is
18648
+ // the local namespace; lineageId is what survives across hops).
18649
+ sessionId: z5.string(),
18650
+ // Required on bundles — the export path backfills if the source
18651
+ // record was written before lineageId existed.
18652
+ lineageId: z5.string(),
18653
+ // The exporter's agent-side session id at export time. Carried so
18654
+ // importers can persist it as a breadcrumb (and, eventually, as the
18655
+ // handle a "connect back to origin" feature would need). Omitted on
18656
+ // bundles whose source record never bound to an agent (e.g. a
18657
+ // re-export of an imported, not-yet-attached session).
18658
+ upstreamSessionId: z5.string().optional(),
18659
+ agentId: z5.string(),
18660
+ cwd: z5.string(),
18661
+ title: z5.string().optional(),
18662
+ currentModel: z5.string().optional(),
18663
+ currentMode: z5.string().optional(),
18664
+ currentUsage: PersistedUsage.optional(),
18665
+ agentCommands: z5.array(PersistedAgentCommand).optional(),
18666
+ agentModes: z5.array(PersistedAgentMode).optional(),
18667
+ createdAt: z5.string(),
18668
+ updatedAt: z5.string()
18669
+ });
18670
+ var Bundle = z5.object({
18671
+ version: z5.literal(1),
18672
+ exportedAt: z5.string(),
18673
+ exportedFrom: z5.object({
18674
+ hydraVersion: z5.string(),
18675
+ machine: z5.string(),
18676
+ // Externally-reachable name (and optional ":port") for the exporting
18677
+ // daemon, sourced from config.daemon.publicHost (or daemon.host when
18678
+ // non-loopback). Carried so an importer can construct a hydra:// URL
18679
+ // that dials back to the origin — e.g. over Tailscale. Omitted when
18680
+ // the exporter has no routable address; never falls back to loopback.
18681
+ hydraHost: z5.string().optional()
18682
+ }),
18683
+ session: BundleSession,
18684
+ history: z5.array(HistoryEntrySchema),
18685
+ promptHistory: z5.array(z5.string()).optional()
18686
+ });
18687
+ function encodeBundle(params) {
18688
+ const bundle = {
18689
+ version: 1,
18690
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
18691
+ exportedFrom: {
18692
+ hydraVersion: params.hydraVersion,
18693
+ machine: params.machine,
18694
+ ...params.hydraHost !== void 0 && params.hydraHost.length > 0 ? { hydraHost: params.hydraHost } : {}
18695
+ },
18696
+ session: {
18697
+ sessionId: params.record.sessionId,
18698
+ lineageId: params.record.lineageId,
18699
+ ...params.record.upstreamSessionId ? { upstreamSessionId: params.record.upstreamSessionId } : {},
18700
+ agentId: params.record.agentId,
18701
+ cwd: params.record.cwd,
18702
+ ...params.record.title !== void 0 ? { title: params.record.title } : {},
18703
+ ...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
18704
+ ...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
18705
+ ...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
18706
+ ...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
18707
+ ...params.record.agentModes !== void 0 ? { agentModes: params.record.agentModes } : {},
18708
+ createdAt: params.record.createdAt,
18709
+ updatedAt: params.record.updatedAt
18710
+ },
18711
+ history: params.history
18712
+ };
18713
+ if (params.promptHistory !== void 0) {
18714
+ bundle.promptHistory = params.promptHistory;
18715
+ }
18716
+ return bundle;
18717
+ }
18718
+ function decodeBundle(raw) {
18719
+ return Bundle.parse(raw);
18720
+ }
17760
18721
 
17761
18722
  // src/core/session-manager.ts
17762
- init_paths();
17763
- init_history();
17764
18723
  init_types();
17765
18724
  init_hydra_version();
17766
18725
  init_queue_store();
@@ -18027,6 +18986,8 @@ var SessionManager = class {
18027
18986
  firstPromptSeeded: !!params.title,
18028
18987
  createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
18029
18988
  originatingClient: params.originatingClient,
18989
+ forkedFromSessionId: params.forkedFromSessionId,
18990
+ forkedFromMessageId: params.forkedFromMessageId,
18030
18991
  extensionCommands: this.extensionCommands
18031
18992
  });
18032
18993
  await this.attachManagerHooks(session);
@@ -18095,6 +19056,8 @@ var SessionManager = class {
18095
19056
  firstPromptSeeded: !!params.title,
18096
19057
  createdAt: params.createdAt ? new Date(params.createdAt).getTime() : void 0,
18097
19058
  originatingClient: params.originatingClient,
19059
+ forkedFromSessionId: params.forkedFromSessionId,
19060
+ forkedFromMessageId: params.forkedFromMessageId,
18098
19061
  extensionCommands: this.extensionCommands
18099
19062
  });
18100
19063
  await this.attachManagerHooks(session);
@@ -18445,7 +19408,9 @@ var SessionManager = class {
18445
19408
  agentModels: record.agentModels,
18446
19409
  createdAt: record.createdAt,
18447
19410
  pendingHistorySync: record.pendingHistorySync,
18448
- originatingClient: record.originatingClient
19411
+ originatingClient: record.originatingClient,
19412
+ forkedFromSessionId: record.forkedFromSessionId,
19413
+ forkedFromMessageId: record.forkedFromMessageId
18449
19414
  };
18450
19415
  }
18451
19416
  async clearPendingHistorySync(sessionId) {
@@ -18546,6 +19511,8 @@ var SessionManager = class {
18546
19511
  currentModel: session.currentModel,
18547
19512
  currentUsage: session.currentUsage,
18548
19513
  parentSessionId: session.parentSessionId,
19514
+ forkedFromSessionId: session.forkedFromSessionId,
19515
+ forkedFromMessageId: session.forkedFromMessageId,
18549
19516
  originatingClient: session.originatingClient,
18550
19517
  updatedAt: used,
18551
19518
  attachedClients: session.attachedCount,
@@ -18576,6 +19543,8 @@ var SessionManager = class {
18576
19543
  importedFromMachine: r.importedFromMachine,
18577
19544
  importedFromUpstreamSessionId: r.importedFromUpstreamSessionId,
18578
19545
  parentSessionId: r.parentSessionId,
19546
+ forkedFromSessionId: r.forkedFromSessionId,
19547
+ forkedFromMessageId: r.forkedFromMessageId,
18579
19548
  originatingClient: r.originatingClient,
18580
19549
  updatedAt: used,
18581
19550
  attachedClients: 0,
@@ -18663,10 +19632,114 @@ var SessionManager = class {
18663
19632
  replaced: false
18664
19633
  };
18665
19634
  }
18666
- // Write the imported bundle's history.jsonl, prompt-history (if
18667
- // present), and meta.json. upstreamSessionId is left empty as the
19635
+ // Branch an existing local session into a new one that shares context
19636
+ // up to the chosen turn boundary and diverges from there. Composes the
19637
+ // import pipeline: synthesizes a Bundle from the source's record and
19638
+ // sliced history, mints a fresh lineageId, then writes the new record
19639
+ // via writeImportedRecord with forked* breadcrumbs instead of
19640
+ // imported*. The fork carries upstreamSessionId="" so the first attach
19641
+ // triggers seedFromImport — same wire shape as an imported session.
19642
+ //
19643
+ // forkAt defaults to the messageId of the source's most recent
19644
+ // turn_complete; explicit forkAt must reference a session/update
19645
+ // entry that's present in the source's history.jsonl. Cutting at a
19646
+ // completed turn excludes any in-flight prompt by construction
19647
+ // (history.jsonl is appended serially per session), so no locking
19648
+ // against the live source is needed.
19649
+ //
19650
+ // agentId defaults to the source's agent. Overriding to a different
19651
+ // agent scrubs agent-specific state from the fork (model, mode,
19652
+ // usage, agent-emitted commands/modes/models) so the new agent boots
19653
+ // clean — title and conversation transcript are agent-agnostic and
19654
+ // are kept.
19655
+ async forkSession(sourceSessionId, opts = {}) {
19656
+ const sourceRecord = await this.store.read(sourceSessionId);
19657
+ if (!sourceRecord) {
19658
+ const err = new Error(`source session not found: ${sourceSessionId}`);
19659
+ err.code = JsonRpcErrorCodes.SessionNotFound;
19660
+ throw err;
19661
+ }
19662
+ const targetAgentId = opts.agentId ?? sourceRecord.agentId;
19663
+ const crossAgent = targetAgentId !== sourceRecord.agentId;
19664
+ if (crossAgent) {
19665
+ const def = await this.registry.getAgent(targetAgentId);
19666
+ if (!def) {
19667
+ const err = new Error(
19668
+ `agent ${targetAgentId} not found in registry`
19669
+ );
19670
+ err.code = JsonRpcErrorCodes.AgentNotInstalled;
19671
+ throw err;
19672
+ }
19673
+ }
19674
+ const sourceHistory = await this.histories.load(sourceSessionId).catch(() => []);
19675
+ let cutoffIndex;
19676
+ let forkedAt;
19677
+ if (opts.forkAt !== void 0) {
19678
+ cutoffIndex = findMessageIdIndex(sourceHistory, opts.forkAt);
19679
+ if (cutoffIndex < 0) {
19680
+ const err = new Error(
19681
+ `forkAt messageId not found in source history: ${opts.forkAt}`
19682
+ );
19683
+ err.code = JsonRpcErrorCodes.InvalidParams;
19684
+ throw err;
19685
+ }
19686
+ forkedAt = opts.forkAt;
19687
+ } else {
19688
+ const found = findLastTurnComplete(sourceHistory);
19689
+ if (!found) {
19690
+ const err = new Error(
19691
+ `source session ${sourceSessionId} has no completed turns to fork from`
19692
+ );
19693
+ err.code = JsonRpcErrorCodes.InvalidParams;
19694
+ throw err;
19695
+ }
19696
+ cutoffIndex = found.index;
19697
+ forkedAt = found.messageId;
19698
+ }
19699
+ const slicedHistory = sourceHistory.slice(0, cutoffIndex + 1);
19700
+ const promptHistory = await loadPromptHistorySafely(sourceSessionId);
19701
+ const recordForBundle = {
19702
+ ...sourceRecord,
19703
+ lineageId: generateLineageId(),
19704
+ agentId: targetAgentId,
19705
+ ...crossAgent ? {
19706
+ currentModel: void 0,
19707
+ currentMode: void 0,
19708
+ currentUsage: void 0,
19709
+ agentCommands: void 0,
19710
+ agentModes: void 0,
19711
+ agentModels: void 0
19712
+ } : {}
19713
+ };
19714
+ const bundle = encodeBundle({
19715
+ record: recordForBundle,
19716
+ history: slicedHistory,
19717
+ promptHistory: promptHistory.length > 0 ? promptHistory : void 0,
19718
+ hydraVersion: HYDRA_VERSION,
19719
+ machine: os3.hostname()
19720
+ });
19721
+ const newId = `${HYDRA_SESSION_PREFIX}${generateRawSessionId()}`;
19722
+ await this.writeImportedRecord({
19723
+ sessionId: newId,
19724
+ bundle,
19725
+ cwd: opts.cwd,
19726
+ forkedFromSessionId: sourceSessionId,
19727
+ forkedFromMessageId: forkedAt
19728
+ });
19729
+ return {
19730
+ sessionId: newId,
19731
+ forkedFromSessionId: sourceSessionId,
19732
+ forkedAt
19733
+ };
19734
+ }
19735
+ // Write the imported (or forked) bundle's history.jsonl, prompt-history
19736
+ // (if present), and meta.json. upstreamSessionId is left empty as the
18668
19737
  // marker that the first attach should bootstrap a fresh agent and
18669
- // run seedFromImport rather than calling session/load.
19738
+ // run seedFromImport rather than calling session/load. When
19739
+ // forkedFromSessionId is set, the record is marked as a local fork
19740
+ // (forked* fields populated) instead of a cross-machine import
19741
+ // (imported* fields populated) — both share the seed-on-first-attach
19742
+ // wire shape but trace differently in list views.
18670
19743
  async writeImportedRecord(args) {
18671
19744
  await this.histories.rewrite(
18672
19745
  args.sessionId,
@@ -18683,14 +19756,20 @@ var SessionManager = class {
18683
19756
  ).catch(() => void 0);
18684
19757
  }
18685
19758
  const now = (/* @__PURE__ */ new Date()).toISOString();
19759
+ const isFork = args.forkedFromSessionId !== void 0;
18686
19760
  await this.enqueueMetaWrite(args.sessionId, async () => {
18687
19761
  await this.store.write({
18688
19762
  sessionId: args.sessionId,
18689
19763
  lineageId: args.bundle.session.lineageId,
18690
19764
  upstreamSessionId: "",
18691
- importedFromSessionId: args.bundle.session.sessionId,
18692
- importedFromUpstreamSessionId: args.bundle.session.upstreamSessionId,
18693
- importedFromMachine: args.bundle.exportedFrom.machine,
19765
+ ...isFork ? {
19766
+ forkedFromSessionId: args.forkedFromSessionId,
19767
+ forkedFromMessageId: args.forkedFromMessageId
19768
+ } : {
19769
+ importedFromSessionId: args.bundle.session.sessionId,
19770
+ importedFromUpstreamSessionId: args.bundle.session.upstreamSessionId,
19771
+ importedFromMachine: args.bundle.exportedFrom.machine
19772
+ },
18694
19773
  agentId: args.bundle.session.agentId,
18695
19774
  cwd: args.cwd ?? args.bundle.session.cwd,
18696
19775
  title: args.bundle.session.title,
@@ -18824,6 +19903,14 @@ var SessionManager = class {
18824
19903
  }
18825
19904
  await Promise.allSettled(pending);
18826
19905
  }
19906
+ // Wait for every pending history.jsonl write to settle. markClosed
19907
+ // broadcasts turn_complete(interrupted) for the in-flight turn via a
19908
+ // fire-and-forget store.append; without flushing, a SIGTERM can exit
19909
+ // before that append hits disk, leaving an unmatched prompt_received
19910
+ // in history that leaks pendingTurns on every client that replays it.
19911
+ async flushHistoryWrites() {
19912
+ await this.histories.flushAll();
19913
+ }
18827
19914
  // Startup hook: scan persisted sessions for non-empty queue files,
18828
19915
  // apply the TTL, resurrect anything with surviving entries, and
18829
19916
  // replay them through the normal queue path. Called from the daemon
@@ -18922,6 +20009,8 @@ function mergeForPersistence(session, existing) {
18922
20009
  agentModes,
18923
20010
  agentModels,
18924
20011
  parentSessionId: session.parentSessionId ?? existing?.parentSessionId,
20012
+ forkedFromSessionId: session.forkedFromSessionId ?? existing?.forkedFromSessionId,
20013
+ forkedFromMessageId: session.forkedFromMessageId ?? existing?.forkedFromMessageId,
18925
20014
  originatingClient: session.originatingClient ?? existing?.originatingClient,
18926
20015
  createdAt: existing?.createdAt ?? new Date(session.createdAt).toISOString()
18927
20016
  });
@@ -19191,6 +20280,23 @@ function parseModesList(list) {
19191
20280
  }
19192
20281
  return out;
19193
20282
  }
20283
+ function findLastTurnComplete(history) {
20284
+ for (let i = history.length - 1; i >= 0; i--) {
20285
+ const entry = history[i];
20286
+ if (!entry || entry.method !== "session/update") {
20287
+ continue;
20288
+ }
20289
+ const update = entry.params?.update;
20290
+ if (update?.sessionUpdate !== "turn_complete") {
20291
+ continue;
20292
+ }
20293
+ if (typeof update.messageId !== "string" || update.messageId.length === 0) {
20294
+ continue;
20295
+ }
20296
+ return { index: i, messageId: update.messageId };
20297
+ }
20298
+ return void 0;
20299
+ }
19194
20300
  async function loadPromptHistorySafely(sessionId) {
19195
20301
  try {
19196
20302
  const raw = await fs12.readFile(paths.tuiHistoryFile(sessionId), "utf8");
@@ -20322,9 +21428,9 @@ init_hydra_version();
20322
21428
 
20323
21429
  // src/core/session-tokens.ts
20324
21430
  init_paths();
20325
- import * as fs15 from "fs/promises";
21431
+ init_json_store();
20326
21432
  import * as path12 from "path";
20327
- import { createHash, randomBytes, timingSafeEqual } from "crypto";
21433
+ import { createHash, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
20328
21434
  var TOKEN_PREFIX = "hydra_session_";
20329
21435
  var DEFAULT_TTL_SEC = 60 * 60 * 24 * 30;
20330
21436
  var ID_LENGTH = 12;
@@ -20337,7 +21443,7 @@ function sha256Hex(input) {
20337
21443
  return createHash("sha256").update(input).digest("hex");
20338
21444
  }
20339
21445
  function randomHex(bytes) {
20340
- return randomBytes(bytes).toString("hex");
21446
+ return randomBytes2(bytes).toString("hex");
20341
21447
  }
20342
21448
  function generateId() {
20343
21449
  return randomHex(ID_LENGTH).slice(0, ID_LENGTH * 2);
@@ -20357,17 +21463,11 @@ var SessionTokenStore = class _SessionTokenStore {
20357
21463
  }
20358
21464
  static async load() {
20359
21465
  let records = [];
20360
- try {
20361
- const raw = await fs15.readFile(tokensFilePath(), "utf8");
20362
- const parsed = JSON.parse(raw);
20363
- if (parsed && Array.isArray(parsed.records)) {
20364
- records = parsed.records.filter(isRecord);
20365
- }
20366
- } catch (err) {
20367
- const e = err;
20368
- if (e.code !== "ENOENT") {
20369
- throw err;
20370
- }
21466
+ const parsed = await readJsonSafe(
21467
+ tokensFilePath()
21468
+ );
21469
+ if (parsed && Array.isArray(parsed.records)) {
21470
+ records = parsed.records.filter(isRecord);
20371
21471
  }
20372
21472
  const store = new _SessionTokenStore(records);
20373
21473
  const removed = store.sweepExpired(/* @__PURE__ */ new Date());
@@ -20484,14 +21584,11 @@ var SessionTokenStore = class _SessionTokenStore {
20484
21584
  await this.writeInflight;
20485
21585
  }
20486
21586
  const records = Array.from(this.records.values());
20487
- const payload = JSON.stringify({ records }, null, 2) + "\n";
20488
- this.writeInflight = (async () => {
20489
- await fs15.mkdir(paths.home(), { recursive: true });
20490
- await fs15.writeFile(tokensFilePath(), payload, {
20491
- encoding: "utf8",
20492
- mode: 384
20493
- });
20494
- })();
21587
+ this.writeInflight = writeJsonAtomic(
21588
+ tokensFilePath(),
21589
+ { records },
21590
+ { mode: 384 }
21591
+ );
20495
21592
  try {
20496
21593
  await this.writeInflight;
20497
21594
  } finally {
@@ -20661,89 +21758,6 @@ var AuthRateLimiter = class {
20661
21758
  init_config();
20662
21759
  import * as os4 from "os";
20663
21760
 
20664
- // src/core/bundle.ts
20665
- import { z as z5 } from "zod";
20666
- var HistoryEntrySchema = z5.object({
20667
- method: z5.string(),
20668
- params: z5.unknown(),
20669
- recordedAt: z5.number()
20670
- });
20671
- var BundleSession = z5.object({
20672
- // The exporter's local id. Regenerated fresh on import (sessionId is
20673
- // the local namespace; lineageId is what survives across hops).
20674
- sessionId: z5.string(),
20675
- // Required on bundles — the export path backfills if the source
20676
- // record was written before lineageId existed.
20677
- lineageId: z5.string(),
20678
- // The exporter's agent-side session id at export time. Carried so
20679
- // importers can persist it as a breadcrumb (and, eventually, as the
20680
- // handle a "connect back to origin" feature would need). Omitted on
20681
- // bundles whose source record never bound to an agent (e.g. a
20682
- // re-export of an imported, not-yet-attached session).
20683
- upstreamSessionId: z5.string().optional(),
20684
- agentId: z5.string(),
20685
- cwd: z5.string(),
20686
- title: z5.string().optional(),
20687
- currentModel: z5.string().optional(),
20688
- currentMode: z5.string().optional(),
20689
- currentUsage: PersistedUsage.optional(),
20690
- agentCommands: z5.array(PersistedAgentCommand).optional(),
20691
- agentModes: z5.array(PersistedAgentMode).optional(),
20692
- createdAt: z5.string(),
20693
- updatedAt: z5.string()
20694
- });
20695
- var Bundle = z5.object({
20696
- version: z5.literal(1),
20697
- exportedAt: z5.string(),
20698
- exportedFrom: z5.object({
20699
- hydraVersion: z5.string(),
20700
- machine: z5.string(),
20701
- // Externally-reachable name (and optional ":port") for the exporting
20702
- // daemon, sourced from config.daemon.publicHost (or daemon.host when
20703
- // non-loopback). Carried so an importer can construct a hydra:// URL
20704
- // that dials back to the origin — e.g. over Tailscale. Omitted when
20705
- // the exporter has no routable address; never falls back to loopback.
20706
- hydraHost: z5.string().optional()
20707
- }),
20708
- session: BundleSession,
20709
- history: z5.array(HistoryEntrySchema),
20710
- promptHistory: z5.array(z5.string()).optional()
20711
- });
20712
- function encodeBundle(params) {
20713
- const bundle = {
20714
- version: 1,
20715
- exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
20716
- exportedFrom: {
20717
- hydraVersion: params.hydraVersion,
20718
- machine: params.machine,
20719
- ...params.hydraHost !== void 0 && params.hydraHost.length > 0 ? { hydraHost: params.hydraHost } : {}
20720
- },
20721
- session: {
20722
- sessionId: params.record.sessionId,
20723
- lineageId: params.record.lineageId,
20724
- ...params.record.upstreamSessionId ? { upstreamSessionId: params.record.upstreamSessionId } : {},
20725
- agentId: params.record.agentId,
20726
- cwd: params.record.cwd,
20727
- ...params.record.title !== void 0 ? { title: params.record.title } : {},
20728
- ...params.record.currentModel !== void 0 ? { currentModel: params.record.currentModel } : {},
20729
- ...params.record.currentMode !== void 0 ? { currentMode: params.record.currentMode } : {},
20730
- ...params.record.currentUsage !== void 0 ? { currentUsage: params.record.currentUsage } : {},
20731
- ...params.record.agentCommands !== void 0 ? { agentCommands: params.record.agentCommands } : {},
20732
- ...params.record.agentModes !== void 0 ? { agentModes: params.record.agentModes } : {},
20733
- createdAt: params.record.createdAt,
20734
- updatedAt: params.record.updatedAt
20735
- },
20736
- history: params.history
20737
- };
20738
- if (params.promptHistory !== void 0) {
20739
- bundle.promptHistory = params.promptHistory;
20740
- }
20741
- return bundle;
20742
- }
20743
- function decodeBundle(raw) {
20744
- return Bundle.parse(raw);
20745
- }
20746
-
20747
21761
  // src/core/transcript.ts
20748
21762
  init_render_update();
20749
21763
  init_session();
@@ -21519,6 +22533,48 @@ function registerSessionRoutes(app, manager, defaults) {
21519
22533
  reply.header("Content-Type", "text/markdown; charset=utf-8");
21520
22534
  reply.code(200).send(bundleToMarkdown(bundle));
21521
22535
  });
22536
+ app.post("/v1/sessions/:id/fork", async (request, reply) => {
22537
+ const raw = request.params.id;
22538
+ const id = await manager.resolveCanonicalId(raw) ?? raw;
22539
+ const body = request.body ?? {};
22540
+ const opts = {};
22541
+ if (body.forkAt !== void 0) {
22542
+ if (typeof body.forkAt !== "string" || body.forkAt.length === 0) {
22543
+ reply.code(400).send({ error: "forkAt must be a non-empty string" });
22544
+ return;
22545
+ }
22546
+ opts.forkAt = body.forkAt;
22547
+ }
22548
+ if (body.cwd !== void 0) {
22549
+ if (typeof body.cwd !== "string" || body.cwd.length === 0) {
22550
+ reply.code(400).send({ error: "cwd must be a non-empty string" });
22551
+ return;
22552
+ }
22553
+ opts.cwd = expandHome(body.cwd);
22554
+ }
22555
+ if (body.agentId !== void 0) {
22556
+ if (typeof body.agentId !== "string" || body.agentId.length === 0) {
22557
+ reply.code(400).send({ error: "agentId must be a non-empty string" });
22558
+ return;
22559
+ }
22560
+ opts.agentId = body.agentId;
22561
+ }
22562
+ try {
22563
+ const result = await manager.forkSession(id, opts);
22564
+ reply.code(201).send(result);
22565
+ } catch (err) {
22566
+ const e = err;
22567
+ if (e.code === JsonRpcErrorCodes.SessionNotFound) {
22568
+ reply.code(404).send({ error: e.message });
22569
+ return;
22570
+ }
22571
+ if (e.code === JsonRpcErrorCodes.InvalidParams || e.code === JsonRpcErrorCodes.AgentNotInstalled) {
22572
+ reply.code(400).send({ error: e.message });
22573
+ return;
22574
+ }
22575
+ reply.code(500).send({ error: e.message });
22576
+ }
22577
+ });
21522
22578
  app.post("/v1/sessions/import", async (request, reply) => {
21523
22579
  const body = request.body ?? {};
21524
22580
  if (body.bundle === void 0) {
@@ -21964,9 +23020,9 @@ import { z as z6 } from "zod";
21964
23020
 
21965
23021
  // src/core/password.ts
21966
23022
  init_paths();
21967
- import * as fs16 from "fs/promises";
23023
+ import * as fs15 from "fs/promises";
21968
23024
  import * as path13 from "path";
21969
- import { randomBytes as randomBytes2, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
23025
+ import { randomBytes as randomBytes3, scrypt, timingSafeEqual as timingSafeEqual2 } from "crypto";
21970
23026
  import { promisify } from "util";
21971
23027
  var scryptAsync = promisify(scrypt);
21972
23028
  function passwordHashPath() {
@@ -21982,7 +23038,7 @@ async function setPassword(plaintext) {
21982
23038
  if (typeof plaintext !== "string" || plaintext.length === 0) {
21983
23039
  throw new Error("password must be a non-empty string");
21984
23040
  }
21985
- const salt = randomBytes2(SALT_LEN);
23041
+ const salt = randomBytes3(SALT_LEN);
21986
23042
  const key = await scryptAsync(plaintext, salt, KEY_LEN, {
21987
23043
  N: DEFAULT_N,
21988
23044
  r: DEFAULT_R,
@@ -21991,15 +23047,15 @@ async function setPassword(plaintext) {
21991
23047
  });
21992
23048
  const encoded = `scrypt$${DEFAULT_N}$${DEFAULT_R}$${DEFAULT_P}$${salt.toString("hex")}$${key.toString("hex")}
21993
23049
  `;
21994
- await fs16.mkdir(paths.home(), { recursive: true });
21995
- await fs16.writeFile(passwordHashPath(), encoded, {
23050
+ await fs15.mkdir(paths.home(), { recursive: true });
23051
+ await fs15.writeFile(passwordHashPath(), encoded, {
21996
23052
  encoding: "utf8",
21997
23053
  mode: 384
21998
23054
  });
21999
23055
  }
22000
23056
  async function hasPassword() {
22001
23057
  try {
22002
- const text = await fs16.readFile(passwordHashPath(), "utf8");
23058
+ const text = await fs15.readFile(passwordHashPath(), "utf8");
22003
23059
  return text.trim().length > 0;
22004
23060
  } catch (err) {
22005
23061
  const e = err;
@@ -22015,7 +23071,7 @@ async function verifyPassword(plaintext) {
22015
23071
  }
22016
23072
  let line;
22017
23073
  try {
22018
- line = (await fs16.readFile(passwordHashPath(), "utf8")).trim();
23074
+ line = (await fs15.readFile(passwordHashPath(), "utf8")).trim();
22019
23075
  } catch (err) {
22020
23076
  const e = err;
22021
23077
  if (e.code === "ENOENT") {
@@ -22142,7 +23198,7 @@ import { nanoid as nanoid2 } from "nanoid";
22142
23198
  import * as os5 from "os";
22143
23199
  import * as path14 from "path";
22144
23200
  init_hydra_version();
22145
- import { randomBytes as randomBytes3 } from "crypto";
23201
+ import { randomBytes as randomBytes4 } from "crypto";
22146
23202
  function registerAcpWsEndpoint(app, deps) {
22147
23203
  app.get("/acp", { websocket: true }, async (socket, request) => {
22148
23204
  const token = tokenFromUpgradeRequest({
@@ -22339,6 +23395,23 @@ function registerAcpWsEndpoint(app, deps) {
22339
23395
  });
22340
23396
  return { childSessionId: child.sessionId };
22341
23397
  });
23398
+ connection.onRequest("hydra-acp/fork_session", async (raw) => {
23399
+ const params = raw ?? {};
23400
+ if (typeof params.sessionId !== "string") {
23401
+ throw Object.assign(
23402
+ new Error("fork_session requires sessionId"),
23403
+ { code: JsonRpcErrorCodes.InvalidParams }
23404
+ );
23405
+ }
23406
+ const forkAt = typeof params.forkAt === "string" ? params.forkAt : void 0;
23407
+ const cwd = typeof params.cwd === "string" ? params.cwd : void 0;
23408
+ const agentId = typeof params.agentId === "string" ? params.agentId : void 0;
23409
+ return await deps.manager.forkSession(params.sessionId, {
23410
+ ...forkAt !== void 0 ? { forkAt } : {},
23411
+ ...cwd !== void 0 ? { cwd } : {},
23412
+ ...agentId !== void 0 ? { agentId } : {}
23413
+ });
23414
+ });
22342
23415
  connection.onRequest("hydra-acp/await_child", async (raw) => {
22343
23416
  const params = raw ?? {};
22344
23417
  const childSessionId = typeof params.childSessionId === "string" ? params.childSessionId : void 0;
@@ -22413,7 +23486,7 @@ function registerAcpWsEndpoint(app, deps) {
22413
23486
  let stdinReservation;
22414
23487
  let augmentedMcpServers = params.mcpServers;
22415
23488
  if (hydraMeta.mcpStdin === true && deps.mcpTokenRegistry !== void 0 && deps.getDaemonOrigin !== void 0) {
22416
- stdinToken = randomBytes3(32).toString("hex");
23489
+ stdinToken = randomBytes4(32).toString("hex");
22417
23490
  stdinReservation = deps.mcpTokenRegistry.reserve(stdinToken);
22418
23491
  const url = `${deps.getDaemonOrigin()}/mcp/hydra-acp-stdin`;
22419
23492
  const descriptor = {
@@ -22431,7 +23504,7 @@ function registerAcpWsEndpoint(app, deps) {
22431
23504
  if (deps.extensionMcp !== void 0 && deps.mcpTokenRegistry !== void 0 && deps.getDaemonOrigin !== void 0) {
22432
23505
  const extNames = deps.extensionMcp.list();
22433
23506
  if (extNames.length > 0) {
22434
- extMcpToken = randomBytes3(32).toString("hex");
23507
+ extMcpToken = randomBytes4(32).toString("hex");
22435
23508
  extMcpReservation = deps.mcpTokenRegistry.reserve(extMcpToken);
22436
23509
  const origin = deps.getDaemonOrigin();
22437
23510
  const descriptors = extNames.map((name) => ({
@@ -23990,12 +25063,13 @@ async function startDaemon(config, serviceToken) {
23990
25063
  await transformers.stop();
23991
25064
  await manager.closeAll();
23992
25065
  await manager.flushMetaWrites();
25066
+ await manager.flushHistoryWrites();
23993
25067
  setBinaryInstallLogger(null);
23994
25068
  setNpmInstallLogger(null);
23995
25069
  setAgentPruneLogger(null);
23996
25070
  await app.close();
23997
25071
  try {
23998
- fs17.unlinkSync(paths.pidFile());
25072
+ fs16.unlinkSync(paths.pidFile());
23999
25073
  } catch {
24000
25074
  }
24001
25075
  try {
@@ -24045,7 +25119,7 @@ init_daemon_bootstrap();
24045
25119
  init_hydra_version();
24046
25120
 
24047
25121
  // src/cli/commands/log-tail.ts
24048
- import * as fs18 from "fs";
25122
+ import * as fs17 from "fs";
24049
25123
  import * as fsp9 from "fs/promises";
24050
25124
  async function runLogTail(logPath, argv, notFoundMessage) {
24051
25125
  const opts = parseLogTailFlags(argv);
@@ -24069,7 +25143,7 @@ async function runLogTail(logPath, argv, notFoundMessage) {
24069
25143
  process.stdout.write(`-- following ${logPath} --
24070
25144
  `);
24071
25145
  let pending = false;
24072
- const watcher = fs18.watch(logPath, () => {
25146
+ const watcher = fs17.watch(logPath, () => {
24073
25147
  if (pending) {
24074
25148
  return;
24075
25149
  }
@@ -24392,7 +25466,7 @@ init_remote_url();
24392
25466
  init_session();
24393
25467
  init_discovery();
24394
25468
  init_hydra_version();
24395
- import * as fs19 from "fs/promises";
25469
+ import * as fs18 from "fs/promises";
24396
25470
  import * as path15 from "path";
24397
25471
  init_session_row();
24398
25472
  async function runSessionsList(opts = {}) {
@@ -24542,8 +25616,8 @@ async function runSessionsExport(id, outPath) {
24542
25616
  return;
24543
25617
  }
24544
25618
  const resolved = outPath === "." ? deriveFilenameFrom(response, id) : outPath;
24545
- await fs19.mkdir(path15.dirname(path15.resolve(resolved)), { recursive: true });
24546
- await fs19.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
25619
+ await fs18.mkdir(path15.dirname(path15.resolve(resolved)), { recursive: true });
25620
+ await fs18.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
24547
25621
  process.stdout.write(`Wrote ${resolved}
24548
25622
  `);
24549
25623
  }
@@ -24590,21 +25664,21 @@ async function runSessionsTranscript(idOrFile, outPath) {
24590
25664
  return;
24591
25665
  }
24592
25666
  const resolved = outPath === "." ? defaultName : outPath;
24593
- await fs19.mkdir(path15.dirname(path15.resolve(resolved)), { recursive: true });
24594
- await fs19.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
25667
+ await fs18.mkdir(path15.dirname(path15.resolve(resolved)), { recursive: true });
25668
+ await fs18.writeFile(resolved, body, { encoding: "utf8", mode: 384 });
24595
25669
  process.stdout.write(`Wrote ${resolved}
24596
25670
  `);
24597
25671
  }
24598
25672
  async function readBundleFileIfExists(arg) {
24599
25673
  try {
24600
- const stat5 = await fs19.stat(arg);
25674
+ const stat5 = await fs18.stat(arg);
24601
25675
  if (!stat5.isFile()) {
24602
25676
  return null;
24603
25677
  }
24604
25678
  } catch {
24605
25679
  return null;
24606
25680
  }
24607
- const text = await fs19.readFile(arg, "utf8");
25681
+ const text = await fs18.readFile(arg, "utf8");
24608
25682
  try {
24609
25683
  return { raw: JSON.parse(text) };
24610
25684
  } catch (err) {
@@ -24633,7 +25707,7 @@ async function runSessionsImport(file, opts = {}) {
24633
25707
  if (opts.cwd !== void 0) {
24634
25708
  const resolved = path15.resolve(opts.cwd);
24635
25709
  try {
24636
- const stat5 = await fs19.stat(resolved);
25710
+ const stat5 = await fs18.stat(resolved);
24637
25711
  if (!stat5.isDirectory()) {
24638
25712
  process.stderr.write(`--cwd ${resolved} is not a directory
24639
25713
  `);
@@ -24650,7 +25724,7 @@ async function runSessionsImport(file, opts = {}) {
24650
25724
  if (file === "-") {
24651
25725
  body = await readStdin();
24652
25726
  } else {
24653
- body = await fs19.readFile(file, "utf8");
25727
+ body = await fs18.readFile(file, "utf8");
24654
25728
  }
24655
25729
  let bundle;
24656
25730
  try {
@@ -26676,14 +27750,15 @@ function renderLine(line, mode) {
26676
27750
  }
26677
27751
  var ANSI_BOLD = "\x1B[1m";
26678
27752
  var ANSI_CODE = "\x1B[96m";
27753
+ var ANSI_BRIGHT_YELLOW = "\x1B[93m";
26679
27754
  var ANSI_RESET = "\x1B[0m";
26680
27755
  var CARET_SENTINEL = "\0";
26681
27756
  function translateMarkup(text, mode) {
26682
27757
  let s = text.replace(/\^\^/g, CARET_SENTINEL);
26683
27758
  if (mode === "ansi") {
26684
- s = s.replace(/\^\+/g, ANSI_BOLD).replace(/\^C/g, ANSI_CODE).replace(/\^:/g, ANSI_RESET);
27759
+ s = s.replace(/\^\+/g, ANSI_BOLD).replace(/\^C/g, ANSI_CODE).replace(/\^Y/g, ANSI_BRIGHT_YELLOW).replace(/\^:/g, ANSI_RESET);
26685
27760
  }
26686
- s = s.replace(/\^[+\-:CcK]/g, "");
27761
+ s = s.replace(/\^[+\-:CcKY]/g, "");
26687
27762
  s = s.replace(/\x00/g, "^");
26688
27763
  return s;
26689
27764
  }
@@ -27730,7 +28805,7 @@ async function resolveSessionFlagOrExit(input, opts) {
27730
28805
  }
27731
28806
  function readVersion() {
27732
28807
  try {
27733
- const here = dirname6(fileURLToPath2(import.meta.url));
28808
+ const here = dirname7(fileURLToPath2(import.meta.url));
27734
28809
  const pkg = JSON.parse(
27735
28810
  readFileSync2(resolve6(here, "../package.json"), "utf8")
27736
28811
  );