@songsid/agend 2.3.4-beta.3 → 2.3.4-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/backend/claude-code.js +1 -1
  2. package/dist/backend/claude-code.js.map +1 -1
  3. package/dist/backend/codex-session-lookup.d.ts +35 -0
  4. package/dist/backend/codex-session-lookup.js +93 -0
  5. package/dist/backend/codex-session-lookup.js.map +1 -1
  6. package/dist/backend/codex.d.ts +27 -0
  7. package/dist/backend/codex.js +116 -20
  8. package/dist/backend/codex.js.map +1 -1
  9. package/dist/daemon.d.ts +9 -0
  10. package/dist/daemon.js +144 -44
  11. package/dist/daemon.js.map +1 -1
  12. package/dist/dialog-screen-text.d.ts +16 -0
  13. package/dist/dialog-screen-text.js +51 -0
  14. package/dist/dialog-screen-text.js.map +1 -0
  15. package/dist/fleet-manager.d.ts +4 -0
  16. package/dist/fleet-manager.js +46 -12
  17. package/dist/fleet-manager.js.map +1 -1
  18. package/dist/install-stream.d.ts +53 -0
  19. package/dist/install-stream.js +134 -0
  20. package/dist/install-stream.js.map +1 -0
  21. package/dist/instance-lifecycle.d.ts +12 -0
  22. package/dist/instance-lifecycle.js +31 -2
  23. package/dist/instance-lifecycle.js.map +1 -1
  24. package/dist/locale.js +26 -6
  25. package/dist/locale.js.map +1 -1
  26. package/dist/log-service.d.ts +2 -0
  27. package/dist/log-service.js +7 -3
  28. package/dist/log-service.js.map +1 -1
  29. package/dist/login-manager.d.ts +6 -0
  30. package/dist/login-manager.js +10 -1
  31. package/dist/login-manager.js.map +1 -1
  32. package/dist/outbound-handlers.js +56 -0
  33. package/dist/outbound-handlers.js.map +1 -1
  34. package/dist/outbound-schemas.d.ts +1 -0
  35. package/dist/outbound-schemas.js +3 -0
  36. package/dist/outbound-schemas.js.map +1 -1
  37. package/dist/selection-card.d.ts +2 -3
  38. package/dist/selection-card.js +99 -26
  39. package/dist/selection-card.js.map +1 -1
  40. package/dist/tmux-manager.d.ts +6 -0
  41. package/dist/tmux-manager.js +17 -0
  42. package/dist/tmux-manager.js.map +1 -1
  43. package/dist/ui/chat-store.js +20 -3
  44. package/dist/ui/fleet-canvas.js +4 -3
  45. package/dist/ui/fleet-office-model.js +55 -22
  46. package/dist/ui/fleet-office-overview.js +6 -38
  47. package/dist/ui/fleet-office-skin.js +12 -4
  48. package/dist/ui/fleet-office-store.js +5 -0
  49. package/dist/ui/fleet-office-zones.js +2 -2
  50. package/dist/ui/fleet-office.js +31 -1
  51. package/dist/ui/panel-log.js +7 -1
  52. package/dist/ui/shared/app-i18n.js +2 -2
  53. package/dist/ui/shared/app-shell.js +4 -2
  54. package/dist/ui/shared/app-stream.js +26 -0
  55. package/dist/ui/shared/app.css +21 -1
  56. package/dist/ui/shared/panel-view.js +185 -10
  57. package/dist/ui/shared/view-strings.js +23 -0
  58. package/dist/unattended-install.d.ts +6 -0
  59. package/dist/unattended-install.js +65 -0
  60. package/dist/unattended-install.js.map +1 -0
  61. package/dist/view-api.d.ts +15 -0
  62. package/dist/view-api.js +94 -33
  63. package/dist/view-api.js.map +1 -1
  64. package/dist/web-api.js +7 -2
  65. package/dist/web-api.js.map +1 -1
  66. package/dist/web-chat-history.d.ts +20 -0
  67. package/dist/web-chat-history.js +21 -0
  68. package/dist/web-chat-history.js.map +1 -1
  69. package/dist/web-guest-access.js +5 -1
  70. package/dist/web-guest-access.js.map +1 -1
  71. package/package.json +1 -1
package/dist/daemon.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { dialogScreenText } from "./dialog-screen-text.js";
1
2
  import { readActiveKiroModel } from "./backend/kiro.js";
2
3
  import { dashboardAccessOf } from "./dashboard-permissions.js";
3
4
  import { sendPackSources } from "./web-office-pack-read.js";
@@ -531,6 +532,9 @@ function countOccurrences(haystack, needle) {
531
532
  }
532
533
  /** Bottom-ready re-poll cadence for Enter-dropping TUIs once the pane is quiet. */
533
534
  const BOTTOM_READY_POLL_MS = 250;
535
+ /** M-2267: how often a pane left in a tmux mode is looked at again, and how long the delivery Enter waits for it to end. */
536
+ const PANE_MODE_POLL_MS = 500;
537
+ const PANE_MODE_ENTER_WAIT_MS = 10 * 60_000;
534
538
  /** Consecutive unreadable pane probes tolerated by the delivery gate before the delivery is failed (≈10s at the poll cadence). */
535
539
  const DIALOG_PROBE_UNKNOWN_MAX = 40;
536
540
  /** Startup dialog scan: poll cadence and wall-clock budget (keys and waits included). */
@@ -3706,7 +3710,9 @@ export class Daemon extends EventEmitter {
3706
3710
  const interactivePrompt = this.interactivePromptDetector.observe(pane, Date.now(), this.instanceStateLastOutputAt);
3707
3711
  if (interactivePrompt) {
3708
3712
  this.logger.warn(interactivePrompt, "Interactive terminal prompt is waiting for human input");
3709
- this.emit("interactive_prompt", { name: this.name, ...interactivePrompt });
3713
+ // M-2269: the prompt as it was on screen, from this capture, for the Log.
3714
+ const screen = dialogScreenText(pane);
3715
+ this.emit("interactive_prompt", { name: this.name, ...interactivePrompt, ...(screen ? { screen } : {}) });
3710
3716
  // A prompt is not an error and must not enter the PTY recovery gate.
3711
3717
  // Continue scanning real errors in this same snapshot.
3712
3718
  }
@@ -3901,7 +3907,7 @@ export class Daemon extends EventEmitter {
3901
3907
  else if (dialog.verifyAfterKeys && !resolved) {
3902
3908
  this.logger.warn({ dialog: dialog.description }, "Safety dialog choice was sent but follow-up verification or notice submission failed");
3903
3909
  }
3904
- this.trackDialogParked(blockingSeen);
3910
+ this.trackDialogParked(blockingSeen, pane);
3905
3911
  return; // Dialog handled (or deliberately deferred): skip error checks this cycle
3906
3912
  }
3907
3913
  // Reached only when no dialog was answered this poll (none on screen, a fenced one, or one waiting out its
@@ -3911,7 +3917,7 @@ export class Daemon extends EventEmitter {
3911
3917
  this.dialogAnswers = null;
3912
3918
  this.retireParkedNotice("answer");
3913
3919
  }
3914
- this.trackDialogParked(blockingSeen);
3920
+ this.trackDialogParked(blockingSeen, pane);
3915
3921
  this.trackRemoteCard(blockingSeen, pane, captureMono);
3916
3922
  if (blockingSeen)
3917
3923
  return; // held dialog: skip error checks this cycle
@@ -7768,6 +7774,9 @@ export class Daemon extends EventEmitter {
7768
7774
  // The hand-off's last capture had no box: nothing was begun or written — wait for readiness, then try again.
7769
7775
  if (written === "handoff-box-unread")
7770
7776
  return "dialog";
7777
+ // M-2267: the pane is in a tmux mode: nothing written — wait for readiness (it holds while the mode lasts), retry.
7778
+ if (written === "pane-in-mode")
7779
+ return "dialog";
7771
7780
  // Fenced before its first write: not attempted — redo after a spawn, else drop without a ❌.
7772
7781
  return written === false && verdict.fenced ? stale() : written;
7773
7782
  });
@@ -8241,7 +8250,7 @@ export class Daemon extends EventEmitter {
8241
8250
  && this.hasAutoDialogClaim(dialog)
8242
8251
  && dialogs.some(other => other !== dialog && Daemon.dialogMatches(other, pane)))
8243
8252
  continue;
8244
- this.trackDialogParked(dialog);
8253
+ this.trackDialogParked(dialog, pane);
8245
8254
  this.trackRemoteCard(dialog, pane, probeMono);
8246
8255
  return { state: "dialog", dialog };
8247
8256
  }
@@ -8620,7 +8629,8 @@ export class Daemon extends EventEmitter {
8620
8629
  this.parkedNoticeOut = null;
8621
8630
  this.emit("dialog_unparked", { name: this.name });
8622
8631
  }
8623
- trackDialogParked(dialog) {
8632
+ /** `pane`: the capture the caller already holds — M-2269 keeps the dialog's text from it for the Log (no new capture). */
8633
+ trackDialogParked(dialog, pane = "") {
8624
8634
  if (!dialog) {
8625
8635
  if (this.dialogParkedSince !== 0)
8626
8636
  this.logger.info({ dialog: this.dialogParkedKey }, "Blocking dialog is gone from the pane");
@@ -8658,7 +8668,9 @@ export class Daemon extends EventEmitter {
8658
8668
  this.dialogParkedReported = true;
8659
8669
  this.parkedNoticeOut = "parked";
8660
8670
  this.logger.warn({ dialog: dialog.description, parkedForMs: Date.now() - this.dialogParkedSince }, "CLI dialog is still on screen — not auto-answering, reporting for a human");
8671
+ const screen = dialogScreenText(pane);
8661
8672
  this.emit("dialog_parked", { name: this.name, description: dialog.description, holdOnly: dialog.holdOnly === true,
8673
+ ...(screen ? { screen } : {}),
8662
8674
  ...(dialog.parkedAdvice ? { advice: dialog.parkedAdvice } : {}),
8663
8675
  ...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}),
8664
8676
  ...(unrecognised ? { unrecognised: true, question: this.unrecognisedPrompt?.shape.question ?? null } : {}) });
@@ -9018,6 +9030,14 @@ export class Daemon extends EventEmitter {
9018
9030
  : await this.waitForPaneIdleForDelivery(windowId, remaining);
9019
9031
  if (idle !== true)
9020
9032
  return idle;
9033
+ // M-2267: a person left the pane in a tmux mode (copy-mode): its keys would go to the mode. Hold — never take the
9034
+ // pane out of it — and look again; the delivery goes once the mode ends (or this wait's own deadline).
9035
+ if (await this.paneInTmuxMode("readiness")) {
9036
+ if (stop && await stop())
9037
+ return "stopped";
9038
+ await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
9039
+ continue;
9040
+ }
9021
9041
  // The idle wait just resolved, so re-reading the silence gate would only
9022
9042
  // add a stale-cache race; ask the pane the remaining questions directly.
9023
9043
  const transient = await this.probeInputTransient();
@@ -9568,6 +9588,38 @@ export class Daemon extends EventEmitter {
9568
9588
  return false;
9569
9589
  return this.sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt, onInputHeld);
9570
9590
  }
9591
+ /**
9592
+ * M-2267 (hmsg-b8a170c0): is the pane in a tmux mode (a person left it in copy-mode)? Logged once per stretch. Unknown
9593
+ * (unreadable) is not a mode: the delivery's own reads decide then, as before.
9594
+ */
9595
+ paneModeSince = 0;
9596
+ async paneInTmuxMode(phase) {
9597
+ const inMode = await this.tmux?.paneInMode?.();
9598
+ if (inMode !== true) {
9599
+ this.paneModeSince = 0;
9600
+ return false;
9601
+ }
9602
+ if (!this.paneModeSince) {
9603
+ this.paneModeSince = performance.now();
9604
+ this.logger.warn({ phase }, "The pane is in a tmux mode (copy-mode) — delivery waits for it to end; no key is sent to it");
9605
+ }
9606
+ return true;
9607
+ }
9608
+ /** M-2267: wait while the pane is in a tmux mode, until `deadline` (PANE_MODE_ENTER_WAIT_MS) — "clear", "stale" (fenced), "held". */
9609
+ async waitWhilePaneInMode(phase, current, deadline = performance.now() + PANE_MODE_ENTER_WAIT_MS) {
9610
+ for (;;) {
9611
+ const inMode = await this.paneInTmuxMode(phase);
9612
+ if (!current())
9613
+ return "stale";
9614
+ if (!inMode)
9615
+ return "clear";
9616
+ if (performance.now() >= deadline)
9617
+ return "held";
9618
+ await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
9619
+ if (!current())
9620
+ return "stale";
9621
+ }
9622
+ }
9571
9623
  /** Write half only: callers must finish the availability wait before any final recovery ownership proof. */
9572
9624
  async sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt, onInputHeld) {
9573
9625
  // A recovery Enter may have waited for a transient while cancel or spawn
@@ -9589,47 +9641,75 @@ export class Daemon extends EventEmitter {
9589
9641
  this.logger.warn({ phase }, "Delivery input is not a verified composer; Enter withheld");
9590
9642
  return false;
9591
9643
  };
9592
- let pane;
9593
- try {
9594
- pane = await tmux.capturePane();
9595
- }
9596
- catch {
9597
- return current() ? hold() : false;
9598
- }
9599
- if (!current())
9600
- return false;
9601
- let composer = false;
9602
- try {
9603
- if (this.runtimeDialogs().some(dialog => Daemon.dialogMatches(dialog, pane)))
9644
+ // M-2267 (#2971 review, Prism P2): a pane in a tmux mode would take this Enter. Wait for the mode to end FIRST, then
9645
+ // prove the composer (and that no dialog took its place) on a capture taken after it — and again whenever the pane
9646
+ // went back into a mode meanwhile: the Enter only ever goes on a proof newer than the last mode. Never ended for the
9647
+ // person, never answering a dialog that appeared during the wait (hold() then, as for any).
9648
+ const modeDeadline = performance.now() + PANE_MODE_ENTER_WAIT_MS;
9649
+ for (;;) {
9650
+ const mode = await this.waitWhilePaneInMode(phase, current, modeDeadline);
9651
+ if (mode === "stale")
9652
+ return false;
9653
+ if (mode === "held")
9604
9654
  return hold();
9605
- if (backend?.readInputRow)
9606
- composer = backend.readInputRow(pane) != null;
9607
- else if (backend?.inputDraft)
9608
- composer = backend.inputDraft(pane) != null;
9609
- else if (backend?.isDeliveryInputReadyPane)
9610
- composer = this.deliveryInputReadyPane(pane);
9611
- else if (backend?.readSteerComposer?.(pane) != null)
9612
- composer = true;
9613
- else if (backend) {
9614
- // Legacy backends still need their own current ready chrome, never a menu's cursor or old transcript row.
9615
- const shape = unrecognisedPromptShape(pane);
9616
- const ownComposer = backend.isDeliveryComposerPane?.(pane) === true;
9617
- if ((!selectionCard(pane, "CLI") || ownComposer) && (!shape || ownComposer || this.cliComposerShowing(pane, shape))) {
9618
- const bottom = backend.getBottomReadyPattern?.();
9619
- const ready = backend.getReadyPattern();
9620
- ready.lastIndex = 0;
9621
- composer = ownComposer || (bottom ? bottomRowIsReady(pane, bottom)
9622
- : ready.test(pane.trimEnd().split("\n").slice(-COMPOSER_TAIL_ROWS).join("\n")));
9655
+ let pane;
9656
+ try {
9657
+ pane = await tmux.capturePane();
9658
+ }
9659
+ catch {
9660
+ return current() ? hold() : false;
9661
+ }
9662
+ if (!current())
9663
+ return false;
9664
+ let composer = false;
9665
+ try {
9666
+ if (this.runtimeDialogs().some(dialog => Daemon.dialogMatches(dialog, pane)))
9667
+ return hold();
9668
+ if (backend?.readInputRow)
9669
+ composer = backend.readInputRow(pane) != null;
9670
+ else if (backend?.inputDraft)
9671
+ composer = backend.inputDraft(pane) != null;
9672
+ else if (backend?.isDeliveryInputReadyPane)
9673
+ composer = this.deliveryInputReadyPane(pane);
9674
+ else if (backend?.readSteerComposer?.(pane) != null)
9675
+ composer = true;
9676
+ else if (backend) {
9677
+ // Legacy backends still need their own current ready chrome, never a menu's cursor or old transcript row.
9678
+ const shape = unrecognisedPromptShape(pane);
9679
+ const ownComposer = backend.isDeliveryComposerPane?.(pane) === true;
9680
+ if ((!selectionCard(pane, "CLI") || ownComposer) && (!shape || ownComposer || this.cliComposerShowing(pane, shape))) {
9681
+ const bottom = backend.getBottomReadyPattern?.();
9682
+ const ready = backend.getReadyPattern();
9683
+ ready.lastIndex = 0;
9684
+ composer = ownComposer || (bottom ? bottomRowIsReady(pane, bottom)
9685
+ : ready.test(pane.trimEnd().split("\n").slice(-COMPOSER_TAIL_ROWS).join("\n")));
9686
+ }
9623
9687
  }
9624
9688
  }
9689
+ catch {
9690
+ return hold();
9691
+ }
9692
+ if (!composer)
9693
+ return hold();
9694
+ if (!current())
9695
+ return false;
9696
+ // The proof stands only if the pane did not go into a mode while it was made: else wait, and prove again — within
9697
+ // the same deadline (#2971 re-review, Prism P2: a pane flickering in and out of a mode must not hold the write lock
9698
+ // past it), and never spinning: one poll interval before the next look.
9699
+ if (await this.paneInTmuxMode(phase)) {
9700
+ if (!current())
9701
+ return false;
9702
+ if (performance.now() >= modeDeadline)
9703
+ return hold();
9704
+ await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
9705
+ if (!current())
9706
+ return false;
9707
+ continue;
9708
+ }
9709
+ if (!current())
9710
+ return false;
9711
+ break;
9625
9712
  }
9626
- catch {
9627
- return hold();
9628
- }
9629
- if (!composer)
9630
- return hold();
9631
- if (!current())
9632
- return false;
9633
9713
  // W1: persist the "Enter may have happened" boundary before tmux receives
9634
9714
  // the key. A crash after this commit is never auto-replayed without a
9635
9715
  // positive transcript marker, even if the stale pane still shows composer.
@@ -9697,6 +9777,10 @@ export class Daemon extends EventEmitter {
9697
9777
  verdict.fenced = !verdict.durableBeginCommitted;
9698
9778
  return false;
9699
9779
  }
9780
+ // M-2267: the pane went into a tmux mode after the readiness wait. Nothing begun or written: the caller waits for
9781
+ // readiness (which holds while it lasts) and tries again — as for a dialog that appeared before the write.
9782
+ if (!verdict.durableBeginCommitted && await this.paneInTmuxMode("paste"))
9783
+ return "pane-in-mode";
9700
9784
  // A hand-off pastes and presses Enter into a busy pane: the box must still be readable on this, the last capture
9701
9785
  // before the write (#1169 review) — a modal that appeared after the under-lock check would take that Enter. Before
9702
9786
  // the durable begin the caller waits for readiness outside the lock and tries again; after it (a paste retry),
@@ -9793,6 +9877,22 @@ export class Daemon extends EventEmitter {
9793
9877
  // failed/throwing write as uncertain rather than retrying bytes into the
9794
9878
  // same pane; the write-ahead Enter marker later separates safe pre-Enter
9795
9879
  // recovery from a possibly executed command.
9880
+ // M-2267 (#2971 review, Prism P2): EVERY paste attempt — a retry after a failed paste, a first paste whose durable
9881
+ // begin already went — asks too. Before the begin: nothing begun, the caller waits for readiness and tries again.
9882
+ // After it: this attempt waits here, keeping its durable attempt; a mode outlasting the wait fails it unwritten.
9883
+ if (!verdict.durableBeginCommitted) {
9884
+ if (await this.paneInTmuxMode("paste"))
9885
+ return "pane-in-mode";
9886
+ }
9887
+ else {
9888
+ const mode = await this.waitWhilePaneInMode("paste", () => (!stillCurrent || stillCurrent()) && !this.deliveryWritesStopping);
9889
+ if (mode === "stale") {
9890
+ verdict.fenced = false;
9891
+ return false;
9892
+ }
9893
+ if (mode === "held")
9894
+ return this.failDelivery(verdict, status, "paste", "pane-in-tmux-mode");
9895
+ }
9796
9896
  const pasteGeneration = this.spawnGeneration;
9797
9897
  beforePaste?.();
9798
9898
  const pasted = await this.tmux.pasteBuffer(formatted);
@@ -12960,7 +13060,7 @@ export class Daemon extends EventEmitter {
12960
13060
  // ones included: if the auto-dismiss keeps failing here, the human
12961
13061
  // report should count from first sight, not from the runtime scan.
12962
13062
  if (!dialog.fatal && this.deliveryBlockingDialogs().some(d => Daemon.dialogKey(d) === Daemon.dialogKey(dialog))) {
12963
- this.trackDialogParked(dialog);
13063
+ this.trackDialogParked(dialog, pane);
12964
13064
  }
12965
13065
  // A fatal startup screen cannot be dismissed by keypresses (e.g. a
12966
13066
  // corrupt claude.json modal offering only "exit" / "reset config").
@@ -12990,7 +13090,7 @@ export class Daemon extends EventEmitter {
12990
13090
  this.heldReasonLoggedKey = Daemon.dialogKey(dialog);
12991
13091
  this.logger.warn({ description: dialog.description, ...reason }, "Startup dialog held — not auto-answering, deliveries stay blocked");
12992
13092
  }
12993
- this.trackDialogParked(dialog);
13093
+ this.trackDialogParked(dialog, pane);
12994
13094
  matched = true;
12995
13095
  break;
12996
13096
  }