@xmoon76/dsh-pi-tui 0.4.7-alpha.1 → 0.4.7-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +3 -3
- package/README.md +3 -3
- package/cordis.patch.yml +20 -1
- package/dist/builtins.mjs +2 -2
- package/dist/extensions.mjs +1 -2
- package/dist/index.d.mts +4 -10
- package/dist/index.mjs +2 -3
- package/dist/{src-D-4-xTUd.mjs → src-5aY9svZj.mjs} +1264 -685
- package/dist/startup-CGeJ1k1B.mjs +284 -0
- package/dist/startup.d.mts +19 -8
- package/dist/startup.mjs +1 -237
- package/package.json +108 -100
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TUI_STARTUP_SERVICE } from "./startup.mjs";
|
|
1
|
+
import { n as TUI_STARTUP_SERVICE } from "./startup-CGeJ1k1B.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { StringDecoder } from "node:string_decoder";
|
|
@@ -65,7 +65,8 @@ async function resolvePresetRequest(presets, requestedId, signal) {
|
|
|
65
65
|
//#endregion
|
|
66
66
|
//#region src/runtime/direct/session-preset-direct.ts
|
|
67
67
|
/**
|
|
68
|
-
* Direct session-preset adapter for DSH 0.1.6-
|
|
68
|
+
* Direct session-preset adapter for the DSH 0.1.6 session-projection API
|
|
69
|
+
* (introduced in `0.1.6-alpha.1`).
|
|
69
70
|
*
|
|
70
71
|
* The Harness owns the `agentPreset` projection. TUI callers must not fold the
|
|
71
72
|
* session log themselves: the projection initializes from the creation header
|
|
@@ -5867,6 +5868,19 @@ function invokeTask(task) {
|
|
|
5867
5868
|
}
|
|
5868
5869
|
}
|
|
5869
5870
|
/**
|
|
5871
|
+
* Observe the settlement of an ALREADY-OWNED promise without taking ownership.
|
|
5872
|
+
* The callback runs on fulfilment or rejection and its own result is dropped,
|
|
5873
|
+
* so bookkeeping (for example removing a promise from a teardown-drained set)
|
|
5874
|
+
* never adds a second, unhandled rejection branch next to the real owner's
|
|
5875
|
+
* failure handling. Never use this to START work — that is `runDetached` /
|
|
5876
|
+
* `runOwned`.
|
|
5877
|
+
* @param promise - the promise whose failure semantics already have an owner.
|
|
5878
|
+
* @param onSettled - runs once, on either settlement.
|
|
5879
|
+
*/
|
|
5880
|
+
function observeSettled(promise, onSettled) {
|
|
5881
|
+
promise.then(onSettled, onSettled);
|
|
5882
|
+
}
|
|
5883
|
+
/**
|
|
5870
5884
|
* Attach rejection handling to a fire-and-forget task. The factory is
|
|
5871
5885
|
* invoked synchronously (a synchronous throw is a rejection like any
|
|
5872
5886
|
* other); every rejection is caught and classified:
|
|
@@ -19746,6 +19760,7 @@ var TuiAltScreen = class extends TuiBase {
|
|
|
19746
19760
|
onRightClickPaste;
|
|
19747
19761
|
copyOnSelect;
|
|
19748
19762
|
copySelection;
|
|
19763
|
+
selectionLineText;
|
|
19749
19764
|
onCellClick;
|
|
19750
19765
|
onCellPress;
|
|
19751
19766
|
onFramePainted;
|
|
@@ -19778,6 +19793,7 @@ var TuiAltScreen = class extends TuiBase {
|
|
|
19778
19793
|
this.onRightClickPaste = options.onRightClickPaste;
|
|
19779
19794
|
this.copyOnSelect = options.copyOnSelect ?? true;
|
|
19780
19795
|
this.copySelection = options.copySelection;
|
|
19796
|
+
this.selectionLineText = options.selectionLineText;
|
|
19781
19797
|
this.onCellClick = options.onCellClick;
|
|
19782
19798
|
this.onCellPress = options.onCellPress;
|
|
19783
19799
|
this.onFramePainted = options.onFramePainted;
|
|
@@ -20957,8 +20973,14 @@ var TuiAltScreen = class extends TuiBase {
|
|
|
20957
20973
|
const lines = [];
|
|
20958
20974
|
for (let row = selection.start.row; row <= selection.end.row; row++) {
|
|
20959
20975
|
const line = sourceLines[row] ?? "";
|
|
20976
|
+
const override = this.selectionLineText?.({
|
|
20977
|
+
row,
|
|
20978
|
+
line,
|
|
20979
|
+
...selection.start.scrollView === void 0 ? {} : { scrollView: selection.start.scrollView }
|
|
20980
|
+
});
|
|
20981
|
+
const copyLine = override === void 0 ? line : override;
|
|
20960
20982
|
const columns = this.getSelectionColumns(line, row, selection);
|
|
20961
|
-
const sliced = stripTerminalSequences(sliceByColumn(
|
|
20983
|
+
const sliced = stripTerminalSequences(sliceByColumn(copyLine, columns.start, Math.max(0, columns.end - columns.start), true)).trimEnd();
|
|
20962
20984
|
lines.push(columns.start === 0 ? sliced.replace(/^ {1,3}/, "") : sliced);
|
|
20963
20985
|
}
|
|
20964
20986
|
const text = lines.join("\n");
|
|
@@ -40848,6 +40870,7 @@ var UserBubbleComponent = class {
|
|
|
40848
40870
|
lastWidth = -1;
|
|
40849
40871
|
cached;
|
|
40850
40872
|
lastCompactMarkerRow;
|
|
40873
|
+
lastCollapseEligible = false;
|
|
40851
40874
|
constructor(child, marker, bg, compactOptions) {
|
|
40852
40875
|
this.child = child;
|
|
40853
40876
|
this.marker = marker;
|
|
@@ -40868,6 +40891,13 @@ var UserBubbleComponent = class {
|
|
|
40868
40891
|
compactMarkerRow() {
|
|
40869
40892
|
return this.lastCompactMarkerRow;
|
|
40870
40893
|
}
|
|
40894
|
+
/** Whether the current render is an EXPANDED compact-capable bubble: the
|
|
40895
|
+
* visual rows exceed the threshold, so the caller must place a tail
|
|
40896
|
+
* collapse control after the body (the spacer row when one follows, or one
|
|
40897
|
+
* dedicated presentation row for the final block). */
|
|
40898
|
+
showsCollapseControl() {
|
|
40899
|
+
return this.lastCollapseEligible;
|
|
40900
|
+
}
|
|
40871
40901
|
render(width) {
|
|
40872
40902
|
const inner = Math.max(1, width - this.markerWidth);
|
|
40873
40903
|
const child = this.child.render(inner);
|
|
@@ -40886,11 +40916,17 @@ var UserBubbleComponent = class {
|
|
|
40886
40916
|
/** Collapse the child's FULL visual rows to head + marker + tail when the
|
|
40887
40917
|
* row-count threshold is exceeded. The decision and the slice both run on
|
|
40888
40918
|
* the rows the current width actually produces, so a resize re-decides
|
|
40889
|
-
* (no baked compact count/marker position).
|
|
40919
|
+
* (no baked compact count/marker position). An EXPANDED bubble keeps the
|
|
40920
|
+
* full rows and only records that it is collapse-eligible. */
|
|
40890
40921
|
compactRows(child, inner) {
|
|
40891
40922
|
this.lastCompactMarkerRow = void 0;
|
|
40923
|
+
this.lastCollapseEligible = false;
|
|
40892
40924
|
const options = this.compactOptions;
|
|
40893
40925
|
if (options === void 0 || child.length <= options.thresholdRows) return child;
|
|
40926
|
+
if (options.expanded) {
|
|
40927
|
+
this.lastCollapseEligible = true;
|
|
40928
|
+
return child;
|
|
40929
|
+
}
|
|
40894
40930
|
const head = Math.min(options.headRows, child.length);
|
|
40895
40931
|
const tail = Math.min(options.tailRows, Math.max(0, child.length - head));
|
|
40896
40932
|
if (head + tail >= child.length) return child;
|
|
@@ -40909,15 +40945,36 @@ var UserBubbleComponent = class {
|
|
|
40909
40945
|
* The ephemeral pending user-input row: the SAME floating user bubble as a
|
|
40910
40946
|
* durable human message, plus one dim pending-status line so it reads as
|
|
40911
40947
|
* accepted-but-not-yet-materialized rather than as durable transcript content.
|
|
40912
|
-
* `
|
|
40913
|
-
* transcript folder.
|
|
40914
|
-
|
|
40915
|
-
|
|
40916
|
-
|
|
40917
|
-
|
|
40918
|
-
container
|
|
40919
|
-
|
|
40920
|
-
|
|
40948
|
+
* `PendingUserComponent` is presentation-only and is never inserted into the
|
|
40949
|
+
* transcript folder. It exposes the same disclosure geometry as the durable
|
|
40950
|
+
* bubble (the compact marker row and the expanded collapse-control
|
|
40951
|
+
* eligibility) so the row map shares ONE hit model for both.
|
|
40952
|
+
*/
|
|
40953
|
+
var PendingUserComponent = class {
|
|
40954
|
+
container;
|
|
40955
|
+
bubble;
|
|
40956
|
+
constructor(row, running, compactOptions) {
|
|
40957
|
+
this.bubble = new UserBubbleComponent(new Text(row.text, 0, 0), `${color.roleUser("❯")} `, color.roleUserBg, compactOptions);
|
|
40958
|
+
this.container = new Container();
|
|
40959
|
+
this.container.addChild(this.bubble);
|
|
40960
|
+
this.container.addChild(new Text(color.textDim(` ${pendingUserStatusText(row, running)}`), 0, 0));
|
|
40961
|
+
}
|
|
40962
|
+
render(width) {
|
|
40963
|
+
return this.container.render(width);
|
|
40964
|
+
}
|
|
40965
|
+
invalidate() {
|
|
40966
|
+
this.container.invalidate?.();
|
|
40967
|
+
}
|
|
40968
|
+
dispose() {
|
|
40969
|
+
this.container.dispose?.();
|
|
40970
|
+
}
|
|
40971
|
+
compactMarkerRow() {
|
|
40972
|
+
return this.bubble.compactMarkerRow();
|
|
40973
|
+
}
|
|
40974
|
+
showsCollapseControl() {
|
|
40975
|
+
return this.bubble.showsCollapseControl();
|
|
40976
|
+
}
|
|
40977
|
+
};
|
|
40921
40978
|
/**
|
|
40922
40979
|
* The presentation-only pending status label. An accepted steer whose subject
|
|
40923
40980
|
* is RUNNING reads `steering…`. When an AUTHORITATIVE steering occurrence's
|
|
@@ -41264,6 +41321,14 @@ function approvalOverlayGeometry(columns, rows) {
|
|
|
41264
41321
|
contentWidth: Math.max(1, width - 8)
|
|
41265
41322
|
};
|
|
41266
41323
|
}
|
|
41324
|
+
/** The stable presentation identity of one pending-user row: the rpc
|
|
41325
|
+
* correlation when present (a local echo and its authoritative occurrence
|
|
41326
|
+
* share it), the occurrence/request id otherwise. Never text, and never the
|
|
41327
|
+
* entry index — a projection update must not transfer disclosure state (or a
|
|
41328
|
+
* stale click) to a different pending row. */
|
|
41329
|
+
function pendingUserDisclosureKey(row) {
|
|
41330
|
+
return row.rpcId !== void 0 ? `rpc:${row.rpcId}` : `id:${row.id}`;
|
|
41331
|
+
}
|
|
41267
41332
|
/**
|
|
41268
41333
|
* The interactive surface: header, transcript, editor, footer. Owns the
|
|
41269
41334
|
* TUI lifecycle, mode switching, folding, approval dialogs, and settings
|
|
@@ -41292,6 +41357,14 @@ function deepFreeze(value) {
|
|
|
41292
41357
|
for (const key of Object.keys(value)) deepFreeze(value[key]);
|
|
41293
41358
|
return Object.freeze(value);
|
|
41294
41359
|
}
|
|
41360
|
+
/** The Host-owned long-user disclosure geometry. Only the two Host bubbles
|
|
41361
|
+
* implement it, and the row map never reaches into their children. The brand
|
|
41362
|
+
* is EXPLICIT (`instanceof`): a plugin renderer that happens to expose
|
|
41363
|
+
* same-named methods must never be mistaken for a Host disclosure component
|
|
41364
|
+
* (the M7 plugin-owned `kind: 'user'` contract). */
|
|
41365
|
+
function userDisclosureComponentOf(component) {
|
|
41366
|
+
if (component instanceof UserBubbleComponent || component instanceof PendingUserComponent) return component;
|
|
41367
|
+
}
|
|
41295
41368
|
var TuiApp = class TuiApp {
|
|
41296
41369
|
terminal;
|
|
41297
41370
|
/** The extension surface host (M2), when the runner attached one. */
|
|
@@ -41763,6 +41836,11 @@ var TuiApp = class TuiApp {
|
|
|
41763
41836
|
* regardless of the fold or the override.
|
|
41764
41837
|
*/
|
|
41765
41838
|
expandedOverride = /* @__PURE__ */ new Map();
|
|
41839
|
+
/** Per-pending-row long-user disclosure overrides, keyed by the stable
|
|
41840
|
+
* {@link pendingUserDisclosureKey}. Presentation-only ephemeral state: it
|
|
41841
|
+
* never enters the transcript folder, persistence, or the session, and it
|
|
41842
|
+
* is pruned to the live pending keys on every presentation update. */
|
|
41843
|
+
pendingUserExpanded = /* @__PURE__ */ new Map();
|
|
41766
41844
|
/** PTC sub-call bodies the user expanded by subCallId (stable across
|
|
41767
41845
|
* live updates, replay and sibling insertion — never an array index). */
|
|
41768
41846
|
subCallExpanded = /* @__PURE__ */ new Set();
|
|
@@ -42346,6 +42424,7 @@ var TuiApp = class TuiApp {
|
|
|
42346
42424
|
}
|
|
42347
42425
|
this.terminalSchemeListeners.clear();
|
|
42348
42426
|
this.expandedOverride.clear();
|
|
42427
|
+
this.pendingUserExpanded.clear();
|
|
42349
42428
|
this.disposeMessageComponents();
|
|
42350
42429
|
this.localMessages.length = 0;
|
|
42351
42430
|
this.pendingUserRows = [];
|
|
@@ -42859,19 +42938,19 @@ var TuiApp = class TuiApp {
|
|
|
42859
42938
|
toggleTranscriptExpand: () => {
|
|
42860
42939
|
if (this.fullscreen !== void 0 && this.focusModeEnabled) {
|
|
42861
42940
|
if (this.hasVisibleExpandedFocusRoots()) {
|
|
42862
|
-
this.
|
|
42941
|
+
this.clearUserDisclosureOverrides();
|
|
42863
42942
|
this.toggleFullscreenFocusRoots();
|
|
42864
|
-
} else if (this.
|
|
42865
|
-
|
|
42866
|
-
this.rebuildMessages();
|
|
42867
|
-
} else this.toggleFullscreenFocusRoots();
|
|
42943
|
+
} else if (this.hasVisibleExpandedUserDisclosure()) this.mutateTranscriptDisclosure(() => this.clearUserDisclosureOverrides());
|
|
42944
|
+
else this.toggleFullscreenFocusRoots();
|
|
42868
42945
|
return true;
|
|
42869
42946
|
}
|
|
42870
|
-
if (this.toolOutputExpanded || this.
|
|
42947
|
+
if (this.toolOutputExpanded || this.hasVisibleExpandedUserDisclosure()) this.mutateTranscriptDisclosure(() => {
|
|
42871
42948
|
this.toolOutputExpanded = false;
|
|
42872
|
-
this.
|
|
42873
|
-
}
|
|
42874
|
-
this.
|
|
42949
|
+
this.clearUserDisclosureOverrides();
|
|
42950
|
+
});
|
|
42951
|
+
else this.mutateTranscriptDisclosure(() => {
|
|
42952
|
+
this.toolOutputExpanded = true;
|
|
42953
|
+
});
|
|
42875
42954
|
return true;
|
|
42876
42955
|
},
|
|
42877
42956
|
toggleThinking: () => {
|
|
@@ -43271,7 +43350,7 @@ var TuiApp = class TuiApp {
|
|
|
43271
43350
|
this.clearExitConfirmation();
|
|
43272
43351
|
this.keybindings.cancelLeader();
|
|
43273
43352
|
if (enabled === (this.fullscreen !== void 0)) return;
|
|
43274
|
-
if (enabled && !this.userDisclosureAffordanceAvailable()) this.
|
|
43353
|
+
if (enabled && !this.userDisclosureAffordanceAvailable()) this.clearUserDisclosureOverrides();
|
|
43275
43354
|
this.clearFocusLiveHeightState();
|
|
43276
43355
|
this.disposeTrackedKeybindingEditors();
|
|
43277
43356
|
for (const close of [...this.footerConfiguratorClosers]) close();
|
|
@@ -43309,6 +43388,7 @@ var TuiApp = class TuiApp {
|
|
|
43309
43388
|
shouldShowScrollToEndIndicator: () => this.transcriptWindow?.mode === "history",
|
|
43310
43389
|
onScrollToEndIndicator: () => this.events.onTranscriptJumpLatest?.() === true,
|
|
43311
43390
|
copySelection: this.copySelection,
|
|
43391
|
+
selectionLineText: (context) => this.selectionLineText(context),
|
|
43312
43392
|
openUrl: this.openExternalUrl,
|
|
43313
43393
|
onRightClickPaste: this.readClipboardText === void 0 ? void 0 : () => {
|
|
43314
43394
|
this.rightClickPasteFromClipboard();
|
|
@@ -43725,30 +43805,38 @@ var TuiApp = class TuiApp {
|
|
|
43725
43805
|
}
|
|
43726
43806
|
return entry.rendererId === void 0;
|
|
43727
43807
|
}
|
|
43728
|
-
/** Whether the CURRENT projection shows a long user
|
|
43808
|
+
/** Whether the CURRENT projection shows a long user disclosure that is
|
|
43729
43809
|
* ACTUALLY compacted and explicitly expanded (the Ctrl+O user-collapse
|
|
43730
|
-
* target, plan §6.4/§21.7)
|
|
43731
|
-
*
|
|
43732
|
-
* override on a
|
|
43733
|
-
*
|
|
43734
|
-
*
|
|
43735
|
-
|
|
43810
|
+
* target, plan §6.4/§21.7) — a durable message OR an ephemeral pending row.
|
|
43811
|
+
* Only VISIBLE, HOST-rendered, compact-capable entries count: a parked
|
|
43812
|
+
* override on a windowed-away message, a stale override on a
|
|
43813
|
+
* short/resized-short bubble with no visible effect, or an override on a
|
|
43814
|
+
* plugin-owned presentation must not consume the Ctrl+O press and wedge the
|
|
43815
|
+
* toggle into a no-op. */
|
|
43816
|
+
hasVisibleExpandedUserDisclosure() {
|
|
43736
43817
|
for (const message of this.messages) {
|
|
43737
43818
|
if (!isUserMessageDisclosureCandidate(message)) continue;
|
|
43738
43819
|
if (this.expandedOverride.get(message) !== true) continue;
|
|
43739
43820
|
if (!this.isHostUserDisclosure(message)) continue;
|
|
43740
43821
|
if (this.userMessageCompactsAtCurrentWidth(message)) return true;
|
|
43741
43822
|
}
|
|
43823
|
+
for (const row of this.pendingUserRows) {
|
|
43824
|
+
if (row.foldableText !== true) continue;
|
|
43825
|
+
if (this.pendingUserExpanded.get(pendingUserDisclosureKey(row)) !== true) continue;
|
|
43826
|
+
if (this.pendingUserCompactsAtCurrentWidth(row)) return true;
|
|
43827
|
+
}
|
|
43742
43828
|
return false;
|
|
43743
43829
|
}
|
|
43744
43830
|
/** Clear every long-user disclosure override (the Ctrl+O user-collapse
|
|
43745
|
-
* pass)
|
|
43746
|
-
*
|
|
43747
|
-
|
|
43831
|
+
* pass): durable message overrides AND ephemeral pending-row overrides. It
|
|
43832
|
+
* filters by the user-disclosure classification ONLY — thinking, tool,
|
|
43833
|
+
* system and compaction overrides are never touched. */
|
|
43834
|
+
clearUserDisclosureOverrides() {
|
|
43748
43835
|
for (const message of [...this.expandedOverride.keys()]) {
|
|
43749
43836
|
if (!isUserMessageDisclosureCandidate(message)) continue;
|
|
43750
43837
|
this.expandedOverride.delete(message);
|
|
43751
43838
|
}
|
|
43839
|
+
this.pendingUserExpanded.clear();
|
|
43752
43840
|
}
|
|
43753
43841
|
/** The eligible Focus roots for Ctrl+O bulk expansion: turns with a
|
|
43754
43842
|
* real TurnActivity that are CURRENTLY PROJECTED (present in the folded
|
|
@@ -43925,6 +44013,27 @@ var TuiApp = class TuiApp {
|
|
|
43925
44013
|
const welcomeHeight = this.welcomeCard.render(width).length;
|
|
43926
44014
|
this.fullscreenScroll.scrollTo(welcomeHeight + transcriptRow - 1, { disableFollow: true });
|
|
43927
44015
|
}
|
|
44016
|
+
/** The single long-user disclosure transition (plan §10): capture the
|
|
44017
|
+
* fullscreen viewport intent, mutate the disclosure state, rebuild once, and
|
|
44018
|
+
* restore the SAME visual intent. A user who was following the live tail
|
|
44019
|
+
* keeps following it (never a historical browse); a historical reader is
|
|
44020
|
+
* restored to the SAME semantic row (durable message identity or the stable
|
|
44021
|
+
* pending key) and viewport offset — never a raw delta-height scrollTop.
|
|
44022
|
+
* Regular mode (no fullscreen scroll view) just rebuilds. Focus root
|
|
44023
|
+
* transitions own their own `anchor-turn` contract and never route here. */
|
|
44024
|
+
mutateTranscriptDisclosure(mutate) {
|
|
44025
|
+
const scroll = this.fullscreenScroll;
|
|
44026
|
+
const wasFollowingEnd = scroll?.isFollowingEnd === true;
|
|
44027
|
+
const anchor = scroll === void 0 || wasFollowingEnd ? void 0 : this.captureTranscriptViewportAnchor();
|
|
44028
|
+
mutate();
|
|
44029
|
+
this.clearFocusLiveHeightState();
|
|
44030
|
+
this.rebuildMessages();
|
|
44031
|
+
if (scroll !== void 0) {
|
|
44032
|
+
if (wasFollowingEnd) this.applyFullscreenFollowEndViewport();
|
|
44033
|
+
else if (anchor !== void 0) this.restoreTranscriptViewportAnchor(anchor, "top");
|
|
44034
|
+
}
|
|
44035
|
+
this.requestRender();
|
|
44036
|
+
}
|
|
43928
44037
|
/** The projected transcript row (in `messageRows` coordinates, welcome
|
|
43929
44038
|
* card excluded) where a turn's Thought header starts. Returns undefined
|
|
43930
44039
|
* when the turn is not currently projected (e.g. Focus off, or the turn
|
|
@@ -44176,7 +44285,7 @@ var TuiApp = class TuiApp {
|
|
|
44176
44285
|
let attachments = [];
|
|
44177
44286
|
let subCallHits;
|
|
44178
44287
|
let workflowHits;
|
|
44179
|
-
let
|
|
44288
|
+
let userDisclosureHit;
|
|
44180
44289
|
const collapseFocusOwnerOnClick = this.focusOwnerForRenderBlock(block);
|
|
44181
44290
|
if (block.kind === "activity") {
|
|
44182
44291
|
component = this.focusActivityComponentFor(block.activity, projectionExpanded.has(block.activity.turn), this.focusToolDisplayFor(block.activity), this.focusModeEnabled && !projectionExpanded.has(block.activity.turn) ? focusPreparingSummary(this.streamingToolPreviewsForTurn(block.activity.turn)) : void 0);
|
|
@@ -44185,14 +44294,21 @@ var TuiApp = class TuiApp {
|
|
|
44185
44294
|
component = this.streamingToolPreviewComponent(block.previews, width);
|
|
44186
44295
|
rendered = component.render(width);
|
|
44187
44296
|
} else if (block.kind === "pending-user") {
|
|
44188
|
-
component =
|
|
44297
|
+
component = this.pendingUserComponentFor(block.row);
|
|
44189
44298
|
rendered = component.render(width);
|
|
44299
|
+
userDisclosureHit = this.userDisclosureHitFor(component, rendered, truncatedMarker, {
|
|
44300
|
+
kind: "pending",
|
|
44301
|
+
key: pendingUserDisclosureKey(block.row)
|
|
44302
|
+
});
|
|
44190
44303
|
} else {
|
|
44191
44304
|
component = this.componentForMessage(block.message, boundary, width, userBoundary);
|
|
44192
44305
|
rendered = component.render(width);
|
|
44193
44306
|
truncatedMarker = block.truncated === true;
|
|
44194
44307
|
attachments = this.attachmentRangesOf(component, width);
|
|
44195
|
-
if (
|
|
44308
|
+
if (isUserMessageDisclosureCandidate(block.message)) userDisclosureHit = this.userDisclosureHitFor(component, rendered, truncatedMarker, {
|
|
44309
|
+
kind: "durable",
|
|
44310
|
+
message: block.message
|
|
44311
|
+
});
|
|
44196
44312
|
const subCallInfo = this.subCallHitsByMessage.get(block.message);
|
|
44197
44313
|
subCallHits = subCallInfo === void 0 ? void 0 : subCallInfo.hits.map((hit) => ({
|
|
44198
44314
|
...hit,
|
|
@@ -44213,10 +44329,36 @@ var TuiApp = class TuiApp {
|
|
|
44213
44329
|
...collapseFocusOwnerOnClick === void 0 ? {} : { collapseFocusOwnerOnClick },
|
|
44214
44330
|
...subCallHits === void 0 ? {} : { subCallHits },
|
|
44215
44331
|
...workflowHits === void 0 ? {} : { workflowHits },
|
|
44216
|
-
...
|
|
44332
|
+
...userDisclosureHit === void 0 ? {} : { userDisclosureHit }
|
|
44217
44333
|
};
|
|
44218
44334
|
});
|
|
44219
44335
|
}
|
|
44336
|
+
/** The ONE bidirectional disclosure control of a long-user bubble (durable
|
|
44337
|
+
* or pending): the compact marker while collapsed, the tail row while an
|
|
44338
|
+
* expanded compact-capable bubble needs one. The tail is FULLSCREEN-only:
|
|
44339
|
+
* regular draws into the terminal main screen, where an app-owned copy
|
|
44340
|
+
* filter cannot exist, so a visible label there WOULD be copied by the
|
|
44341
|
+
* terminal's native selection — Ctrl+O stays the regular collapse owner. In
|
|
44342
|
+
* fullscreen the tail row is the trailing separator row when one follows,
|
|
44343
|
+
* or one dedicated presentation row charged to the final block (the height
|
|
44344
|
+
* rule mirrors it). */
|
|
44345
|
+
userDisclosureHitFor(component, rendered, truncatedMarker, target) {
|
|
44346
|
+
const bubble = userDisclosureComponentOf(component);
|
|
44347
|
+
if (bubble === void 0) return void 0;
|
|
44348
|
+
const markerRow = bubble.compactMarkerRow();
|
|
44349
|
+
if (markerRow !== void 0) return {
|
|
44350
|
+
row: markerRow,
|
|
44351
|
+
action: "expand",
|
|
44352
|
+
target
|
|
44353
|
+
};
|
|
44354
|
+
if (this.fullscreen === void 0) return void 0;
|
|
44355
|
+
if (!bubble.showsCollapseControl()) return void 0;
|
|
44356
|
+
return {
|
|
44357
|
+
row: rendered.length + (truncatedMarker ? 1 : 0),
|
|
44358
|
+
action: "collapse",
|
|
44359
|
+
target
|
|
44360
|
+
};
|
|
44361
|
+
}
|
|
44220
44362
|
/**
|
|
44221
44363
|
* Re-measure an already-mounted transcript batch without touching component
|
|
44222
44364
|
* lifecycle: the SAME `entry.component` instances are re-rendered at the
|
|
@@ -44227,12 +44369,26 @@ var TuiApp = class TuiApp {
|
|
|
44227
44369
|
remeasureTranscriptBlocks(mounted, width) {
|
|
44228
44370
|
return mounted.map((entry) => {
|
|
44229
44371
|
const rendered = entry.component.render(width);
|
|
44372
|
+
if (entry.block.kind === "pending-user") {
|
|
44373
|
+
const userDisclosureHit = this.userDisclosureHitFor(entry.component, rendered, entry.truncatedMarker, {
|
|
44374
|
+
kind: "pending",
|
|
44375
|
+
key: pendingUserDisclosureKey(entry.block.row)
|
|
44376
|
+
});
|
|
44377
|
+
return {
|
|
44378
|
+
...entry,
|
|
44379
|
+
rendered,
|
|
44380
|
+
...userDisclosureHit === void 0 ? { userDisclosureHit: void 0 } : { userDisclosureHit }
|
|
44381
|
+
};
|
|
44382
|
+
}
|
|
44230
44383
|
if (entry.block.kind !== "message") return {
|
|
44231
44384
|
...entry,
|
|
44232
44385
|
rendered
|
|
44233
44386
|
};
|
|
44234
44387
|
const attachments = this.attachmentRangesOf(entry.component, width);
|
|
44235
|
-
const
|
|
44388
|
+
const userDisclosureHit = isUserMessageDisclosureCandidate(entry.block.message) ? this.userDisclosureHitFor(entry.component, rendered, entry.truncatedMarker, {
|
|
44389
|
+
kind: "durable",
|
|
44390
|
+
message: entry.block.message
|
|
44391
|
+
}) : void 0;
|
|
44236
44392
|
const subCallInfo = this.subCallHitsByMessage.get(entry.block.message);
|
|
44237
44393
|
const subCallHits = subCallInfo === void 0 ? void 0 : subCallInfo.hits.map((hit) => ({
|
|
44238
44394
|
...hit,
|
|
@@ -44249,7 +44405,7 @@ var TuiApp = class TuiApp {
|
|
|
44249
44405
|
attachments,
|
|
44250
44406
|
...subCallHits === void 0 ? { subCallHits: void 0 } : { subCallHits },
|
|
44251
44407
|
...workflowHits === void 0 ? { workflowHits: void 0 } : { workflowHits },
|
|
44252
|
-
...
|
|
44408
|
+
...userDisclosureHit === void 0 ? { userDisclosureHit: void 0 } : { userDisclosureHit }
|
|
44253
44409
|
};
|
|
44254
44410
|
});
|
|
44255
44411
|
}
|
|
@@ -44263,10 +44419,16 @@ var TuiApp = class TuiApp {
|
|
|
44263
44419
|
return "turn" in block.message ? block.message.turn : void 0;
|
|
44264
44420
|
}
|
|
44265
44421
|
/** The normal physical height charged to one rendered block, preserving the
|
|
44266
|
-
* existing zero-row and original-index Spacer rules exactly.
|
|
44422
|
+
* existing zero-row and original-index Spacer rules exactly. An expanded
|
|
44423
|
+
* long-user tail control REUSES the trailing separator row when one
|
|
44424
|
+
* follows; for the final block it is one dedicated presentation row, so the
|
|
44425
|
+
* height must charge it here (the row map and the mounted tree share this
|
|
44426
|
+
* single rule). */
|
|
44267
44427
|
normalTranscriptBlockHeight(entry, index, total) {
|
|
44268
44428
|
if (entry.rendered.length === 0 && !entry.truncatedMarker) return 0;
|
|
44269
|
-
|
|
44429
|
+
const trailing = index < total - 1 ? 1 : 0;
|
|
44430
|
+
const tailControl = entry.userDisclosureHit?.action === "collapse" && trailing === 0 ? 1 : 0;
|
|
44431
|
+
return entry.rendered.length + (entry.truncatedMarker ? 1 : 0) + trailing + tailControl;
|
|
44270
44432
|
}
|
|
44271
44433
|
/** Drop presentation floors and their measured padding. */
|
|
44272
44434
|
clearFocusLiveHeightState() {
|
|
@@ -44341,12 +44503,13 @@ var TuiApp = class TuiApp {
|
|
|
44341
44503
|
const block = entry.block;
|
|
44342
44504
|
return {
|
|
44343
44505
|
...block.kind === "message" ? { message: block.message } : block.kind === "activity" ? { activity: block.activity } : {},
|
|
44506
|
+
...block.kind === "pending-user" ? { pendingKey: pendingUserDisclosureKey(block.row) } : {},
|
|
44344
44507
|
...entry.collapseFocusOwnerOnClick === void 0 ? {} : { collapseFocusOwnerOnClick: entry.collapseFocusOwnerOnClick },
|
|
44345
44508
|
height,
|
|
44346
44509
|
attachments: entry.attachments,
|
|
44347
44510
|
...entry.subCallHits === void 0 ? {} : { subCallHits: entry.subCallHits },
|
|
44348
44511
|
...entry.workflowHits === void 0 ? {} : { workflowHits: entry.workflowHits },
|
|
44349
|
-
...entry.
|
|
44512
|
+
...entry.userDisclosureHit === void 0 ? {} : { userDisclosureHit: entry.userDisclosureHit },
|
|
44350
44513
|
hasTrailingSpacer
|
|
44351
44514
|
};
|
|
44352
44515
|
}
|
|
@@ -44372,7 +44535,8 @@ var TuiApp = class TuiApp {
|
|
|
44372
44535
|
}
|
|
44373
44536
|
const hasTrailingSpacer = index < renderedBlocks.length - 1;
|
|
44374
44537
|
rows.push(this.fullscreenRowEntry(entry, height, hasTrailingSpacer));
|
|
44375
|
-
if (
|
|
44538
|
+
if (entry.userDisclosureHit?.action === "collapse") this.messagesView.addChild(new TranscriptGutterComponent(this.userCollapseControlText(width)));
|
|
44539
|
+
else if (hasTrailingSpacer) this.messagesView.addChild(new Spacer());
|
|
44376
44540
|
}
|
|
44377
44541
|
const padding = paddingRows.get(index) ?? 0;
|
|
44378
44542
|
if (padding > 0) {
|
|
@@ -44575,19 +44739,20 @@ var TuiApp = class TuiApp {
|
|
|
44575
44739
|
let rowTop = welcomeHeight;
|
|
44576
44740
|
let candidate;
|
|
44577
44741
|
for (const entry of this.messageRows) {
|
|
44578
|
-
const rowKind = entry.activity === void 0 ? "message" : "activity";
|
|
44579
|
-
const turn = entry.message !== void 0 && "turn" in entry.message ? entry.message.turn : entry.activity?.turn;
|
|
44580
|
-
const occurrenceKey = turn === void 0 ? void 0 : `${rowKind}:${turn}`;
|
|
44742
|
+
const rowKind = entry.pendingKey !== void 0 ? "pending-user" : entry.activity === void 0 ? "message" : "activity";
|
|
44743
|
+
const turn = entry.pendingKey !== void 0 ? void 0 : entry.message !== void 0 && "turn" in entry.message ? entry.message.turn : entry.activity?.turn;
|
|
44744
|
+
const occurrenceKey = entry.pendingKey !== void 0 ? `pending-user:${entry.pendingKey}` : turn === void 0 ? void 0 : `${rowKind}:${turn}`;
|
|
44581
44745
|
const occurrence = occurrenceKey === void 0 ? 0 : occurrences.get(occurrenceKey) ?? 0;
|
|
44582
44746
|
if (occurrenceKey !== void 0) occurrences.set(occurrenceKey, occurrence + 1);
|
|
44583
|
-
if (entry.height > 0 && rowTop + entry.height > viewportTop && rowTop <= viewportBottom && turn !== void 0) {
|
|
44747
|
+
if (entry.height > 0 && rowTop + entry.height > viewportTop && rowTop <= viewportBottom && (turn !== void 0 || entry.pendingKey !== void 0)) {
|
|
44584
44748
|
const line = fromTop ? Math.max(viewportTop, rowTop) : Math.min(viewportBottom, rowTop + entry.height - 1);
|
|
44585
44749
|
const point = {
|
|
44586
|
-
turn,
|
|
44587
44750
|
rowKind,
|
|
44751
|
+
...turn === void 0 ? {} : { turn },
|
|
44588
44752
|
occurrence,
|
|
44589
44753
|
...entry.message === void 0 ? {} : { message: entry.message },
|
|
44590
44754
|
...entry.activity === void 0 ? {} : { activity: entry.activity },
|
|
44755
|
+
...entry.pendingKey === void 0 ? {} : { pendingKey: entry.pendingKey },
|
|
44591
44756
|
rowOffset: line - rowTop,
|
|
44592
44757
|
viewportOffset: line - viewportTop
|
|
44593
44758
|
};
|
|
@@ -44621,9 +44786,9 @@ var TuiApp = class TuiApp {
|
|
|
44621
44786
|
const occurrences = /* @__PURE__ */ new Map();
|
|
44622
44787
|
let rowTop = welcomeHeight;
|
|
44623
44788
|
for (const entry of this.messageRows) {
|
|
44624
|
-
const rowKind = entry.activity === void 0 ? "message" : "activity";
|
|
44625
|
-
const turn = entry.message !== void 0 && "turn" in entry.message ? entry.message.turn : entry.activity?.turn;
|
|
44626
|
-
const occurrenceKey = turn === void 0 ? void 0 : `${rowKind}:${turn}`;
|
|
44789
|
+
const rowKind = entry.pendingKey !== void 0 ? "pending-user" : entry.activity === void 0 ? "message" : "activity";
|
|
44790
|
+
const turn = entry.pendingKey !== void 0 ? void 0 : entry.message !== void 0 && "turn" in entry.message ? entry.message.turn : entry.activity?.turn;
|
|
44791
|
+
const occurrenceKey = entry.pendingKey !== void 0 ? `pending-user:${entry.pendingKey}` : turn === void 0 ? void 0 : `${rowKind}:${turn}`;
|
|
44627
44792
|
const occurrence = occurrenceKey === void 0 ? 0 : occurrences.get(occurrenceKey) ?? 0;
|
|
44628
44793
|
if (occurrenceKey !== void 0) occurrences.set(occurrenceKey, occurrence + 1);
|
|
44629
44794
|
if (entry.height <= 0) {
|
|
@@ -44634,20 +44799,28 @@ var TuiApp = class TuiApp {
|
|
|
44634
44799
|
top: rowTop,
|
|
44635
44800
|
height: entry.height
|
|
44636
44801
|
};
|
|
44802
|
+
if (point.rowKind === "pending-user") {
|
|
44803
|
+
if (entry.pendingKey !== void 0 && entry.pendingKey === point.pendingKey) return located;
|
|
44804
|
+
rowTop += entry.height;
|
|
44805
|
+
continue;
|
|
44806
|
+
}
|
|
44637
44807
|
if (point.rowKind === rowKind && (point.message !== void 0 ? entry.message === point.message : point.activity !== void 0 && entry.activity === point.activity)) return located;
|
|
44638
44808
|
if (turn === point.turn && rowKind === point.rowKind && occurrence === point.occurrence) return located;
|
|
44639
44809
|
rowTop += entry.height;
|
|
44640
44810
|
}
|
|
44641
44811
|
};
|
|
44642
44812
|
const points = edge === "top" ? [anchor.top, anchor.bottom] : [anchor.bottom, anchor.top];
|
|
44813
|
+
let sawPendingPoint = false;
|
|
44643
44814
|
for (const point of points) {
|
|
44644
44815
|
if (point === void 0) continue;
|
|
44816
|
+
if (point.rowKind === "pending-user") sawPendingPoint = true;
|
|
44645
44817
|
const row = rowFor(point);
|
|
44646
44818
|
if (row === void 0) continue;
|
|
44647
44819
|
const rowOffset = Math.max(0, Math.min(row.height - 1, point.rowOffset));
|
|
44648
44820
|
scroll.scrollTo(row.top + rowOffset - point.viewportOffset, { disableFollow: true });
|
|
44649
44821
|
return true;
|
|
44650
44822
|
}
|
|
44823
|
+
if (sawPendingPoint) return false;
|
|
44651
44824
|
scroll.scrollTo(anchor.scrollTop, { disableFollow: true });
|
|
44652
44825
|
return false;
|
|
44653
44826
|
}
|
|
@@ -44765,11 +44938,19 @@ var TuiApp = class TuiApp {
|
|
|
44765
44938
|
const scroll = this.fullscreenScroll;
|
|
44766
44939
|
const paintedHeight = (component) => this.fullscreen?.getPaintedBox(component)?.height ?? 0;
|
|
44767
44940
|
this.refreshMessageRows();
|
|
44941
|
+
const welcomeHeight = this.welcomeCard.lastRenderedHeight;
|
|
44942
|
+
const copyBlankRows = /* @__PURE__ */ new Set();
|
|
44943
|
+
let rowTop = welcomeHeight;
|
|
44944
|
+
for (const entry of this.messageRows) {
|
|
44945
|
+
const hit = entry.userDisclosureHit;
|
|
44946
|
+
if (hit !== void 0 && hit.action === "collapse") copyBlankRows.add(rowTop + hit.row);
|
|
44947
|
+
rowTop += entry.height;
|
|
44948
|
+
}
|
|
44768
44949
|
this.fullscreenPaintSnapshot = {
|
|
44769
44950
|
columns: this.terminal.columns,
|
|
44770
44951
|
termRows: this.terminal.rows,
|
|
44771
44952
|
headerHeight: paintedHeight(this.header),
|
|
44772
|
-
welcomeHeight
|
|
44953
|
+
welcomeHeight,
|
|
44773
44954
|
footerHeight: paintedHeight(this.footer),
|
|
44774
44955
|
editorHeight: paintedHeight(this.editorSeat),
|
|
44775
44956
|
workingHeight: paintedHeight(this.working),
|
|
@@ -44783,7 +44964,8 @@ var TuiApp = class TuiApp {
|
|
|
44783
44964
|
ownerId: this.fullscreenRowOwnerId(entry, index),
|
|
44784
44965
|
height: entry.height,
|
|
44785
44966
|
hits: this.fullscreenRowHits(entry, index)
|
|
44786
|
-
}))
|
|
44967
|
+
})),
|
|
44968
|
+
copyBlankRows
|
|
44787
44969
|
};
|
|
44788
44970
|
}
|
|
44789
44971
|
/** The per-row SEMANTIC hit identities of one painted transcript block
|
|
@@ -44809,13 +44991,16 @@ var TuiApp = class TuiApp {
|
|
|
44809
44991
|
}
|
|
44810
44992
|
/** The per-row SEMANTIC hit identity of one transcript row (mouse
|
|
44811
44993
|
* parity): the row's ACTUAL action target, following the click-path
|
|
44812
|
-
* priority EXACTLY (blank-row escape hatch > Focus
|
|
44813
|
-
* sub-call > Workflow > attachment > Focus-secondary/card).
|
|
44814
|
-
*
|
|
44815
|
-
*
|
|
44816
|
-
*
|
|
44817
|
-
*
|
|
44994
|
+
* priority EXACTLY (long-user disclosure > blank-row escape hatch > Focus
|
|
44995
|
+
* activity > PTC sub-call > Workflow > attachment > Focus-secondary/card).
|
|
44996
|
+
* The long-user control wins first because it shares the trailing separator
|
|
44997
|
+
* row with the blank-row escape hatch; a row inside a Workflow card that
|
|
44998
|
+
* matches no hit is INERT (the card consumes every click — it must never
|
|
44999
|
+
* fall through to the owner/card branch). Shared by the paint-snapshot
|
|
45000
|
+
* commit and the release-click validation so both sides compute the EXACT
|
|
45001
|
+
* same identity string. */
|
|
44818
45002
|
fullscreenRowHitIdentity(entry, inMessage, nextVisible) {
|
|
45003
|
+
if (entry.userDisclosureHit !== void 0) return inMessage === entry.userDisclosureHit.row ? this.userDisclosureHitIdentity(entry.userDisclosureHit) : "inert";
|
|
44819
45004
|
if (entry.hasTrailingSpacer && inMessage === entry.height - 1) {
|
|
44820
45005
|
const owner = this.blankRowFocusCollapseOwner(entry, nextVisible);
|
|
44821
45006
|
return owner !== void 0 ? `focus:collapse:${owner}` : "inert";
|
|
@@ -44834,13 +45019,21 @@ var TuiApp = class TuiApp {
|
|
|
44834
45019
|
return "inert";
|
|
44835
45020
|
}
|
|
44836
45021
|
for (const attachment of entry.attachments) if (inMessage >= attachment.start && inMessage < attachment.end) return `attachment:${token}:${attachment.imageIndex}`;
|
|
44837
|
-
if (entry.userDisclosureRow !== void 0) return inMessage === entry.userDisclosureRow ? `user:expand:${token}` : "inert";
|
|
44838
45022
|
if (entry.collapseFocusOwnerOnClick !== void 0) {
|
|
44839
45023
|
if (isFocusSecondaryDisclosure(message)) return `message:toggle:${token}`;
|
|
44840
45024
|
return `focus:collapse:${entry.collapseFocusOwnerOnClick}`;
|
|
44841
45025
|
}
|
|
44842
45026
|
return `message:toggle:${token}`;
|
|
44843
45027
|
}
|
|
45028
|
+
/** The press/release semantic identity of one long-user disclosure control:
|
|
45029
|
+
* the durable message token or the stable pending key, ALWAYS qualified by
|
|
45030
|
+
* the direction. A stale press must never transfer an expand target to a
|
|
45031
|
+
* collapse target (or to a different pending row) that repainted onto the
|
|
45032
|
+
* same cell. */
|
|
45033
|
+
userDisclosureHitIdentity(hit) {
|
|
45034
|
+
if (hit.target.kind === "durable") return `user:${hit.action}:${this.identityToken(hit.target.message)}`;
|
|
45035
|
+
return `pending-user:${hit.action}:${hit.target.key}`;
|
|
45036
|
+
}
|
|
44844
45037
|
/** The semantic identity of one Workflow card row hit (the durable
|
|
44845
45038
|
* run/phase/member/agents target the click acts on). */
|
|
44846
45039
|
workflowHitIdentity(hit) {
|
|
@@ -44874,12 +45067,12 @@ var TuiApp = class TuiApp {
|
|
|
44874
45067
|
return token;
|
|
44875
45068
|
}
|
|
44876
45069
|
/** The press-time semantic identity of a transcript row (mouse parity):
|
|
44877
|
-
* the message/activity owner (kind + turn + the per-object
|
|
44878
|
-
* token, or the entry index for
|
|
44879
|
-
* paint-snapshot commit and the release-click
|
|
44880
|
-
* compute the EXACT same identity string. */
|
|
45070
|
+
* the message/activity/pending-user owner (kind + turn + the per-object
|
|
45071
|
+
* identity token, the stable pending key, or the entry index for truly
|
|
45072
|
+
* ownerless rows). Shared by the paint-snapshot commit and the release-click
|
|
45073
|
+
* validation so both sides compute the EXACT same identity string. */
|
|
44881
45074
|
fullscreenRowOwnerId(entry, entryIndex) {
|
|
44882
|
-
return entry.message !== void 0 ? `msg:${entry.message.kind}:${"turn" in entry.message ? entry.message.turn : 0}:${this.identityToken(entry.message)}` : entry.activity !== void 0 ? `activity:${entry.activity.turn}:${this.identityToken(entry.activity)}` : `entry:${entryIndex}`;
|
|
45075
|
+
return entry.message !== void 0 ? `msg:${entry.message.kind}:${"turn" in entry.message ? entry.message.turn : 0}:${this.identityToken(entry.message)}` : entry.activity !== void 0 ? `activity:${entry.activity.turn}:${this.identityToken(entry.activity)}` : entry.pendingKey !== void 0 ? `pending-user:${entry.pendingKey}` : `entry:${entryIndex}`;
|
|
44883
45076
|
}
|
|
44884
45077
|
/** Resolve a fullscreen physical row to a transcript cell (the entry
|
|
44885
45078
|
* index + its block-relative row) against a GIVEN projection — a pure
|
|
@@ -45055,6 +45248,10 @@ var TuiApp = class TuiApp {
|
|
|
45055
45248
|
}
|
|
45056
45249
|
this.fullscreenCellGesture = void 0;
|
|
45057
45250
|
{
|
|
45251
|
+
if (entry.userDisclosureHit !== void 0) {
|
|
45252
|
+
if (inMessage === entry.userDisclosureHit.row) this.applyUserDisclosureHit(entry.userDisclosureHit);
|
|
45253
|
+
return;
|
|
45254
|
+
}
|
|
45058
45255
|
if (entry.hasTrailingSpacer && inMessage === entry.height - 1) {
|
|
45059
45256
|
if (this.terminal.columns !== this.lastPaintColumns || this.terminal.rows !== this.lastPaintRows) return;
|
|
45060
45257
|
let next;
|
|
@@ -45075,10 +45272,6 @@ var TuiApp = class TuiApp {
|
|
|
45075
45272
|
}
|
|
45076
45273
|
const message = entry.message;
|
|
45077
45274
|
if (message === void 0) return;
|
|
45078
|
-
if (entry.userDisclosureRow !== void 0) {
|
|
45079
|
-
if (inMessage === entry.userDisclosureRow) this.expandUserMessage(message);
|
|
45080
|
-
return;
|
|
45081
|
-
}
|
|
45082
45275
|
if (entry.subCallHits !== void 0) {
|
|
45083
45276
|
const hit = entry.subCallHits.find((candidate) => inMessage >= candidate.top && inMessage < candidate.top + candidate.height);
|
|
45084
45277
|
if (hit !== void 0) {
|
|
@@ -45124,15 +45317,22 @@ var TuiApp = class TuiApp {
|
|
|
45124
45317
|
if (turn === void 0 || !this.focusExpandedTurns.has(turn)) return void 0;
|
|
45125
45318
|
return (next?.activity?.turn ?? next?.collapseFocusOwnerOnClick) === turn ? turn : void 0;
|
|
45126
45319
|
}
|
|
45127
|
-
/**
|
|
45128
|
-
*
|
|
45129
|
-
*
|
|
45130
|
-
*
|
|
45131
|
-
|
|
45132
|
-
|
|
45133
|
-
|
|
45134
|
-
this.
|
|
45135
|
-
|
|
45320
|
+
/** Apply one bidirectional long-user disclosure control click (fullscreen
|
|
45321
|
+
* compact marker → expand, expanded tail control → collapse). The durable
|
|
45322
|
+
* override reuses the per-message disclosure state; the pending override is
|
|
45323
|
+
* presentation-only ephemeral state keyed by the stable pending identity.
|
|
45324
|
+
* The canonical `message.text` is never touched. */
|
|
45325
|
+
applyUserDisclosureHit(hit) {
|
|
45326
|
+
const expanded = hit.action === "expand";
|
|
45327
|
+
this.mutateTranscriptDisclosure(() => {
|
|
45328
|
+
if (hit.target.kind === "durable") {
|
|
45329
|
+
if (this.expandedOverride.get(hit.target.message) === expanded) return;
|
|
45330
|
+
this.expandedOverride.set(hit.target.message, expanded);
|
|
45331
|
+
} else {
|
|
45332
|
+
if (this.pendingUserExpanded.get(hit.target.key) === expanded) return;
|
|
45333
|
+
this.pendingUserExpanded.set(hit.target.key, expanded);
|
|
45334
|
+
}
|
|
45335
|
+
});
|
|
45136
45336
|
}
|
|
45137
45337
|
/** Toggle one collapsible message's individual expansion (mouse click).
|
|
45138
45338
|
* The same foldable set as the render rule (plan §32 — never two
|
|
@@ -45371,6 +45571,7 @@ var TuiApp = class TuiApp {
|
|
|
45371
45571
|
clearSessionOverrides() {
|
|
45372
45572
|
this.clearFocusLiveHeightState();
|
|
45373
45573
|
this.expandedOverride.clear();
|
|
45574
|
+
this.pendingUserExpanded.clear();
|
|
45374
45575
|
this.fullscreenCellGesture = void 0;
|
|
45375
45576
|
this.todoClickCoalesceUntil = 0;
|
|
45376
45577
|
this.focusExpandedTurns.clear();
|
|
@@ -46819,7 +47020,7 @@ var TuiApp = class TuiApp {
|
|
|
46819
47020
|
messageRenderState(message, boundary, userBoundary) {
|
|
46820
47021
|
const expanded = this.effectiveMessageExpanded(message, boundary, userBoundary);
|
|
46821
47022
|
const insideFocusSecondary = "turn" in message && this.isInsideExpandedFocus(message, boundary) && isFocusSecondaryDisclosure(message);
|
|
46822
|
-
const expandHint = message.kind === "thinking" ? this.fullscreen !== void 0 ? "click" : "thinking" : isUserMessageDisclosureCandidate(message) ? this.
|
|
47023
|
+
const expandHint = message.kind === "thinking" ? this.fullscreen !== void 0 ? "click" : "thinking" : isUserMessageDisclosureCandidate(message) ? this.userFoldHint() : insideFocusSecondary ? this.fullscreen !== void 0 ? "click" : void 0 : "fold";
|
|
46823
47024
|
return {
|
|
46824
47025
|
expanded,
|
|
46825
47026
|
fullReveal: this.expandedOverride.get(message) === true || this.fullscreen === void 0 && insideFocusSecondary,
|
|
@@ -47093,12 +47294,13 @@ var TuiApp = class TuiApp {
|
|
|
47093
47294
|
renderMessage(message, expanded, expandHint, fullReveal, width) {
|
|
47094
47295
|
if (message.kind === "user") {
|
|
47095
47296
|
if (message.content !== void 0 && message.content.some((block) => block.type !== "text")) return this.renderUserBlocks(message.content, message);
|
|
47096
|
-
if (
|
|
47297
|
+
if (this.userDisclosureAffordanceAvailable()) {
|
|
47097
47298
|
const hint = expandHint;
|
|
47098
47299
|
return new UserBubbleComponent(new Text(message.text, 0, 0), `${color.roleUser("❯")} `, color.roleUserBg, {
|
|
47099
47300
|
thresholdRows: USER_MESSAGE_COMPACT_THRESHOLD_ROWS,
|
|
47100
47301
|
headRows: USER_MESSAGE_HEAD_ROWS,
|
|
47101
47302
|
tailRows: USER_MESSAGE_TAIL_ROWS,
|
|
47303
|
+
expanded,
|
|
47102
47304
|
compactMarker: (hiddenRows, available) => this.userCompactMarker(hiddenRows, available, hint)
|
|
47103
47305
|
});
|
|
47104
47306
|
}
|
|
@@ -48451,6 +48653,10 @@ var TuiApp = class TuiApp {
|
|
|
48451
48653
|
this.queueItems = presentation.queued;
|
|
48452
48654
|
this.pendingUserRows = presentation.steering;
|
|
48453
48655
|
this.queueRunning = presentation.running;
|
|
48656
|
+
if (this.pendingUserExpanded.size > 0) {
|
|
48657
|
+
const liveKeys = new Set(presentation.steering.map(pendingUserDisclosureKey));
|
|
48658
|
+
for (const key of [...this.pendingUserExpanded.keys()]) if (!liveKeys.has(key)) this.pendingUserExpanded.delete(key);
|
|
48659
|
+
}
|
|
48454
48660
|
this.projectActivity();
|
|
48455
48661
|
this.renderQueuePane();
|
|
48456
48662
|
this.rebuildMessages();
|
|
@@ -48864,6 +49070,63 @@ var TuiApp = class TuiApp {
|
|
|
48864
49070
|
if (visibleWidth(full) <= availableWidth) return color.textDim(full);
|
|
48865
49071
|
return color.textDim(`── ${hiddenRows} rows compacted ──`);
|
|
48866
49072
|
}
|
|
49073
|
+
/** The long-user fold-hint owner for the CURRENT surface: regular is the
|
|
49074
|
+
* Ctrl+O recent-turn master; fullscreen without Focus offers the compact
|
|
49075
|
+
* marker click AND the effective expand key; fullscreen Focus is click-only
|
|
49076
|
+
* (Ctrl+O owns the Thought-root bulk there). Shared by the durable bubble
|
|
49077
|
+
* and the ephemeral pending lane so their labels never diverge. */
|
|
49078
|
+
userFoldHint() {
|
|
49079
|
+
return this.fullscreen === void 0 ? "fold" : this.focusModeEnabled ? "click" : "click-fold";
|
|
49080
|
+
}
|
|
49081
|
+
/** The expanded long-user tail control: presentation chrome naming the
|
|
49082
|
+
* collapse affordance of the CURRENT surface. Right-aligned within the
|
|
49083
|
+
* transcript content width; a narrow width truncates instead of wrapping. */
|
|
49084
|
+
userCollapseControlText(width) {
|
|
49085
|
+
const label = `▴ Collapse · ${this.expandHint(this.userFoldHint())}`;
|
|
49086
|
+
const clipped = visibleWidth(label) <= width ? label : truncateToWidth(label, width, "…");
|
|
49087
|
+
const pad = " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
49088
|
+
return new Text(color.textDim(pad + clipped), 0, 0);
|
|
49089
|
+
}
|
|
49090
|
+
/** The copy-source filter of one selected row: the expanded long-user tail
|
|
49091
|
+
* control is presentation chrome that must never reach the clipboard, and
|
|
49092
|
+
* its visual row copies as the blank separator it replaced (fork seam
|
|
49093
|
+
* X057). Paint, search, word/line selection and the mouse hit map are
|
|
49094
|
+
* untouched — only the copy source is filtered. The decision comes from the
|
|
49095
|
+
* LAST-PAINTED snapshot (the same epoch that produced the ScrollView's
|
|
49096
|
+
* `scrollContentLines`), never the live `messageRows`: a rebuild that has
|
|
49097
|
+
* not repainted yet must not reinterpret a painted row — otherwise the
|
|
49098
|
+
* painted tail chrome would leak, or a painted real row the newer
|
|
49099
|
+
* projection calls chrome would be wrongly blanked. */
|
|
49100
|
+
selectionLineText(context) {
|
|
49101
|
+
if (context.scrollView === void 0 || context.scrollView !== this.fullscreenScroll) return void 0;
|
|
49102
|
+
return this.fullscreenPaintSnapshot?.copyBlankRows.has(context.row) === true ? "" : void 0;
|
|
49103
|
+
}
|
|
49104
|
+
/** The effective disclosure state of one ephemeral pending-user row: the
|
|
49105
|
+
* explicit per-row override wins, otherwise the surface rule — fullscreen
|
|
49106
|
+
* Focus is compact-by-default (Ctrl+O owns the Thought roots there), every
|
|
49107
|
+
* other surface follows the Ctrl+O master exactly like a recent durable
|
|
49108
|
+
* user prompt. */
|
|
49109
|
+
pendingUserExpandedState(row) {
|
|
49110
|
+
const override = this.pendingUserExpanded.get(pendingUserDisclosureKey(row));
|
|
49111
|
+
if (override !== void 0) return override;
|
|
49112
|
+
if (this.fullscreen !== void 0 && this.focusModeEnabled) return false;
|
|
49113
|
+
return this.toolOutputExpanded;
|
|
49114
|
+
}
|
|
49115
|
+
/** Build the ephemeral pending-user component for this surface. A text-only
|
|
49116
|
+
* pending row joins the SAME visual-row compaction as a durable text-only
|
|
49117
|
+
* user bubble when this surface has a disclosure affordance; otherwise it
|
|
49118
|
+
* renders in full (never a dead marker). */
|
|
49119
|
+
pendingUserComponentFor(row) {
|
|
49120
|
+
if (row.foldableText !== true || !this.userDisclosureAffordanceAvailable()) return new PendingUserComponent(row, this.queueRunning);
|
|
49121
|
+
const hint = this.userFoldHint();
|
|
49122
|
+
return new PendingUserComponent(row, this.queueRunning, {
|
|
49123
|
+
thresholdRows: USER_MESSAGE_COMPACT_THRESHOLD_ROWS,
|
|
49124
|
+
headRows: USER_MESSAGE_HEAD_ROWS,
|
|
49125
|
+
tailRows: USER_MESSAGE_TAIL_ROWS,
|
|
49126
|
+
expanded: this.pendingUserExpandedState(row),
|
|
49127
|
+
compactMarker: (hiddenRows, available) => this.userCompactMarker(hiddenRows, available, hint)
|
|
49128
|
+
});
|
|
49129
|
+
}
|
|
48867
49130
|
/** Whether the long-user fold has a usable expand affordance on THIS
|
|
48868
49131
|
* surface. Fullscreen always does (the compact-marker click); regular needs
|
|
48869
49132
|
* the effective `app.transcript.toggleExpand` key — compacting without one
|
|
@@ -48880,6 +49143,13 @@ var TuiApp = class TuiApp {
|
|
|
48880
49143
|
const inner = Math.max(1, this.transcriptRenderWidth() - visibleWidth(`${color.roleUser("❯")} `));
|
|
48881
49144
|
return new Text(message.text, 0, 0).render(inner).length > USER_MESSAGE_COMPACT_THRESHOLD_ROWS;
|
|
48882
49145
|
}
|
|
49146
|
+
/** Whether one pending-user row ACTUALLY compacts at the current transcript
|
|
49147
|
+
* width (the same visual-row threshold the pending bubble renderer applies),
|
|
49148
|
+
* so a stale override on a short row never consumes the Ctrl+O press. */
|
|
49149
|
+
pendingUserCompactsAtCurrentWidth(row) {
|
|
49150
|
+
const inner = Math.max(1, this.transcriptRenderWidth() - visibleWidth(`${color.roleUser("❯")} `));
|
|
49151
|
+
return new Text(row.text, 0, 0).render(inner).length > USER_MESSAGE_COMPACT_THRESHOLD_ROWS;
|
|
49152
|
+
}
|
|
48883
49153
|
/**
|
|
48884
49154
|
* Install slash-command + file-path autocompletion on the editor, plus
|
|
48885
49155
|
* `@`-file mentions through the Host-file port (migration M1.10 — the
|
|
@@ -54073,306 +54343,6 @@ var SessionOperationBarrier = class {
|
|
|
54073
54343
|
}
|
|
54074
54344
|
};
|
|
54075
54345
|
//#endregion
|
|
54076
|
-
//#region src/rewind.ts
|
|
54077
|
-
/** Whether one user/message event is a DIRECT human prompt (the transcript's
|
|
54078
|
-
* classification: `source.kind === 'user'`). Injected context, skill bodies,
|
|
54079
|
-
* system reminders and goal continuations answer false. The single helper
|
|
54080
|
-
* every rewind consumer must use — never scatter source-kind checks. */
|
|
54081
|
-
function isHumanTurnMessage(event) {
|
|
54082
|
-
return event.data.source.kind === "user";
|
|
54083
|
-
}
|
|
54084
|
-
/** Whether a message is empty for rewind purposes: mirror the
|
|
54085
|
-
* transcript's finalized user-content visibility rule. */
|
|
54086
|
-
function isEmptyMessage(blocks) {
|
|
54087
|
-
return !userBlocksVisibleNow(blocks);
|
|
54088
|
-
}
|
|
54089
|
-
/** One-line, width-bounded preview of a prompt (whitespace collapsed). */
|
|
54090
|
-
function singleLinePreview(text) {
|
|
54091
|
-
const flat = text.replace(/\s+/g, " ").trim();
|
|
54092
|
-
return flat.length > 120 ? `${flat.slice(0, 117)}…` : flat;
|
|
54093
|
-
}
|
|
54094
|
-
/**
|
|
54095
|
-
* Derive the rewind candidates from a session's event log.
|
|
54096
|
-
*
|
|
54097
|
-
* Guarantees (plan §6.1):
|
|
54098
|
-
* - only COMPLETED turns (a `turn/end` closed the span) are listed; the
|
|
54099
|
-
* open turn is never a candidate;
|
|
54100
|
-
* - newest turn first;
|
|
54101
|
-
* - one candidate per turn (the turn's primary — first non-empty human
|
|
54102
|
-
* `user/message`); injected/steer messages never form extra rows;
|
|
54103
|
-
* - a malformed span (a new `turn/start` while one is open, an end without
|
|
54104
|
-
* a start) is skipped, never thrown to the UI;
|
|
54105
|
-
* - the first turn is a legal candidate (seed length 0);
|
|
54106
|
-
* - no persistence, no side effects, no agent creation.
|
|
54107
|
-
* @param events - the session log (chronological).
|
|
54108
|
-
* @returns candidates newest-first.
|
|
54109
|
-
*/
|
|
54110
|
-
function collectRewindCandidates(events) {
|
|
54111
|
-
const candidates = [];
|
|
54112
|
-
let open;
|
|
54113
|
-
for (const event of events) if (event.type === "turn/start") open = {
|
|
54114
|
-
turn: event.data.turn,
|
|
54115
|
-
startSeq: event.seq
|
|
54116
|
-
};
|
|
54117
|
-
else if (event.type === "turn/end") {
|
|
54118
|
-
const primary = open?.primary;
|
|
54119
|
-
if (primary !== void 0) {
|
|
54120
|
-
const blocks = primary.blocks;
|
|
54121
|
-
candidates.push({
|
|
54122
|
-
turnStartSeq: open.startSeq,
|
|
54123
|
-
turn: open.turn,
|
|
54124
|
-
messageSeq: primary.seq,
|
|
54125
|
-
editorText: textOf(blocks),
|
|
54126
|
-
preview: singleLinePreview(textOf(blocks)),
|
|
54127
|
-
hasNonTextContent: blocks.some((block) => block.type !== "text")
|
|
54128
|
-
});
|
|
54129
|
-
}
|
|
54130
|
-
open = void 0;
|
|
54131
|
-
} else if (event.type === "user/message" && open !== void 0 && open.primary === void 0) {
|
|
54132
|
-
const blocks = event.data.content;
|
|
54133
|
-
if (isHumanTurnMessage(event) && !isEmptyMessage(blocks)) open.primary = {
|
|
54134
|
-
seq: event.seq,
|
|
54135
|
-
blocks
|
|
54136
|
-
};
|
|
54137
|
-
}
|
|
54138
|
-
return candidates.reverse();
|
|
54139
|
-
}
|
|
54140
|
-
/**
|
|
54141
|
-
* The seed for one candidate: EVERY event before the selected `turn/start`
|
|
54142
|
-
* — including log-only state events between turns (a `todo/write`, a
|
|
54143
|
-
* `permission/preset` record) — but never the `turn/start` itself.
|
|
54144
|
-
* @param events - the session log the candidate was collected from.
|
|
54145
|
-
* @param candidate - the selected rewind point.
|
|
54146
|
-
* @returns the child session's seed events.
|
|
54147
|
-
* @throws when the point no longer exists, or when the seed would end on
|
|
54148
|
-
* an OPEN `turn/start` (malformed log — the fork boundary must be a
|
|
54149
|
-
* completed turn).
|
|
54150
|
-
*/
|
|
54151
|
-
function rewindSeed(events, candidate) {
|
|
54152
|
-
const index = events.findIndex((event) => event.seq === candidate.turnStartSeq && event.type === "turn/start");
|
|
54153
|
-
if (index < 0) throw new Error("rewind point no longer exists");
|
|
54154
|
-
const seed = events.slice(0, index);
|
|
54155
|
-
for (let i = seed.length - 1; i >= 0; i -= 1) {
|
|
54156
|
-
const type = seed[i]?.type;
|
|
54157
|
-
if (type === "turn/end") break;
|
|
54158
|
-
if (type === "turn/start") throw new Error("rewind point is inside an open turn");
|
|
54159
|
-
}
|
|
54160
|
-
return seed;
|
|
54161
|
-
}
|
|
54162
|
-
/** One picker row for a candidate. The value is the `turnStartSeq` string;
|
|
54163
|
-
* the selection resolves against the candidate list captured at open time
|
|
54164
|
-
* (stale selections are rejected by the workflow's generation gates). */
|
|
54165
|
-
function rewindPickerItem(candidate) {
|
|
54166
|
-
const tag = candidate.hasNonTextContent ? "[attachment] " : "";
|
|
54167
|
-
const preview = candidate.preview === "" && candidate.hasNonTextContent ? "(attachment only)" : candidate.preview;
|
|
54168
|
-
return {
|
|
54169
|
-
value: String(candidate.turnStartSeq),
|
|
54170
|
-
label: `turn ${candidate.turn} · ${tag}${preview}`,
|
|
54171
|
-
...candidate.hasNonTextContent ? { description: "non-text content is not re-staged on rewind" } : {}
|
|
54172
|
-
};
|
|
54173
|
-
}
|
|
54174
|
-
//#endregion
|
|
54175
|
-
//#region src/runtime/session-lifecycle-port.ts
|
|
54176
|
-
/** A lifecycle outcome that must ABORT the caller's transition, carrying the
|
|
54177
|
-
* two independent axes so a Remote caller never has to parse a string. */
|
|
54178
|
-
var LifecycleError = class extends Error {
|
|
54179
|
-
settlement;
|
|
54180
|
-
ownership;
|
|
54181
|
-
publishedSessionId;
|
|
54182
|
-
/** CORRELATION ONLY for an indeterminate create — never publication proof. */
|
|
54183
|
-
requestedSessionId;
|
|
54184
|
-
constructor(settlement, ownership, message, publishedSessionId, requestedSessionId) {
|
|
54185
|
-
super(message);
|
|
54186
|
-
this.name = "LifecycleError";
|
|
54187
|
-
this.settlement = settlement;
|
|
54188
|
-
this.ownership = ownership;
|
|
54189
|
-
this.publishedSessionId = publishedSessionId;
|
|
54190
|
-
this.requestedSessionId = requestedSessionId;
|
|
54191
|
-
}
|
|
54192
|
-
};
|
|
54193
|
-
/** Unwrap a CREATE result for the runner's transition: a `created` result that
|
|
54194
|
-
* still owns the surface yields the handle; everything else aborts with a
|
|
54195
|
-
* machine-readable `LifecycleError` (a superseded create is NOT committed to
|
|
54196
|
-
* the local surface, though its identity stays available). */
|
|
54197
|
-
function requireCreated(result) {
|
|
54198
|
-
const { ownership, outcome } = result;
|
|
54199
|
-
switch (outcome.kind) {
|
|
54200
|
-
case "created":
|
|
54201
|
-
if (ownership === "superseded") throw new LifecycleError("superseded", ownership, `Session "${outcome.handle.session.id}" was created, but the local surface was superseded — do not retry creation`, outcome.handle.session.id);
|
|
54202
|
-
return outcome.handle;
|
|
54203
|
-
case "published-with-error": throw new LifecycleError(outcome.kind, ownership, `Session "${outcome.sessionId}" was published, but workspace attach/reconcile failed: ${outcome.error.message} (${outcome.error.code}) — do not retry creation`, outcome.sessionId);
|
|
54204
|
-
case "indeterminate": throw new LifecycleError(outcome.kind, ownership, `the create is indeterminate — do not retry: ${outcome.error.message} (${outcome.error.code})`, void 0, outcome.requestedSessionId);
|
|
54205
|
-
case "rejected": throw new LifecycleError(outcome.kind, ownership, `${outcome.error.message} (${outcome.error.code})`, void 0);
|
|
54206
|
-
case "cancelled": throw new LifecycleError("cancelled", ownership, "the Session creation was cancelled before dispatch", void 0);
|
|
54207
|
-
}
|
|
54208
|
-
}
|
|
54209
|
-
/** Unwrap an OPEN result for the runner's transition. */
|
|
54210
|
-
function requireOpened(result) {
|
|
54211
|
-
const { ownership, outcome } = result;
|
|
54212
|
-
switch (outcome.kind) {
|
|
54213
|
-
case "opened":
|
|
54214
|
-
if (ownership === "superseded") throw new LifecycleError("superseded", ownership, "the Session was opened but the local surface was superseded", outcome.handle.session.id);
|
|
54215
|
-
return outcome.handle;
|
|
54216
|
-
case "unavailable": throw new LifecycleError("unavailable", ownership, outcome.message, void 0);
|
|
54217
|
-
case "cancelled": throw new LifecycleError("cancelled", ownership, "the Session open was cancelled before selection", void 0);
|
|
54218
|
-
}
|
|
54219
|
-
}
|
|
54220
|
-
/** Extract the Direct ownership handle (the real AgentHandle with
|
|
54221
|
-
* `dispose()`) from a lifecycle result. Accepts both the converged
|
|
54222
|
-
* SessionHandle and a legacy AgentHandle so transition code cannot lose the
|
|
54223
|
-
* ownership capability. Remote handles lack `direct` and yield undefined. */
|
|
54224
|
-
function ownerHandleOf(next) {
|
|
54225
|
-
const handle = next;
|
|
54226
|
-
if (handle.direct?.ownerHandle !== void 0) return handle.direct.ownerHandle;
|
|
54227
|
-
if (typeof handle.dispose === "function") return handle;
|
|
54228
|
-
}
|
|
54229
|
-
/** Extract the live in-process agent from a lifecycle result (the Direct
|
|
54230
|
-
* SessionHandle via `direct.agent`, or a legacy AgentHandle's `agent`).
|
|
54231
|
-
* Remote handles yield undefined. */
|
|
54232
|
-
function directAgentOf(next) {
|
|
54233
|
-
const handle = next;
|
|
54234
|
-
return handle.direct?.agent ?? handle.agent;
|
|
54235
|
-
}
|
|
54236
|
-
//#endregion
|
|
54237
|
-
//#region src/session-fork.ts
|
|
54238
|
-
/**
|
|
54239
|
-
* Shared forked-agent creation for `/fork` and conversation rewind.
|
|
54240
|
-
*
|
|
54241
|
-
* Both features create a child session from a seed through ONE chain
|
|
54242
|
-
* (plan §6.2): resolve the source's recorded preset → compose → agents.create
|
|
54243
|
-
* with `parentSession` + `isSeeded`/`inheritedEventCount` metadata → the
|
|
54244
|
-
* caller swaps to it.
|
|
54245
|
-
* Only the seed computation differs (`forkSeed` for `/fork`, `rewindSeed`
|
|
54246
|
-
* for rewind), so the metadata, preset/base provider-model options and cwd
|
|
54247
|
-
* rules can never drift between the two surfaces. Effective Session model
|
|
54248
|
-
* selection is inherited separately through the transition seam.
|
|
54249
|
-
*
|
|
54250
|
-
* `commitRewind` is the rewind selection workflow (stale check → create →
|
|
54251
|
-
* swap COMMIT → restore the prompt), with ONE authoritative pre-create
|
|
54252
|
-
* stale gate: it MUST run inside the caller's session-transition gate, so
|
|
54253
|
-
* no other transition can interleave and a stale selection never creates a
|
|
54254
|
-
* child at all — once the create succeeds, the child is committed (no
|
|
54255
|
-
* post-create disposal of a "ghost" child exists).
|
|
54256
|
-
* @module @xmoon76/dsh-pi-tui/session-fork
|
|
54257
|
-
*/
|
|
54258
|
-
/**
|
|
54259
|
-
* Create one forked agent: the source's projected preset, the source's
|
|
54260
|
-
* workspace cwd, and `parentSession` + `isSeeded` + `inheritedEventCount`
|
|
54261
|
-
* metadata (the exact chain `/fork` used before the extraction). Ordinary
|
|
54262
|
-
* create no longer carries provider/model inputs (D2.3). The effective Session model selection, when
|
|
54263
|
-
* needed, is applied by the transition caller after the inherited seed. The
|
|
54264
|
-
* child's session id is PRE-GENERATED by the caller so
|
|
54265
|
-
* the transition can publish the durable child under a known identity
|
|
54266
|
-
* (review round 6: every fallible operation happens before the durable
|
|
54267
|
-
* child exists).
|
|
54268
|
-
*
|
|
54269
|
-
* The preset rides the semantic create request as the CONCRETE preset id
|
|
54270
|
-
* (migration M1.11): the Direct adapter composes the setup internally from
|
|
54271
|
-
* that id — a composition/setup callback never crosses this seam. `undefined`
|
|
54272
|
-
* = the deployment default (a rosterless deployment records no preset).
|
|
54273
|
-
* @param host - the fork host surface.
|
|
54274
|
-
* @param source - the session being forked from (never modified).
|
|
54275
|
-
* @param seed - the child's seed events (completed-turn prefix).
|
|
54276
|
-
* @param sessionId - the pre-generated child session id.
|
|
54277
|
-
* @param presetId - the concrete preset id the child composes on.
|
|
54278
|
-
* @returns the created agent handle (NOT yet committed).
|
|
54279
|
-
*/
|
|
54280
|
-
async function createForkedAgent(host, source, seed, sessionId, presetId) {
|
|
54281
|
-
const cwd = source.session.header.cwd || host.sessionCwd();
|
|
54282
|
-
const inheritedPreset = presetId ?? host.sessionPreset?.(source.session);
|
|
54283
|
-
return host.agents.create({
|
|
54284
|
-
sessionId: String(sessionId),
|
|
54285
|
-
meta: {
|
|
54286
|
-
cwd,
|
|
54287
|
-
...inheritedPreset === void 0 ? {} : { agentPreset: inheritedPreset },
|
|
54288
|
-
parentSession: source.session.id,
|
|
54289
|
-
isSeeded: true
|
|
54290
|
-
},
|
|
54291
|
-
inheritedEventCount: seed.length,
|
|
54292
|
-
agentPreset: inheritedPreset,
|
|
54293
|
-
seed
|
|
54294
|
-
});
|
|
54295
|
-
}
|
|
54296
|
-
/** Whether a captured rewind identity still matches the live surface: a
|
|
54297
|
-
* sessionless surface (`sessionId: undefined`) can never match a captured
|
|
54298
|
-
* source, so a surface that became sessionless is refused just like a
|
|
54299
|
-
* switched one. Pure so the race is unit-testable without the runner. */
|
|
54300
|
-
function isRewindIdentityCurrent(live, expected) {
|
|
54301
|
-
return live.sessionId === expected.sessionId && live.generation === expected.generation;
|
|
54302
|
-
}
|
|
54303
|
-
/**
|
|
54304
|
-
* Commit one rewind selection: fork the source before the candidate's turn,
|
|
54305
|
-
* commit the child, then restore the selected prompt into the editor.
|
|
54306
|
-
*
|
|
54307
|
-
* Transaction contract (review round 2 — the durable-ghost blocker): the
|
|
54308
|
-
* stale check and the seed derivation run BEFORE anything is created; the
|
|
54309
|
-
* create itself happens inside the runner's unified transaction, which
|
|
54310
|
-
* flushes the old session first, commits synchronously, and never rolls a
|
|
54311
|
-
* published child back. A stale selection therefore never creates a child
|
|
54312
|
-
* at all, a failed create leaves the source untouched, and once the child
|
|
54313
|
-
* exists it IS the surface. The source session is never modified at any
|
|
54314
|
-
* point.
|
|
54315
|
-
*
|
|
54316
|
-
* MUST run inside the caller's session-transition gate (the runner wraps
|
|
54317
|
-
* the commit in the gate): with the gate held, no other transition can
|
|
54318
|
-
* interleave, so the pre-create stale check is authoritative.
|
|
54319
|
-
*
|
|
54320
|
-
* @param host - the commit host.
|
|
54321
|
-
* @param source - the live agent whose session the picker listed.
|
|
54322
|
-
* @param candidate - the selected rewind point.
|
|
54323
|
-
* @param expected - the source identity captured when the picker opened
|
|
54324
|
-
* (a selection must not commit into a surface another path has switched
|
|
54325
|
-
* away from).
|
|
54326
|
-
* @param sourceSelection - the source's current effective selection, applied
|
|
54327
|
-
* after the historical seed when defined.
|
|
54328
|
-
* @returns the outcome; `stale` when the source no longer owns the surface
|
|
54329
|
-
* (detected before any child is created).
|
|
54330
|
-
*/
|
|
54331
|
-
async function commitRewind(host, source, candidate, expected, sourceSelection) {
|
|
54332
|
-
if (!isRewindIdentityCurrent(host.liveIdentity(), expected)) return { kind: "stale" };
|
|
54333
|
-
let seed;
|
|
54334
|
-
try {
|
|
54335
|
-
seed = rewindSeed(source.session.snapshotEvents(), candidate);
|
|
54336
|
-
} catch (error) {
|
|
54337
|
-
return {
|
|
54338
|
-
kind: "failed",
|
|
54339
|
-
message: safeErrorMessage$1(error)
|
|
54340
|
-
};
|
|
54341
|
-
}
|
|
54342
|
-
const sessionId = SessionId(`session-${randomUUID()}`);
|
|
54343
|
-
let composition;
|
|
54344
|
-
try {
|
|
54345
|
-
composition = await host.compose(host.sessionPreset?.(source.session));
|
|
54346
|
-
} catch (error) {
|
|
54347
|
-
return {
|
|
54348
|
-
kind: "failed",
|
|
54349
|
-
message: safeErrorMessage$1(error)
|
|
54350
|
-
};
|
|
54351
|
-
}
|
|
54352
|
-
const result = await host.transitionTo({
|
|
54353
|
-
target: {
|
|
54354
|
-
id: String(sessionId),
|
|
54355
|
-
header: { cwd: source.session.header.cwd || host.sessionCwd() }
|
|
54356
|
-
},
|
|
54357
|
-
...sourceSelection === void 0 ? {} : { inheritSelection: sourceSelection },
|
|
54358
|
-
create: () => createForkedAgent(host, source, seed, sessionId, composition.agentPreset)
|
|
54359
|
-
});
|
|
54360
|
-
if (!result.ok) {
|
|
54361
|
-
if (result.error instanceof LifecycleError && result.error.ownership === "superseded") return { kind: "superseded" };
|
|
54362
|
-
return {
|
|
54363
|
-
kind: "failed",
|
|
54364
|
-
message: result.message
|
|
54365
|
-
};
|
|
54366
|
-
}
|
|
54367
|
-
host.replaceDraft(candidate.editorText);
|
|
54368
|
-
return {
|
|
54369
|
-
kind: "rewound",
|
|
54370
|
-
sessionId: result.next.session.id,
|
|
54371
|
-
turn: candidate.turn,
|
|
54372
|
-
hasNonTextContent: candidate.hasNonTextContent
|
|
54373
|
-
};
|
|
54374
|
-
}
|
|
54375
|
-
//#endregion
|
|
54376
54346
|
//#region src/steer.ts
|
|
54377
54347
|
/**
|
|
54378
54348
|
* The Ctrl+S steer-all orchestration with re-validation. Extracted
|
|
@@ -58090,6 +58060,68 @@ var ModelPicker = class {
|
|
|
58090
58060
|
}
|
|
58091
58061
|
};
|
|
58092
58062
|
//#endregion
|
|
58063
|
+
//#region src/runtime/session-lifecycle-port.ts
|
|
58064
|
+
/** A lifecycle outcome that must ABORT the caller's transition, carrying the
|
|
58065
|
+
* two independent axes so a Remote caller never has to parse a string. */
|
|
58066
|
+
var LifecycleError = class extends Error {
|
|
58067
|
+
settlement;
|
|
58068
|
+
ownership;
|
|
58069
|
+
publishedSessionId;
|
|
58070
|
+
/** CORRELATION ONLY for an indeterminate create — never publication proof. */
|
|
58071
|
+
requestedSessionId;
|
|
58072
|
+
constructor(settlement, ownership, message, publishedSessionId, requestedSessionId) {
|
|
58073
|
+
super(message);
|
|
58074
|
+
this.name = "LifecycleError";
|
|
58075
|
+
this.settlement = settlement;
|
|
58076
|
+
this.ownership = ownership;
|
|
58077
|
+
this.publishedSessionId = publishedSessionId;
|
|
58078
|
+
this.requestedSessionId = requestedSessionId;
|
|
58079
|
+
}
|
|
58080
|
+
};
|
|
58081
|
+
/** Unwrap a CREATE result for the runner's transition: a `created` result that
|
|
58082
|
+
* still owns the surface yields the handle; everything else aborts with a
|
|
58083
|
+
* machine-readable `LifecycleError` (a superseded create is NOT committed to
|
|
58084
|
+
* the local surface, though its identity stays available). */
|
|
58085
|
+
function requireCreated(result) {
|
|
58086
|
+
const { ownership, outcome } = result;
|
|
58087
|
+
switch (outcome.kind) {
|
|
58088
|
+
case "created":
|
|
58089
|
+
if (ownership === "superseded") throw new LifecycleError("superseded", ownership, `Session "${outcome.handle.session.id}" was created, but the local surface was superseded — do not retry creation`, outcome.handle.session.id);
|
|
58090
|
+
return outcome.handle;
|
|
58091
|
+
case "published-with-error": throw new LifecycleError(outcome.kind, ownership, `Session "${outcome.sessionId}" was published, but workspace attach/reconcile failed: ${outcome.error.message} (${outcome.error.code}) — do not retry creation`, outcome.sessionId);
|
|
58092
|
+
case "indeterminate": throw new LifecycleError(outcome.kind, ownership, `the create is indeterminate — do not retry: ${outcome.error.message} (${outcome.error.code})`, void 0, outcome.requestedSessionId);
|
|
58093
|
+
case "rejected": throw new LifecycleError(outcome.kind, ownership, `${outcome.error.message} (${outcome.error.code})`, void 0);
|
|
58094
|
+
case "cancelled": throw new LifecycleError("cancelled", ownership, "the Session creation was cancelled before dispatch", void 0);
|
|
58095
|
+
}
|
|
58096
|
+
}
|
|
58097
|
+
/** Unwrap an OPEN result for the runner's transition. */
|
|
58098
|
+
function requireOpened(result) {
|
|
58099
|
+
const { ownership, outcome } = result;
|
|
58100
|
+
switch (outcome.kind) {
|
|
58101
|
+
case "opened":
|
|
58102
|
+
if (ownership === "superseded") throw new LifecycleError("superseded", ownership, "the Session was opened but the local surface was superseded", outcome.handle.session.id);
|
|
58103
|
+
return outcome.handle;
|
|
58104
|
+
case "unavailable": throw new LifecycleError("unavailable", ownership, outcome.message, void 0);
|
|
58105
|
+
case "cancelled": throw new LifecycleError("cancelled", ownership, "the Session open was cancelled before selection", void 0);
|
|
58106
|
+
}
|
|
58107
|
+
}
|
|
58108
|
+
/** Extract the Direct ownership handle (the real AgentHandle with
|
|
58109
|
+
* `dispose()`) from a lifecycle result. Accepts both the converged
|
|
58110
|
+
* SessionHandle and a legacy AgentHandle so transition code cannot lose the
|
|
58111
|
+
* ownership capability. Remote handles lack `direct` and yield undefined. */
|
|
58112
|
+
function ownerHandleOf(next) {
|
|
58113
|
+
const handle = next;
|
|
58114
|
+
if (handle.direct?.ownerHandle !== void 0) return handle.direct.ownerHandle;
|
|
58115
|
+
if (typeof handle.dispose === "function") return handle;
|
|
58116
|
+
}
|
|
58117
|
+
/** Extract the live in-process agent from a lifecycle result (the Direct
|
|
58118
|
+
* SessionHandle via `direct.agent`, or a legacy AgentHandle's `agent`).
|
|
58119
|
+
* Remote handles yield undefined. */
|
|
58120
|
+
function directAgentOf(next) {
|
|
58121
|
+
const handle = next;
|
|
58122
|
+
return handle.direct?.agent ?? handle.agent;
|
|
58123
|
+
}
|
|
58124
|
+
//#endregion
|
|
58093
58125
|
//#region src/runtime/read-error.ts
|
|
58094
58126
|
/**
|
|
58095
58127
|
* A typed semantic READ failure: the read was superseded by a connection or
|
|
@@ -58262,19 +58294,22 @@ function sessionLabelParts(label) {
|
|
|
58262
58294
|
* fork children, rewind branches AND subagents alike (plan §20: `origin`
|
|
58263
58295
|
* only decides the badge, `parentSession` decides the hierarchy) — hangs
|
|
58264
58296
|
* under its parent chain (depth = distance to the nearest root). Orphans —
|
|
58265
|
-
* a parent outside the shown window, or a missing parent id —
|
|
58266
|
-
* depth
|
|
58267
|
-
*
|
|
58297
|
+
* a parent outside the shown window, or a missing parent id — degrade to root
|
|
58298
|
+
* depth 0. The input order (newest first) is preserved per level; a `placed`
|
|
58299
|
+
* set guards against parent cycles in corrupt data, which are emitted from
|
|
58300
|
+
* root depth when no ordinary root reaches them.
|
|
58268
58301
|
* @param rows - the picker rows, newest first.
|
|
58269
58302
|
* @returns rows in display order with their tree depth.
|
|
58270
58303
|
*/
|
|
58271
58304
|
function buildSessionTree(rows) {
|
|
58305
|
+
const known = new Set(rows.map((row) => row.id));
|
|
58272
58306
|
const children = /* @__PURE__ */ new Map();
|
|
58273
|
-
|
|
58307
|
+
const roots = [];
|
|
58308
|
+
for (const row of rows) if (row.parentSession !== void 0 && known.has(row.parentSession)) {
|
|
58274
58309
|
const list = children.get(row.parentSession);
|
|
58275
58310
|
if (list === void 0) children.set(row.parentSession, [row]);
|
|
58276
58311
|
else list.push(row);
|
|
58277
|
-
}
|
|
58312
|
+
} else roots.push(row);
|
|
58278
58313
|
const result = [];
|
|
58279
58314
|
const placed = /* @__PURE__ */ new Set();
|
|
58280
58315
|
const place = (row, depth) => {
|
|
@@ -58286,8 +58321,8 @@ function buildSessionTree(rows) {
|
|
|
58286
58321
|
});
|
|
58287
58322
|
for (const child of children.get(row.id) ?? []) place(child, depth + 1);
|
|
58288
58323
|
};
|
|
58289
|
-
for (const
|
|
58290
|
-
for (const row of rows) if (row.
|
|
58324
|
+
for (const root of roots) place(root, 0);
|
|
58325
|
+
for (const row of rows) if (!placed.has(row.id)) place(row, 0);
|
|
58291
58326
|
return result;
|
|
58292
58327
|
}
|
|
58293
58328
|
/**
|
|
@@ -59010,19 +59045,10 @@ function sortCommands(commands) {
|
|
|
59010
59045
|
* temp/20260907/dsh-pi-tui-search-sessions-direct-boundary-plan.md).
|
|
59011
59046
|
* @module @xmoon76/dsh-pi-tui/commands
|
|
59012
59047
|
*/
|
|
59013
|
-
/** A balanced completed-turn prefix for forking: the log up to (and including)
|
|
59014
|
-
* the last `turn/end`. Undefined when no turn has completed yet.
|
|
59015
|
-
* @param events - the session log.
|
|
59016
|
-
* @returns the fork seed events, or undefined.
|
|
59017
|
-
*/
|
|
59018
|
-
function forkSeed(events) {
|
|
59019
|
-
for (let index = events.length - 1; index >= 0; index -= 1) if (events[index]?.type === "turn/end") return events.slice(0, index + 1);
|
|
59020
|
-
}
|
|
59021
59048
|
/** Shorten a session id for read-only display rows, capped at 28 characters. */
|
|
59022
59049
|
function displaySessionId(id) {
|
|
59023
59050
|
return id.length > 28 ? `${id.slice(0, 28)}…` : id;
|
|
59024
59051
|
}
|
|
59025
|
-
/** Session meta for a fresh/forked session: the cwd plus the preset id when composed. */
|
|
59026
59052
|
/**
|
|
59027
59053
|
* The `/sessions` category tabs (the 2026-08-22 plan, item 3): the session
|
|
59028
59054
|
* picker is a HUMAN surface, so subagent children never appear in either
|
|
@@ -59152,8 +59178,8 @@ function presetDisplayText(preset) {
|
|
|
59152
59178
|
}
|
|
59153
59179
|
/**
|
|
59154
59180
|
* Resolve one submission's delivery mode — the DSH WEB
|
|
59155
|
-
* `ComposerSubmissionPolicy.resolve()` contract (baseline
|
|
59156
|
-
* shared by every UI client):
|
|
59181
|
+
* `ComposerSubmissionPolicy.resolve()` contract (baseline established in DSH
|
|
59182
|
+
* `0.1.6-alpha.1`, shared by every UI client):
|
|
59157
59183
|
*
|
|
59158
59184
|
* ```text
|
|
59159
59185
|
* !running -> queue
|
|
@@ -61058,7 +61084,6 @@ function registerTuiCommands(runner, initial) {
|
|
|
61058
61084
|
text: `skill "${name}" is not invocable by the user`
|
|
61059
61085
|
};
|
|
61060
61086
|
const line = args.trim() === "" ? "/" + skill.name : "/" + skill.name + " " + args.trimStart();
|
|
61061
|
-
const releasePin = pinDraftAttachments(line, runner.imageStore, runner.fileStore);
|
|
61062
61087
|
const hostLoadsSkillBody = runner.catalog.skills.hostLoadsSkillBody(agent.session.id);
|
|
61063
61088
|
const fallbackBody = !hostLoadsSkillBody ? (() => {
|
|
61064
61089
|
const body = typeof skill.content === "string" && skill.content !== "" ? skill.content : skill.description;
|
|
@@ -61080,6 +61105,7 @@ function registerTuiCommands(runner, initial) {
|
|
|
61080
61105
|
}
|
|
61081
61106
|
});
|
|
61082
61107
|
})() : void 0;
|
|
61108
|
+
const releasePin = pinDraftAttachments(line, runner.imageStore, runner.fileStore);
|
|
61083
61109
|
let userMessage;
|
|
61084
61110
|
try {
|
|
61085
61111
|
userMessage = await runner.prepareDraftMessage(line);
|
|
@@ -61546,7 +61572,7 @@ function registerTuiCommands(runner, initial) {
|
|
|
61546
61572
|
await runner.awaitPendingDefaultWrite(signal);
|
|
61547
61573
|
return runner.agents.create({
|
|
61548
61574
|
sessionId: String(sessionId),
|
|
61549
|
-
|
|
61575
|
+
cwd,
|
|
61550
61576
|
agentPreset: resolved.id
|
|
61551
61577
|
});
|
|
61552
61578
|
}
|
|
@@ -62146,48 +62172,19 @@ function registerTuiCommands(runner, initial) {
|
|
|
62146
62172
|
});
|
|
62147
62173
|
commands.register({
|
|
62148
62174
|
name: "fork",
|
|
62149
|
-
description: "Fork this session at the
|
|
62150
|
-
handler:
|
|
62175
|
+
description: "Fork this session at the Host-selected completed-turn boundary",
|
|
62176
|
+
handler: async () => {
|
|
62151
62177
|
const source = runner.liveAgent;
|
|
62152
|
-
|
|
62153
|
-
if (seed === void 0 || source === void 0) return {
|
|
62178
|
+
if (source === void 0) return {
|
|
62154
62179
|
kind: "error",
|
|
62155
|
-
text: "no
|
|
62180
|
+
text: "no conversation to fork from"
|
|
62156
62181
|
};
|
|
62157
|
-
|
|
62158
|
-
|
|
62159
|
-
|
|
62160
|
-
const sourcePreset = runner.currentPreset();
|
|
62161
|
-
const result = await runner.transitionTo({
|
|
62162
|
-
target: {
|
|
62163
|
-
id: String(sessionId),
|
|
62164
|
-
header: { cwd: childCwd }
|
|
62165
|
-
},
|
|
62166
|
-
...sourceSelection === void 0 ? {} : { inheritSelection: sourceSelection },
|
|
62167
|
-
create: () => createForkedAgent(runner, source, seed, sessionId, sourcePreset)
|
|
62168
|
-
});
|
|
62169
|
-
if (!result.ok) {
|
|
62170
|
-
if (result.error instanceof LifecycleError) {
|
|
62171
|
-
runner.diag.warn("fork create did not own the surface", {
|
|
62172
|
-
settlement: result.error.settlement,
|
|
62173
|
-
ownership: result.error.ownership,
|
|
62174
|
-
publishedSessionId: result.error.publishedSessionId,
|
|
62175
|
-
requestedSessionId: result.error.requestedSessionId
|
|
62176
|
-
});
|
|
62177
|
-
if (result.error.ownership === "superseded") return { kind: "success" };
|
|
62178
|
-
}
|
|
62179
|
-
return {
|
|
62180
|
-
kind: "error",
|
|
62181
|
-
text: result.message
|
|
62182
|
-
};
|
|
62183
|
-
}
|
|
62184
|
-
runner.imageStore.clearUnpinned();
|
|
62185
|
-
runner.fileStore?.clearUnpinned();
|
|
62186
|
-
return {
|
|
62187
|
-
kind: "success",
|
|
62188
|
-
text: `forked as ${result.next.session.id}`
|
|
62182
|
+
if (runner.forkSession === void 0) return {
|
|
62183
|
+
kind: "error",
|
|
62184
|
+
text: "session fork is unavailable in this backend"
|
|
62189
62185
|
};
|
|
62190
|
-
|
|
62186
|
+
return runner.forkSession(source.session.id);
|
|
62187
|
+
}
|
|
62191
62188
|
});
|
|
62192
62189
|
commands.register({
|
|
62193
62190
|
name: "rewind",
|
|
@@ -64294,7 +64291,8 @@ function isRemoteBusinessRefusalCode(code) {
|
|
|
64294
64291
|
/**
|
|
64295
64292
|
* The interactive subagent viewer's HUMAN PROMPT delivery seam — the pure,
|
|
64296
64293
|
* dependency-injected layer between TuiApp's semantic submit event and the
|
|
64297
|
-
* DSH official subagent control API (plan §17;
|
|
64294
|
+
* DSH official subagent control API (plan §17; introduced in DSH
|
|
64295
|
+
* `0.1.6-alpha.1`). It owns
|
|
64298
64296
|
* validation, the `ctx.subagents.prompt(...)` call, and error
|
|
64299
64297
|
* classification; the runner owns the surface effects (draft restore,
|
|
64300
64298
|
* notices, stale-viewer guards).
|
|
@@ -65334,6 +65332,23 @@ var DirectPendingInputReader = class {
|
|
|
65334
65332
|
};
|
|
65335
65333
|
//#endregion
|
|
65336
65334
|
//#region src/runtime/direct/session-lifecycle-direct.ts
|
|
65335
|
+
/**
|
|
65336
|
+
* The Direct session lifecycle (D2.1–D2.4) — the in-process
|
|
65337
|
+
* implementation of `SessionLifecycle` over the dsh `agents` service.
|
|
65338
|
+
*
|
|
65339
|
+
* D2.3 moved ordinary create/open activation requirements INSIDE the adapter.
|
|
65340
|
+
* D2.4 does the same for fork: this adapter maps the official Host fork
|
|
65341
|
+
* algorithm (observation, completed-turn boundary, seed, lineage, preset,
|
|
65342
|
+
* default model and workspace attachment) without exposing those details
|
|
65343
|
+
* through the semantic port.
|
|
65344
|
+
*
|
|
65345
|
+
* The adapter is the only module in these lifecycle paths that touches `ctx`
|
|
65346
|
+
* and the preset composition. It keeps the real AgentHandle ownership escape
|
|
65347
|
+
* required by the current Direct runner.
|
|
65348
|
+
*
|
|
65349
|
+
* Full contract: docs/client-server-migration.md + docs/client-server-coupling.md.
|
|
65350
|
+
* @module @xmoon76/dsh-pi-tui/runtime/direct/session-lifecycle-direct
|
|
65351
|
+
*/
|
|
65337
65352
|
/** The Direct backend's session lifecycle: the `ctx.agents` service behind
|
|
65338
65353
|
* the semantic `SessionLifecycle` interface. The preset composition (and
|
|
65339
65354
|
* with it the agent-setup callback) is resolved here from the request's
|
|
@@ -65343,9 +65358,11 @@ var DirectPendingInputReader = class {
|
|
|
65343
65358
|
var DirectSessionLifecycle = class {
|
|
65344
65359
|
ctx;
|
|
65345
65360
|
compose;
|
|
65346
|
-
|
|
65361
|
+
ownerPool;
|
|
65362
|
+
constructor(ctx, compose, ownerPool) {
|
|
65347
65363
|
this.ctx = ctx;
|
|
65348
65364
|
this.compose = compose;
|
|
65365
|
+
this.ownerPool = ownerPool;
|
|
65349
65366
|
}
|
|
65350
65367
|
/** The in-process activation fallback: the Host global default, never a
|
|
65351
65368
|
* cross-backend request input. */
|
|
@@ -65366,18 +65383,14 @@ var DirectSessionLifecycle = class {
|
|
|
65366
65383
|
const agentOptions = this.agentOptions();
|
|
65367
65384
|
try {
|
|
65368
65385
|
const composition = await this.compose(request.agentPreset);
|
|
65369
|
-
const metaPreset = request.meta.agentPreset;
|
|
65370
|
-
if (metaPreset !== void 0 && composition.agentPreset !== void 0 && metaPreset !== composition.agentPreset) throw new Error(`create meta.agentPreset "${String(metaPreset)}" disagrees with the semantic agentPreset "${composition.agentPreset}"`);
|
|
65371
65386
|
const handle = await agents.create({
|
|
65372
65387
|
sessionId: SessionId(request.sessionId),
|
|
65373
|
-
meta:
|
|
65374
|
-
...request.
|
|
65375
|
-
agentPreset: composition.agentPreset
|
|
65388
|
+
meta: {
|
|
65389
|
+
...request.cwd === void 0 ? {} : { cwd: request.cwd },
|
|
65390
|
+
...composition.agentPreset === void 0 ? {} : { agentPreset: composition.agentPreset }
|
|
65376
65391
|
},
|
|
65377
65392
|
agentOptions,
|
|
65378
65393
|
setup: composition.setup,
|
|
65379
|
-
seed: request.seed,
|
|
65380
|
-
...request.inheritedEventCount === void 0 ? {} : { inheritedEventCount: SessionLogOffset(request.inheritedEventCount) },
|
|
65381
65394
|
signal: request.signal
|
|
65382
65395
|
});
|
|
65383
65396
|
return {
|
|
@@ -65401,11 +65414,50 @@ var DirectSessionLifecycle = class {
|
|
|
65401
65414
|
return currentCreateRejected("session/create-failed", safeErrorMessage$1(error));
|
|
65402
65415
|
}
|
|
65403
65416
|
}
|
|
65417
|
+
/**
|
|
65418
|
+
* Await a pending Direct owner release, honoring the caller's cancellation.
|
|
65419
|
+
* @returns `true` when the release settled (or there was none), `false` when
|
|
65420
|
+
* the signal aborted first — the caller then settles as `cancelled`
|
|
65421
|
+
* instead of staying pending behind a slow retirement.
|
|
65422
|
+
*/
|
|
65423
|
+
async waitForReleaseOrAbort(sessionId, signal) {
|
|
65424
|
+
const release = this.ownerPool?.waitForRelease?.(sessionId);
|
|
65425
|
+
if (release === void 0) return true;
|
|
65426
|
+
if (signal === void 0) {
|
|
65427
|
+
await release;
|
|
65428
|
+
return true;
|
|
65429
|
+
}
|
|
65430
|
+
if (signal.aborted) return false;
|
|
65431
|
+
let onAbort;
|
|
65432
|
+
const aborted = new Promise((resolve) => {
|
|
65433
|
+
onAbort = () => resolve(false);
|
|
65434
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
65435
|
+
});
|
|
65436
|
+
try {
|
|
65437
|
+
return await Promise.race([release.then(() => true), aborted]);
|
|
65438
|
+
} finally {
|
|
65439
|
+
if (onAbort !== void 0) signal.removeEventListener("abort", onAbort);
|
|
65440
|
+
}
|
|
65441
|
+
}
|
|
65404
65442
|
async open(request) {
|
|
65405
65443
|
if (Boolean(request.signal?.aborted)) return {
|
|
65406
65444
|
ownership: "current",
|
|
65407
65445
|
outcome: { kind: "cancelled" }
|
|
65408
65446
|
};
|
|
65447
|
+
const parked = this.ownerPool?.claim(request.sessionId);
|
|
65448
|
+
if (parked !== void 0) return {
|
|
65449
|
+
ownership: "current",
|
|
65450
|
+
outcome: {
|
|
65451
|
+
kind: "opened",
|
|
65452
|
+
handle: {
|
|
65453
|
+
session: { id: String(parked.agent.session.id) },
|
|
65454
|
+
direct: {
|
|
65455
|
+
agent: parked.agent,
|
|
65456
|
+
ownerHandle: parked
|
|
65457
|
+
}
|
|
65458
|
+
}
|
|
65459
|
+
}
|
|
65460
|
+
};
|
|
65409
65461
|
const agents = this.ctx.get("agents");
|
|
65410
65462
|
if (agents === void 0) return {
|
|
65411
65463
|
ownership: "current",
|
|
@@ -65415,6 +65467,10 @@ var DirectSessionLifecycle = class {
|
|
|
65415
65467
|
}
|
|
65416
65468
|
};
|
|
65417
65469
|
const agentOptions = this.agentOptions();
|
|
65470
|
+
if (!await this.waitForReleaseOrAbort(request.sessionId, request.signal)) return {
|
|
65471
|
+
ownership: "current",
|
|
65472
|
+
outcome: { kind: "cancelled" }
|
|
65473
|
+
};
|
|
65418
65474
|
try {
|
|
65419
65475
|
const recorded = await recordedSessionPreset(this.ctx, request.sessionId, request.signal);
|
|
65420
65476
|
const composition = await this.compose(recorded);
|
|
@@ -65451,6 +65507,109 @@ var DirectSessionLifecycle = class {
|
|
|
65451
65507
|
};
|
|
65452
65508
|
}
|
|
65453
65509
|
}
|
|
65510
|
+
/** Map the official Host fork algorithm into the Direct implementation. The
|
|
65511
|
+
* only raw seed construction in the repository lives here, immediately next
|
|
65512
|
+
* to `agents.create`; commands and the semantic port see only the source id
|
|
65513
|
+
* and optional official anchor. */
|
|
65514
|
+
async fork(request) {
|
|
65515
|
+
const atSeq = request.atSeq;
|
|
65516
|
+
if (atSeq !== void 0 && (!Number.isSafeInteger(atSeq) || atSeq < 0)) return currentForkRejected("gateway/bad-request", "atSeq must be a non-negative safe integer");
|
|
65517
|
+
const agents = this.ctx.get("agents");
|
|
65518
|
+
if (agents === void 0) return currentForkRejected("gateway/internal", "agents service unavailable");
|
|
65519
|
+
const query = this.ctx.get("sessionQuery");
|
|
65520
|
+
if (query === void 0) return currentForkRejected("gateway/internal", "session query service unavailable");
|
|
65521
|
+
let source;
|
|
65522
|
+
try {
|
|
65523
|
+
source = await query.observeSession(SessionId(request.sourceSessionId));
|
|
65524
|
+
} catch (error) {
|
|
65525
|
+
return currentForkRejected((typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0) === "SESSION_QUERY_SESSION_NOT_FOUND" ? "session/not-found" : "gateway/internal", `fork source unavailable: ${safeErrorMessage$1(error)}`);
|
|
65526
|
+
}
|
|
65527
|
+
try {
|
|
65528
|
+
const lastSeq = source.events.at(-1)?.seq ?? -1;
|
|
65529
|
+
const boundary = (atSeq === void 0 ? void 0 : source.events.find((event) => event.type === "turn/end" && Number(event.seq) >= atSeq)) ?? (atSeq === void 0 || atSeq > Number(lastSeq) ? [...source.events].reverse().find((event) => event.type === "turn/end") : void 0);
|
|
65530
|
+
if (boundary === void 0) return currentForkRejected("session/fork-unavailable", atSeq !== void 0 && atSeq <= Number(lastSeq) ? `session "${request.sourceSessionId}" has not completed the turn containing event ${String(atSeq)}` : `session "${request.sourceSessionId}" has no completed turn to fork from`);
|
|
65531
|
+
let workspace;
|
|
65532
|
+
try {
|
|
65533
|
+
workspace = await this.forkWorkspace(source);
|
|
65534
|
+
} catch (error) {
|
|
65535
|
+
return currentForkRejected("gateway/internal", `failed to resolve fork workspace: ${safeErrorMessage$1(error)}`);
|
|
65536
|
+
}
|
|
65537
|
+
const preset = source.projections.values.agentPreset ?? void 0;
|
|
65538
|
+
let composition;
|
|
65539
|
+
try {
|
|
65540
|
+
composition = await this.compose(preset);
|
|
65541
|
+
} catch (error) {
|
|
65542
|
+
return currentForkRejected("gateway/internal", `failed to compose fork session: ${safeErrorMessage$1(error)}`);
|
|
65543
|
+
}
|
|
65544
|
+
const cut = Number(boundary.seq) + 1;
|
|
65545
|
+
const childMeta = {
|
|
65546
|
+
...source.header.cwd === void 0 ? {} : { cwd: source.header.cwd },
|
|
65547
|
+
parentSession: source.header.id,
|
|
65548
|
+
isSeeded: true,
|
|
65549
|
+
...composition.agentPreset === void 0 ? {} : { agentPreset: composition.agentPreset }
|
|
65550
|
+
};
|
|
65551
|
+
let handle;
|
|
65552
|
+
try {
|
|
65553
|
+
handle = await agents.create({
|
|
65554
|
+
sessionId: SessionId(`session-${randomUUID()}`),
|
|
65555
|
+
seed: source.events.slice(0, cut),
|
|
65556
|
+
inheritedEventCount: SessionLogOffset(cut),
|
|
65557
|
+
meta: childMeta,
|
|
65558
|
+
agentOptions: this.agentOptions(),
|
|
65559
|
+
setup: composition.setup
|
|
65560
|
+
});
|
|
65561
|
+
} catch (error) {
|
|
65562
|
+
return currentForkRejected("gateway/internal", `failed to fork session "${request.sourceSessionId}": ${safeErrorMessage$1(error)}`);
|
|
65563
|
+
}
|
|
65564
|
+
const child = {
|
|
65565
|
+
session: { id: String(handle.agent.session.id) },
|
|
65566
|
+
direct: {
|
|
65567
|
+
agent: handle.agent,
|
|
65568
|
+
ownerHandle: handle
|
|
65569
|
+
}
|
|
65570
|
+
};
|
|
65571
|
+
if (workspace !== void 0) try {
|
|
65572
|
+
await workspace.attachSession(SessionId(child.session.id));
|
|
65573
|
+
} catch (error) {
|
|
65574
|
+
return {
|
|
65575
|
+
ownership: "current",
|
|
65576
|
+
outcome: {
|
|
65577
|
+
kind: "published-with-error",
|
|
65578
|
+
sessionId: child.session.id,
|
|
65579
|
+
handle: child,
|
|
65580
|
+
error: {
|
|
65581
|
+
code: "session/workspace-attach-failed",
|
|
65582
|
+
message: `Session "${child.session.id}" was forked but could not attach to workspace "${workspace.id}": ${safeErrorMessage$1(error)}`,
|
|
65583
|
+
details: { workspaceId: workspace.id }
|
|
65584
|
+
}
|
|
65585
|
+
}
|
|
65586
|
+
};
|
|
65587
|
+
}
|
|
65588
|
+
return {
|
|
65589
|
+
ownership: "current",
|
|
65590
|
+
outcome: {
|
|
65591
|
+
kind: "forked",
|
|
65592
|
+
handle: child
|
|
65593
|
+
}
|
|
65594
|
+
};
|
|
65595
|
+
} finally {
|
|
65596
|
+
source[Symbol.dispose]();
|
|
65597
|
+
}
|
|
65598
|
+
}
|
|
65599
|
+
async forkWorkspace(source) {
|
|
65600
|
+
const registry = this.ctx.get("workspaceRegistry");
|
|
65601
|
+
if (registry === void 0) return void 0;
|
|
65602
|
+
const workspaces = registry.list();
|
|
65603
|
+
const direct = workspaces.find((workspace) => workspace.sessionIds.includes(source.header.id));
|
|
65604
|
+
if (direct !== void 0 || source.header.origin !== "subagent") return direct;
|
|
65605
|
+
const query = this.ctx.get("sessionQuery");
|
|
65606
|
+
if (query?.traceSession === void 0) return void 0;
|
|
65607
|
+
const lineage = await query.traceSession(source.header.id);
|
|
65608
|
+
for (const ancestor of lineage.ancestors) {
|
|
65609
|
+
const workspace = workspaces.find((candidate) => candidate.sessionIds.includes(ancestor.header.id));
|
|
65610
|
+
if (workspace !== void 0) return workspace;
|
|
65611
|
+
}
|
|
65612
|
+
}
|
|
65454
65613
|
};
|
|
65455
65614
|
/** A Direct create rejection that still owns the local surface. */
|
|
65456
65615
|
function currentCreateRejected(code, message) {
|
|
@@ -65465,6 +65624,18 @@ function currentCreateRejected(code, message) {
|
|
|
65465
65624
|
}
|
|
65466
65625
|
};
|
|
65467
65626
|
}
|
|
65627
|
+
function currentForkRejected(code, message) {
|
|
65628
|
+
return {
|
|
65629
|
+
ownership: "current",
|
|
65630
|
+
outcome: {
|
|
65631
|
+
kind: "rejected",
|
|
65632
|
+
error: {
|
|
65633
|
+
code,
|
|
65634
|
+
message
|
|
65635
|
+
}
|
|
65636
|
+
}
|
|
65637
|
+
};
|
|
65638
|
+
}
|
|
65468
65639
|
//#endregion
|
|
65469
65640
|
//#region src/runtime/direct/interaction-direct.ts
|
|
65470
65641
|
/** The Direct backend's interaction port: the ctx services/events behind
|
|
@@ -67552,24 +67723,112 @@ var CoalescingRefreshGate = class {
|
|
|
67552
67723
|
}
|
|
67553
67724
|
};
|
|
67554
67725
|
//#endregion
|
|
67726
|
+
//#region src/rewind.ts
|
|
67727
|
+
/** Whether one user/message event is a DIRECT human prompt. Injected context,
|
|
67728
|
+
* skill bodies, system reminders and goal continuations answer false. */
|
|
67729
|
+
function isHumanTurnMessage(event) {
|
|
67730
|
+
return event.data.source.kind === "user";
|
|
67731
|
+
}
|
|
67732
|
+
/** Whether a message is empty for rewind purposes: mirror the transcript's
|
|
67733
|
+
* finalized user-content visibility rule. */
|
|
67734
|
+
function isEmptyMessage(blocks) {
|
|
67735
|
+
return !userBlocksVisibleNow(blocks);
|
|
67736
|
+
}
|
|
67737
|
+
/** One-line, width-bounded preview of a prompt (whitespace collapsed). */
|
|
67738
|
+
function singleLinePreview(text) {
|
|
67739
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
67740
|
+
return flat.length > 120 ? `${flat.slice(0, 117)}…` : flat;
|
|
67741
|
+
}
|
|
67742
|
+
/**
|
|
67743
|
+
* Derive rewind candidates from a session event log.
|
|
67744
|
+
*
|
|
67745
|
+
* Only a completed human turn after a valid completed predecessor is
|
|
67746
|
+
* rewindable. A non-human closed turn still becomes a legal predecessor; an
|
|
67747
|
+
* open or malformed span never does. The first human turn therefore has no
|
|
67748
|
+
* candidate because official fork omission would mean "latest boundary", not
|
|
67749
|
+
* an empty prefix.
|
|
67750
|
+
*/
|
|
67751
|
+
function collectRewindCandidates(events) {
|
|
67752
|
+
const candidates = [];
|
|
67753
|
+
let lastValidClosedTurnEndSeq;
|
|
67754
|
+
let open;
|
|
67755
|
+
for (const event of events) {
|
|
67756
|
+
if (event.type === "turn/start") {
|
|
67757
|
+
open = {
|
|
67758
|
+
turn: event.data.turn,
|
|
67759
|
+
startSeq: Number(event.seq),
|
|
67760
|
+
predecessorEndSeq: lastValidClosedTurnEndSeq
|
|
67761
|
+
};
|
|
67762
|
+
continue;
|
|
67763
|
+
}
|
|
67764
|
+
if (event.type === "turn/end") {
|
|
67765
|
+
if (open !== void 0 && event.data.turn !== open.turn) continue;
|
|
67766
|
+
if (open !== void 0) {
|
|
67767
|
+
const primary = open.primary;
|
|
67768
|
+
if (primary !== void 0 && open.predecessorEndSeq !== void 0) {
|
|
67769
|
+
const editorText = textOf(primary.blocks);
|
|
67770
|
+
candidates.push({
|
|
67771
|
+
turnStartSeq: open.startSeq,
|
|
67772
|
+
forkAtSeq: open.predecessorEndSeq,
|
|
67773
|
+
turn: open.turn,
|
|
67774
|
+
messageSeq: primary.seq,
|
|
67775
|
+
editorText,
|
|
67776
|
+
preview: singleLinePreview(editorText),
|
|
67777
|
+
hasNonTextContent: primary.blocks.some((block) => block.type !== "text")
|
|
67778
|
+
});
|
|
67779
|
+
}
|
|
67780
|
+
lastValidClosedTurnEndSeq = Number(event.seq);
|
|
67781
|
+
}
|
|
67782
|
+
open = void 0;
|
|
67783
|
+
continue;
|
|
67784
|
+
}
|
|
67785
|
+
if (event.type === "user/message" && open !== void 0 && open.primary === void 0) {
|
|
67786
|
+
const blocks = event.data.content;
|
|
67787
|
+
if (isHumanTurnMessage(event) && !isEmptyMessage(blocks)) open.primary = {
|
|
67788
|
+
seq: Number(event.seq),
|
|
67789
|
+
blocks
|
|
67790
|
+
};
|
|
67791
|
+
}
|
|
67792
|
+
}
|
|
67793
|
+
return candidates.reverse();
|
|
67794
|
+
}
|
|
67795
|
+
/** One picker row for a candidate. The value remains the selected turn-start
|
|
67796
|
+
* identity; the workflow resolves the captured row before dispatch. */
|
|
67797
|
+
function rewindPickerItem(candidate) {
|
|
67798
|
+
const tag = candidate.hasNonTextContent ? "[attachment] " : "";
|
|
67799
|
+
const preview = candidate.preview === "" && candidate.hasNonTextContent ? "(attachment only)" : candidate.preview;
|
|
67800
|
+
return {
|
|
67801
|
+
value: String(candidate.turnStartSeq),
|
|
67802
|
+
label: `turn ${candidate.turn} · ${tag}${preview}`,
|
|
67803
|
+
...candidate.hasNonTextContent ? { description: "non-text content is not re-staged on rewind" } : {}
|
|
67804
|
+
};
|
|
67805
|
+
}
|
|
67806
|
+
//#endregion
|
|
67807
|
+
//#region src/session-fork.ts
|
|
67808
|
+
/** A captured fork/rewind intent still owns the visible surface only while all
|
|
67809
|
+
* three local identity facts remain unchanged. */
|
|
67810
|
+
function isRewindIdentityCurrent(live, expected) {
|
|
67811
|
+
return live.sessionId === expected.sessionId && live.generation === expected.generation && live.navigationEpoch === expected.navigationEpoch;
|
|
67812
|
+
}
|
|
67813
|
+
//#endregion
|
|
67555
67814
|
//#region src/transition-gate.ts
|
|
67556
67815
|
/**
|
|
67557
67816
|
* The process-local session-transition gate: ONE writer for the live
|
|
67558
67817
|
* session at a time.
|
|
67559
67818
|
*
|
|
67560
|
-
* Every path that changes which session owns the surface — /new,
|
|
67561
|
-
*
|
|
67562
|
-
*
|
|
67563
|
-
*
|
|
67564
|
-
*
|
|
67819
|
+
* Every ordinary path that changes which session owns the surface — /new,
|
|
67820
|
+
* `/sessions` switch/resume, and first-session creation — must run its whole
|
|
67821
|
+
* workflow (quiesce old → preflight → create/resume → COMMIT (assign new +
|
|
67822
|
+
* generation bump) → retire old) inside {@link SessionTransitionGate.run}.
|
|
67823
|
+
* Host fork dispatch runs outside this FIFO; only adoption of a returned Direct
|
|
67824
|
+
* child enters the gate. Without this gate, two interleaved transitions can:
|
|
67565
67825
|
*
|
|
67566
67826
|
* - create a child whose metadata mixes two surfaces (parent captured
|
|
67567
67827
|
* before an await, cwd read after a concurrent switch — the P2 "cwd
|
|
67568
67828
|
* race");
|
|
67569
|
-
* - publish
|
|
67570
|
-
* its seed) and then detect
|
|
67571
|
-
*
|
|
67572
|
-
* the agent but does NOT delete the persisted session;
|
|
67829
|
+
* - publish an ordinary child (session/created → persistence starts writing
|
|
67830
|
+
* its seed) and then detect a stale surface only AFTER the create, leaving
|
|
67831
|
+
* two live Direct surfaces with no serialized adoption/retirement order;
|
|
67573
67832
|
* - pass a stale identity check and then yield inside the swap (flush /
|
|
67574
67833
|
* old-owner retirement), letting a second transition land in between and
|
|
67575
67834
|
* later get overwritten by the first continuation.
|
|
@@ -67657,7 +67916,8 @@ var PendingSubmissions = class {
|
|
|
67657
67916
|
text: input.text,
|
|
67658
67917
|
createdAt: input.createdAt,
|
|
67659
67918
|
...input.sessionId === void 0 ? {} : { sessionId: input.sessionId },
|
|
67660
|
-
...input.generation === void 0 ? {} : { generation: input.generation }
|
|
67919
|
+
...input.generation === void 0 ? {} : { generation: input.generation },
|
|
67920
|
+
...input.foldableText === void 0 ? {} : { foldableText: input.foldableText }
|
|
67661
67921
|
});
|
|
67662
67922
|
}
|
|
67663
67923
|
/** Remove one echo after its durable `user/message` became renderable. */
|
|
@@ -67723,7 +67983,8 @@ function buildPendingPresentation(input) {
|
|
|
67723
67983
|
id: item.id,
|
|
67724
67984
|
...item.rpcId === void 0 ? {} : { rpcId: item.rpcId },
|
|
67725
67985
|
text: input.textOf(item.content),
|
|
67726
|
-
status: "steering"
|
|
67986
|
+
status: "steering",
|
|
67987
|
+
foldableText: isTextOnlyContent(item.content)
|
|
67727
67988
|
});
|
|
67728
67989
|
}
|
|
67729
67990
|
const authoritativeRpcIds = /* @__PURE__ */ new Set();
|
|
@@ -67742,7 +68003,8 @@ function buildPendingPresentation(input) {
|
|
|
67742
68003
|
rpcId: echo.requestId,
|
|
67743
68004
|
text,
|
|
67744
68005
|
local: true,
|
|
67745
|
-
status: echo.placement === "transcript" ? "sending" : "steering"
|
|
68006
|
+
status: echo.placement === "transcript" ? "sending" : "steering",
|
|
68007
|
+
...echo.foldableText === void 0 ? {} : { foldableText: echo.foldableText }
|
|
67746
68008
|
});
|
|
67747
68009
|
}
|
|
67748
68010
|
return {
|
|
@@ -67752,6 +68014,17 @@ function buildPendingPresentation(input) {
|
|
|
67752
68014
|
};
|
|
67753
68015
|
}
|
|
67754
68016
|
/**
|
|
68017
|
+
* Whether an authoritative pending occurrence's structural content is
|
|
68018
|
+
* text-only, so the ephemeral row may join the long-user visual-row fold. Any
|
|
68019
|
+
* unrecognized block (image/file/attachment/generic non-text) fails open to
|
|
68020
|
+
* the FULL presentation — the pending row must never be folded only to
|
|
68021
|
+
* materialize as a full mixed-content durable bubble.
|
|
68022
|
+
*/
|
|
68023
|
+
function isTextOnlyContent(content) {
|
|
68024
|
+
if (content.length === 0) return false;
|
|
68025
|
+
return content.every((block) => typeof block === "object" && block !== null && block.type === "text");
|
|
68026
|
+
}
|
|
68027
|
+
/**
|
|
67755
68028
|
* Render one local echo's display text. The Direct ledger carries attachment
|
|
67756
68029
|
* markers inside `text`; the official Remote pending submission does not, so
|
|
67757
68030
|
* its structured attachments are appended here — an image-only echo must never
|
|
@@ -67780,7 +68053,8 @@ var DirectSubmissionPresentation = class {
|
|
|
67780
68053
|
placement: echo.placement,
|
|
67781
68054
|
text: echo.text,
|
|
67782
68055
|
createdAt: echo.createdAt,
|
|
67783
|
-
attachments: []
|
|
68056
|
+
attachments: [],
|
|
68057
|
+
...echo.foldableText === void 0 ? {} : { foldableText: echo.foldableText }
|
|
67784
68058
|
}));
|
|
67785
68059
|
}
|
|
67786
68060
|
};
|
|
@@ -67852,8 +68126,8 @@ var SubmitLatencyTracker = class {
|
|
|
67852
68126
|
//#endregion
|
|
67853
68127
|
//#region src/transition.ts
|
|
67854
68128
|
/**
|
|
67855
|
-
* The session
|
|
67856
|
-
* /
|
|
68129
|
+
* The ordinary session-switch transaction — the canonical ordering shared by
|
|
68130
|
+
* /new and `/sessions` switch/resume:
|
|
67857
68131
|
*
|
|
67858
68132
|
* ```text
|
|
67859
68133
|
* 1. quiesce old (whenIdle + final flush)
|
|
@@ -68434,8 +68708,8 @@ function packageVersion() {
|
|
|
68434
68708
|
}
|
|
68435
68709
|
/**
|
|
68436
68710
|
* The welcome card's version line: the installed dsh version plus the
|
|
68437
|
-
* bundle's own version (header-badge parity — `dsh-0.1.6-alpha.
|
|
68438
|
-
* tui-v0.4.7-alpha.
|
|
68711
|
+
* bundle's own version (header-badge parity — `dsh-0.1.6-alpha.2 ·
|
|
68712
|
+
* tui-v0.4.7-alpha.2`). Without a resolvable dsh launcher it degrades to
|
|
68439
68713
|
* the bundle version alone.
|
|
68440
68714
|
* @returns the combined version string.
|
|
68441
68715
|
*/
|
|
@@ -68793,8 +69067,8 @@ async function composeAgent(ctx, installSelection, presetId, focusState, diag) {
|
|
|
68793
69067
|
};
|
|
68794
69068
|
}
|
|
68795
69069
|
/**
|
|
68796
|
-
* The preset a persisted session actually runs, read from DSH 0.1.6
|
|
68797
|
-
*
|
|
69070
|
+
* The preset a persisted session actually runs, read from the DSH 0.1.6 V3
|
|
69071
|
+
* session projection (header initialization plus the latest selection event).
|
|
68798
69072
|
* @param ctx - the runner context.
|
|
68799
69073
|
* @param sessionId - the persisted session id.
|
|
68800
69074
|
* @returns the recorded preset id, or undefined to compose the default.
|
|
@@ -68904,6 +69178,69 @@ function apply(ctx, config) {
|
|
|
68904
69178
|
}
|
|
68905
69179
|
const transitionGate = new SessionTransitionGate();
|
|
68906
69180
|
const operationBarrier = new SessionOperationBarrier();
|
|
69181
|
+
const parkedDirectOwners = /* @__PURE__ */ new Map();
|
|
69182
|
+
const pendingOwnerReleases = /* @__PURE__ */ new Map();
|
|
69183
|
+
/** Register a pending release for one session; returns the resolver the
|
|
69184
|
+
* retirement calls when it has finished. */
|
|
69185
|
+
const beginOwnerRelease = (sessionId) => {
|
|
69186
|
+
let resolve;
|
|
69187
|
+
const promise = new Promise((settle) => {
|
|
69188
|
+
resolve = settle;
|
|
69189
|
+
});
|
|
69190
|
+
const releases = pendingOwnerReleases.get(sessionId) ?? /* @__PURE__ */ new Set();
|
|
69191
|
+
releases.add(promise);
|
|
69192
|
+
pendingOwnerReleases.set(sessionId, releases);
|
|
69193
|
+
return () => {
|
|
69194
|
+
releases.delete(promise);
|
|
69195
|
+
if (releases.size === 0) pendingOwnerReleases.delete(sessionId);
|
|
69196
|
+
resolve();
|
|
69197
|
+
};
|
|
69198
|
+
};
|
|
69199
|
+
const waitForOwnerRelease = async (sessionId) => {
|
|
69200
|
+
while (true) {
|
|
69201
|
+
const releases = pendingOwnerReleases.get(sessionId);
|
|
69202
|
+
if (releases === void 0 || releases.size === 0) return;
|
|
69203
|
+
await Promise.allSettled([...releases]);
|
|
69204
|
+
}
|
|
69205
|
+
};
|
|
69206
|
+
/** Admission-time pin for one `/fork` source. It is held from the moment the
|
|
69207
|
+
* fork is admitted — before the child is even created — and released when
|
|
69208
|
+
* the fork settles AND, if it committed, after the source owner's
|
|
69209
|
+
* retirement finishes. While it is held, an open/resume of that session
|
|
69210
|
+
* waits for the release instead of resuming a live, lease-held owner. */
|
|
69211
|
+
const beginForkSourcePin = (sessionId) => {
|
|
69212
|
+
const finish = beginOwnerRelease(sessionId);
|
|
69213
|
+
const state = {
|
|
69214
|
+
released: false,
|
|
69215
|
+
retirementOwnsRelease: false
|
|
69216
|
+
};
|
|
69217
|
+
return {
|
|
69218
|
+
state,
|
|
69219
|
+
release: () => {
|
|
69220
|
+
if (state.released) return;
|
|
69221
|
+
state.released = true;
|
|
69222
|
+
finish();
|
|
69223
|
+
}
|
|
69224
|
+
};
|
|
69225
|
+
};
|
|
69226
|
+
const directOwnerPool = {
|
|
69227
|
+
claim: (sessionId) => {
|
|
69228
|
+
const handle = parkedDirectOwners.get(sessionId);
|
|
69229
|
+
if (handle !== void 0) parkedDirectOwners.delete(sessionId);
|
|
69230
|
+
return handle;
|
|
69231
|
+
},
|
|
69232
|
+
park: (handle) => {
|
|
69233
|
+
const sessionId = String(handle.agent.session.id);
|
|
69234
|
+
const previous = parkedDirectOwners.get(sessionId);
|
|
69235
|
+
if (previous !== void 0 && previous !== handle) throw new Error(`duplicate parked Direct owner for session "${sessionId}"`);
|
|
69236
|
+
parkedDirectOwners.set(sessionId, handle);
|
|
69237
|
+
},
|
|
69238
|
+
waitForRelease: waitForOwnerRelease
|
|
69239
|
+
};
|
|
69240
|
+
let navigationEpoch = 0;
|
|
69241
|
+
const pendingForks = /* @__PURE__ */ new Set();
|
|
69242
|
+
const pendingSettlementWork = /* @__PURE__ */ new Set();
|
|
69243
|
+
const pendingSourceRetirements = /* @__PURE__ */ new Set();
|
|
68907
69244
|
const pendingQueueRecalls = [];
|
|
68908
69245
|
const settlePendingQueueRecalls = (committed) => {
|
|
68909
69246
|
const recalls = pendingQueueRecalls.splice(0);
|
|
@@ -68915,6 +69252,17 @@ function apply(ctx, config) {
|
|
|
68915
69252
|
if (retirementPromise !== void 0) return retirementPromise;
|
|
68916
69253
|
retirementPromise = (async () => {
|
|
68917
69254
|
if (transitionGate?.pending === true) liveAgent?.cancel({ kind: "user" });
|
|
69255
|
+
const retireParked = async (agent, handle) => retireDirectOwnedSession({
|
|
69256
|
+
cancel: () => agent.cancel({ kind: "user" }),
|
|
69257
|
+
whenIdle: () => agent.whenIdle(),
|
|
69258
|
+
drainDescendants: async () => {
|
|
69259
|
+
await ctx.get("subagents")?.drainContinuableDescendants?.([agent]);
|
|
69260
|
+
},
|
|
69261
|
+
flush: async () => {
|
|
69262
|
+
await sessions.flush(agent.session);
|
|
69263
|
+
},
|
|
69264
|
+
disposeOwner: () => handle.dispose()
|
|
69265
|
+
});
|
|
68918
69266
|
const retire = async () => {
|
|
68919
69267
|
const agent = liveAgent;
|
|
68920
69268
|
const handle = liveHandle;
|
|
@@ -68962,7 +69310,18 @@ function apply(ctx, config) {
|
|
|
68962
69310
|
return report;
|
|
68963
69311
|
};
|
|
68964
69312
|
try {
|
|
68965
|
-
|
|
69313
|
+
while (pendingForks.size > 0) await Promise.allSettled([...pendingForks]);
|
|
69314
|
+
while (pendingSettlementWork.size > 0) await Promise.allSettled([...pendingSettlementWork]);
|
|
69315
|
+
while (pendingSourceRetirements.size > 0) await Promise.allSettled([...pendingSourceRetirements]);
|
|
69316
|
+
return await transitionGate.run(() => operationBarrier.runTransition(async () => {
|
|
69317
|
+
const failures = [...(await retire()).failures];
|
|
69318
|
+
for (const [sessionId, parked] of parkedDirectOwners) {
|
|
69319
|
+
parkedDirectOwners.delete(sessionId);
|
|
69320
|
+
const report = await retireParked(parked.agent, parked);
|
|
69321
|
+
failures.push(...report.failures);
|
|
69322
|
+
}
|
|
69323
|
+
return { failures };
|
|
69324
|
+
}));
|
|
68966
69325
|
} catch (error) {
|
|
68967
69326
|
diag.error("retire barrier failed", { error: safeErrorMessage$1(error) });
|
|
68968
69327
|
return { failures: [{
|
|
@@ -69125,7 +69484,7 @@ function apply(ctx, config) {
|
|
|
69125
69484
|
flushSession: async (session) => {
|
|
69126
69485
|
await sessions.flush(session);
|
|
69127
69486
|
}
|
|
69128
|
-
}), directPendingInputReader, directSessionWriter, new DirectSessionLifecycle(ctx, (presetId) => compose(presetId)), new DirectInteractionPort(ctx, (sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0), new DirectCatalogPort(ctx, (sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0, modelSelections, diag), new DirectConfigPort(ctx, tuiSettings, (sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0), new DirectHostFilePort((sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0), new DirectSessionArchive(ctx), new DirectHostCommandPort(ctx, (sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0));
|
|
69487
|
+
}), directPendingInputReader, directSessionWriter, new DirectSessionLifecycle(ctx, (presetId) => compose(presetId), directOwnerPool), new DirectInteractionPort(ctx, (sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0), new DirectCatalogPort(ctx, (sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0, modelSelections, diag), new DirectConfigPort(ctx, tuiSettings, (sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0), new DirectHostFilePort((sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0), new DirectSessionArchive(ctx), new DirectHostCommandPort(ctx, (sessionId) => liveAgent?.session.id === sessionId ? liveAgent : void 0));
|
|
69129
69488
|
const userFooterCustomItemsForSave = () => {
|
|
69130
69489
|
const raw = backend.config.footerCustomItems.rawForPersistence();
|
|
69131
69490
|
if (raw.kind === "unavailable") throw new Error("custom footer definitions unavailable; settings write aborted");
|
|
@@ -69312,59 +69671,27 @@ function apply(ctx, config) {
|
|
|
69312
69671
|
...currentPreset() === void 0 ? {} : { preset: currentPreset() }
|
|
69313
69672
|
});
|
|
69314
69673
|
};
|
|
69315
|
-
/** The
|
|
69316
|
-
*
|
|
69317
|
-
*
|
|
69318
|
-
*
|
|
69319
|
-
*
|
|
69320
|
-
* with another: a fork child could be created (and its seed published
|
|
69321
|
-
* to persistence) before a stale check sees the surface already moved —
|
|
69322
|
-
* `dispose()` stops the agent but never deletes the persisted session —
|
|
69323
|
-
* and a stale identity check could pass, then yield across an await
|
|
69324
|
-
* inside the commit preparation, letting a concurrent switch land and
|
|
69325
|
-
* later get overwritten. The rewind commit holds the gate from BEFORE
|
|
69326
|
-
* `createForkedAgent` (the create itself is inside the exclusive
|
|
69327
|
-
* section), so a stale rewind never creates a child at all.
|
|
69674
|
+
/** The transition gate protects ordinary session surface changes — `/new`,
|
|
69675
|
+
* `/sessions` switch/open and first-session creation — from interleaving.
|
|
69676
|
+
* Fork dispatch is intentionally outside this destructive queue: Host
|
|
69677
|
+
* publication may outlive local navigation, while the visible adoption and
|
|
69678
|
+
* old-owner retirement use a short gated handoff.
|
|
69328
69679
|
* @see SessionTransitionGate */
|
|
69329
|
-
/**
|
|
69330
|
-
*
|
|
69331
|
-
*
|
|
69332
|
-
*
|
|
69333
|
-
*
|
|
69334
|
-
*
|
|
69335
|
-
* The ordering is the whole point (review rounds 2–3):
|
|
69336
|
-
*
|
|
69337
|
-
* 1. QUIESCE OLD — `whenIdle()` then the FINAL flush. `dispose()` is
|
|
69338
|
-
* an async quiescence and a cancelled RUNNING turn appends its
|
|
69339
|
-
* closure events (interrupted assistant/message, step/end,
|
|
69340
|
-
* turn/end) in finally blocks — the old agent must be idle before
|
|
69341
|
-
* the flush so those closures are never split across the switch.
|
|
69342
|
-
* Old-idle-then-flush closes that window; may fail → abort with
|
|
69343
|
-
* ZERO child side effects.
|
|
69344
|
-
* 2. caller `prepare` (rewind's stale gate, switch pre-checks)
|
|
69345
|
-
* — may fail → abort;
|
|
69346
|
-
* 3. create/resume the CHILD — may fail → abort; once it SUCCEEDS the
|
|
69347
|
-
* child is published (session/created → persistence may already
|
|
69348
|
-
* write its seed) and there is NO failure path afterwards that may
|
|
69349
|
-
* be interpreted as "the child never happened" (`dispose()` stops
|
|
69350
|
-
* an agent but never deletes a persisted session; dsh has no
|
|
69351
|
-
* durable rollback);
|
|
69352
|
-
* 4. COMMIT — a synchronous critical section (generation bump, live
|
|
69353
|
-
* handle/agent replacement) with no awaits between its steps;
|
|
69354
|
-
* 5. RETIRE — retire the old Direct owner in the fixed order
|
|
69355
|
-
* (cancel → idle → drain continuable descendants → final flush →
|
|
69356
|
-
* dispose — see src/runtime/direct/owned-session-retirement.ts),
|
|
69357
|
-
* then child whenIdle, surface rebuild, catalog refresh —
|
|
69358
|
-
* failures WARN ONLY, the committed child always stands.
|
|
69359
|
-
*
|
|
69360
|
-
* Must be called inside {@link transitionGate} (via
|
|
69361
|
-
* `withSessionTransition` or the rewind commit's own gate wrapper).
|
|
69362
|
-
*/
|
|
69680
|
+
/** The ordinary session-transition transaction. Its canonical ordering
|
|
69681
|
+
* lives in `runTransitionTo` (src/transition.ts — unit-tested): quiesce and
|
|
69682
|
+
* flush the old owner, run caller preflight, create/open the child, commit
|
|
69683
|
+
* the visible handle synchronously, then retire the old Direct owner and
|
|
69684
|
+
* refresh the child surface. Published children are never treated as if
|
|
69685
|
+
* they did not exist, and callers use this only inside the gate. */
|
|
69363
69686
|
/** Extract the live in-process agent from a transition next value: the
|
|
69364
69687
|
* Direct SessionHandle carries it via direct.agent; an AgentHandle IS the
|
|
69365
|
-
* agent handle. Remote
|
|
69366
|
-
*
|
|
69688
|
+
* agent handle. A Remote handle carries no Direct agent: it carries exactly one
|
|
69689
|
+
* Client generation reference (`SessionHandle.client`), which a composing
|
|
69690
|
+
* Remote runner WILL release through `clientOwnerOf()`. This Direct runner does
|
|
69691
|
+
* not take that owner over yet — the Remote caller-side handoff is deferred to
|
|
69692
|
+
* Remote production composition (M2+/M3). */
|
|
69367
69693
|
const transitionTo = async (steps) => {
|
|
69694
|
+
navigationEpoch += 1;
|
|
69368
69695
|
const from = liveAgent?.session.id;
|
|
69369
69696
|
const opening = {
|
|
69370
69697
|
id: steps.target.id,
|
|
@@ -69390,18 +69717,6 @@ function apply(ctx, config) {
|
|
|
69390
69717
|
liveAgent = directAgentOf(next);
|
|
69391
69718
|
if (cleanedUp) return;
|
|
69392
69719
|
completionController.setLiveAgent(liveAgent.id);
|
|
69393
|
-
if (steps.inheritSelection !== void 0) {
|
|
69394
|
-
const target = directAgentOf(next);
|
|
69395
|
-
const folded = foldPendingModelSelection(target.session.snapshotEvents());
|
|
69396
|
-
if (!sameModelSelection(folded.pending ?? folded.lastUsed, steps.inheritSelection)) try {
|
|
69397
|
-
modelSelections.selectForNextRequest(target, steps.inheritSelection);
|
|
69398
|
-
} catch (error) {
|
|
69399
|
-
diag.error("transition selection seed failed", {
|
|
69400
|
-
session: target.session.id,
|
|
69401
|
-
error: safeErrorMessage$1(error)
|
|
69402
|
-
});
|
|
69403
|
-
}
|
|
69404
|
-
}
|
|
69405
69720
|
},
|
|
69406
69721
|
retireOld: async (next) => {
|
|
69407
69722
|
const retired = [];
|
|
@@ -69432,7 +69747,6 @@ function apply(ctx, config) {
|
|
|
69432
69747
|
} catch (error) {
|
|
69433
69748
|
retired.push(`surface rebuild: ${safeErrorMessage$1(error)}`);
|
|
69434
69749
|
}
|
|
69435
|
-
if (openingSession === opening) openingSession = void 0;
|
|
69436
69750
|
try {
|
|
69437
69751
|
await refreshLiveCatalog(directAgentOf(next));
|
|
69438
69752
|
} catch (error) {
|
|
@@ -69465,15 +69779,18 @@ function apply(ctx, config) {
|
|
|
69465
69779
|
* failure (unknown session, broken log, preset mount) returns an error
|
|
69466
69780
|
* string so callers' `.then(error => ...)` need no rejection path. The
|
|
69467
69781
|
* whole switch (compose → resume → commit) runs inside the
|
|
69468
|
-
* session-transition gate, so it can never interleave with
|
|
69469
|
-
*
|
|
69470
|
-
const switchSession = (sessionId) =>
|
|
69471
|
-
|
|
69472
|
-
|
|
69473
|
-
|
|
69474
|
-
|
|
69475
|
-
|
|
69476
|
-
|
|
69782
|
+
* session-transition gate, so it can never interleave with another
|
|
69783
|
+
* ordinary transition (the single-writer rule). */
|
|
69784
|
+
const switchSession = (sessionId) => {
|
|
69785
|
+
navigationEpoch += 1;
|
|
69786
|
+
return transitionGate.run(() => operationBarrier.runTransition(async () => {
|
|
69787
|
+
try {
|
|
69788
|
+
return await switchSessionLocked(sessionId);
|
|
69789
|
+
} finally {
|
|
69790
|
+
settlePendingQueueRecalls(false);
|
|
69791
|
+
}
|
|
69792
|
+
}));
|
|
69793
|
+
};
|
|
69477
69794
|
const switchSessionLocked = async (sessionId) => {
|
|
69478
69795
|
if (liveAgent !== void 0 && liveAgent.session.id === sessionId) return "already on this session";
|
|
69479
69796
|
try {
|
|
@@ -69719,6 +70036,249 @@ function apply(ctx, config) {
|
|
|
69719
70036
|
signal
|
|
69720
70037
|
}))
|
|
69721
70038
|
};
|
|
70039
|
+
let commandExecutionDepth = 0;
|
|
70040
|
+
const deferredSourceRetirements = [];
|
|
70041
|
+
const startSourceRetirement = (sessionId, retire) => {
|
|
70042
|
+
let pending;
|
|
70043
|
+
runOwned("fork source retirement", () => {
|
|
70044
|
+
const task = retire();
|
|
70045
|
+
pending = task;
|
|
70046
|
+
return task;
|
|
70047
|
+
}, {
|
|
70048
|
+
diag,
|
|
70049
|
+
sessionId: () => void 0
|
|
70050
|
+
});
|
|
70051
|
+
if (pending !== void 0) {
|
|
70052
|
+
const tracked = pending;
|
|
70053
|
+
pendingSourceRetirements.add(tracked);
|
|
70054
|
+
const untrack = () => {
|
|
70055
|
+
pendingSourceRetirements.delete(tracked);
|
|
70056
|
+
};
|
|
70057
|
+
observeSettled(tracked, untrack);
|
|
70058
|
+
}
|
|
70059
|
+
return pending;
|
|
70060
|
+
};
|
|
70061
|
+
/** Retire the source owner. A DSH command defers it to its own settlement
|
|
70062
|
+
* (the executor's `command/done` append must land first) and returns
|
|
70063
|
+
* `undefined`; outside a command the retirement promise is returned so the
|
|
70064
|
+
* caller keeps the baseline ordering.
|
|
70065
|
+
*
|
|
70066
|
+
* `finishRelease` settles the fork's admission pin when the retirement has
|
|
70067
|
+
* FINISHED — success or a CONTAINED failure. `retireDirectOwnedSession`
|
|
70068
|
+
* never rejects: a failed phase (notably `disposeOwner`) is recorded in its
|
|
70069
|
+
* report and diag-logged, and the pin still releases. That is deliberate:
|
|
70070
|
+
* holding the pin on a failed dispose would leave `open`/`resume` pending
|
|
70071
|
+
* forever, so the reopen instead proceeds and fails LOUDLY on the official
|
|
70072
|
+
* exclusive write claim (`session "X" is already owned by an active write
|
|
70073
|
+
* handle`). A failed dispose is a leaked-handle bug in its own right; the
|
|
70074
|
+
* retirement helper already contains and reports it. */
|
|
70075
|
+
const retireSourceOwnerAfterSettlement = (sessionId, retire, finishRelease) => {
|
|
70076
|
+
const run = async () => {
|
|
70077
|
+
try {
|
|
70078
|
+
await retire();
|
|
70079
|
+
} finally {
|
|
70080
|
+
finishRelease();
|
|
70081
|
+
}
|
|
70082
|
+
};
|
|
70083
|
+
if (commandExecutionDepth === 0) return startSourceRetirement(sessionId, run);
|
|
70084
|
+
deferredSourceRetirements.push({
|
|
70085
|
+
sessionId,
|
|
70086
|
+
retire: run
|
|
70087
|
+
});
|
|
70088
|
+
};
|
|
70089
|
+
/** Start every retirement queued by the settled command(s) and return the
|
|
70090
|
+
* started promises so the settlement can WAIT for them. */
|
|
70091
|
+
const flushSourceRetirementsAfterSettlement = () => {
|
|
70092
|
+
const started = [];
|
|
70093
|
+
while (deferredSourceRetirements.length > 0) {
|
|
70094
|
+
const entry = deferredSourceRetirements.shift();
|
|
70095
|
+
if (entry === void 0) continue;
|
|
70096
|
+
const pending = startSourceRetirement(entry.sessionId, entry.retire);
|
|
70097
|
+
if (pending !== void 0) started.push(pending);
|
|
70098
|
+
}
|
|
70099
|
+
return started;
|
|
70100
|
+
};
|
|
70101
|
+
/** Close one command-settlement window EXACTLY once and WAIT for the source
|
|
70102
|
+
* retirements it queued. The command workflow must not report completion —
|
|
70103
|
+
* nor release the submit FIFO — while the old owner still holds its write
|
|
70104
|
+
* lease, or an immediate reopen of that Session would be refused. The
|
|
70105
|
+
* executor's `command/done` append already happened inside the wrapped
|
|
70106
|
+
* execution, so this preserves durability AND makes `/fork`'s outward
|
|
70107
|
+
* completion imply the source is released. */
|
|
70108
|
+
const settleCommandExecution = async () => {
|
|
70109
|
+
commandExecutionDepth -= 1;
|
|
70110
|
+
if (commandExecutionDepth === 0) await Promise.allSettled(flushSourceRetirementsAfterSettlement());
|
|
70111
|
+
};
|
|
70112
|
+
const parkForkOwner = (handle) => {
|
|
70113
|
+
const owner = handle === void 0 ? void 0 : ownerHandleOf(handle);
|
|
70114
|
+
if (owner !== void 0) directOwnerPool.park(owner);
|
|
70115
|
+
};
|
|
70116
|
+
const forkNavigationCurrent = (expected) => isRewindIdentityCurrent({
|
|
70117
|
+
sessionId: liveAgent?.session.id,
|
|
70118
|
+
generation: sessionGeneration,
|
|
70119
|
+
navigationEpoch
|
|
70120
|
+
}, expected);
|
|
70121
|
+
const adoptFork = async (handle, expected, onAdopted, pin) => {
|
|
70122
|
+
let adopted = false;
|
|
70123
|
+
try {
|
|
70124
|
+
await transitionGate.run(() => operationBarrier.runTransition(async () => {
|
|
70125
|
+
if (cleanedUp || !forkNavigationCurrent(expected)) {
|
|
70126
|
+
parkForkOwner(handle);
|
|
70127
|
+
return;
|
|
70128
|
+
}
|
|
70129
|
+
const oldAgent = liveAgent;
|
|
70130
|
+
const oldHandle = liveHandle;
|
|
70131
|
+
const nextAgent = directAgentOf(handle);
|
|
70132
|
+
const nextHandle = ownerHandleOf(handle);
|
|
70133
|
+
if (nextAgent === void 0 || nextHandle === void 0) throw new Error(`forked session "${handle.session.id}" has no Direct owner`);
|
|
70134
|
+
settlePendingQueueRecalls(true);
|
|
70135
|
+
settleLocalSubmitAck("session forked");
|
|
70136
|
+
submitLatencyTracker.reset();
|
|
70137
|
+
bumpSessionGeneration();
|
|
70138
|
+
liveAgent = nextAgent;
|
|
70139
|
+
liveHandle = nextHandle;
|
|
70140
|
+
completionController.setLiveAgent(nextAgent.id);
|
|
70141
|
+
adopted = true;
|
|
70142
|
+
try {
|
|
70143
|
+
onAdopted?.();
|
|
70144
|
+
} catch (error) {
|
|
70145
|
+
diag.error("fork adoption callback failed after child commit", {
|
|
70146
|
+
error: safeErrorMessage$1(error),
|
|
70147
|
+
session: nextAgent.session.id
|
|
70148
|
+
});
|
|
70149
|
+
}
|
|
70150
|
+
if (oldAgent !== void 0 && oldHandle !== void 0) {
|
|
70151
|
+
if (pin !== void 0) pin.state.retirementOwnsRelease = true;
|
|
70152
|
+
const retirement = retireSourceOwnerAfterSettlement(oldAgent.session.id, async () => {
|
|
70153
|
+
const report = await retireDirectOwnedSession({
|
|
70154
|
+
cancel: () => oldAgent.cancel({ kind: "user" }),
|
|
70155
|
+
whenIdle: () => oldAgent.whenIdle(),
|
|
70156
|
+
drainDescendants: async () => {
|
|
70157
|
+
await ctx.get("subagents")?.drainContinuableDescendants?.([oldAgent]);
|
|
70158
|
+
},
|
|
70159
|
+
flush: async () => {
|
|
70160
|
+
await sessions.flush(oldAgent.session);
|
|
70161
|
+
},
|
|
70162
|
+
disposeOwner: () => oldHandle.dispose()
|
|
70163
|
+
});
|
|
70164
|
+
if (report.failures.length > 0) diag.error("fork old-owner retirement failed (child committed)", {
|
|
70165
|
+
from: oldAgent.session.id,
|
|
70166
|
+
failures: report.failures
|
|
70167
|
+
});
|
|
70168
|
+
}, pin?.release ?? (() => {}));
|
|
70169
|
+
if (retirement !== void 0) await retirement;
|
|
70170
|
+
}
|
|
70171
|
+
let aborted = false;
|
|
70172
|
+
try {
|
|
70173
|
+
aborted = await whenIdleOrAbort(nextAgent, lifecycleController.signal);
|
|
70174
|
+
} catch (error) {
|
|
70175
|
+
diag.error("fork child quiescence failed after commit", {
|
|
70176
|
+
error: safeErrorMessage$1(error),
|
|
70177
|
+
session: nextAgent.session.id
|
|
70178
|
+
});
|
|
70179
|
+
}
|
|
70180
|
+
if (!aborted) try {
|
|
70181
|
+
await initLiveSession(nextAgent);
|
|
70182
|
+
} catch (error) {
|
|
70183
|
+
diag.error("fork child initialization failed after commit", {
|
|
70184
|
+
error: safeErrorMessage$1(error),
|
|
70185
|
+
session: nextAgent.session.id
|
|
70186
|
+
});
|
|
70187
|
+
}
|
|
70188
|
+
try {
|
|
70189
|
+
await refreshLiveCatalog(nextAgent);
|
|
70190
|
+
} catch (error) {
|
|
70191
|
+
diag.error("fork child catalog refresh failed after commit", {
|
|
70192
|
+
error: safeErrorMessage$1(error),
|
|
70193
|
+
session: nextAgent.session.id
|
|
70194
|
+
});
|
|
70195
|
+
}
|
|
70196
|
+
}));
|
|
70197
|
+
} catch (error) {
|
|
70198
|
+
if (!adopted) throw error;
|
|
70199
|
+
diag.error("fork post-commit handoff failed", {
|
|
70200
|
+
error: safeErrorMessage$1(error),
|
|
70201
|
+
session: handle.session.id
|
|
70202
|
+
});
|
|
70203
|
+
}
|
|
70204
|
+
return adopted;
|
|
70205
|
+
};
|
|
70206
|
+
const forkSession = async (sourceSessionId, atSeq, onAdopted, pickerIdentity) => {
|
|
70207
|
+
const before = {
|
|
70208
|
+
sessionId: liveAgent?.session.id,
|
|
70209
|
+
generation: sessionGeneration,
|
|
70210
|
+
navigationEpoch
|
|
70211
|
+
};
|
|
70212
|
+
const pickerCurrent = pickerIdentity === void 0 || isRewindIdentityCurrent(before, pickerIdentity);
|
|
70213
|
+
const expectedSessionId = pickerIdentity?.sessionId ?? before.sessionId;
|
|
70214
|
+
if (cleanedUp || !pickerCurrent || expectedSessionId !== sourceSessionId) return {
|
|
70215
|
+
kind: "error",
|
|
70216
|
+
text: "the session changed before fork dispatch"
|
|
70217
|
+
};
|
|
70218
|
+
const expected = {
|
|
70219
|
+
sessionId: expectedSessionId,
|
|
70220
|
+
generation: pickerIdentity?.generation ?? before.generation,
|
|
70221
|
+
navigationEpoch: ++navigationEpoch
|
|
70222
|
+
};
|
|
70223
|
+
const pin = beginForkSourcePin(sourceSessionId);
|
|
70224
|
+
let settleFork;
|
|
70225
|
+
let forkedHandle;
|
|
70226
|
+
const pending = new Promise((resolve) => {
|
|
70227
|
+
settleFork = resolve;
|
|
70228
|
+
});
|
|
70229
|
+
pendingForks.add(pending);
|
|
70230
|
+
try {
|
|
70231
|
+
const result = await backend.sessionLifecycle.fork({
|
|
70232
|
+
sourceSessionId,
|
|
70233
|
+
...atSeq === void 0 ? {} : { atSeq }
|
|
70234
|
+
});
|
|
70235
|
+
const outcome = result.outcome;
|
|
70236
|
+
if (outcome.kind === "unavailable") {
|
|
70237
|
+
if (result.ownership === "superseded" || !forkNavigationCurrent(expected)) return { kind: "success" };
|
|
70238
|
+
return {
|
|
70239
|
+
kind: "error",
|
|
70240
|
+
text: outcome.message
|
|
70241
|
+
};
|
|
70242
|
+
}
|
|
70243
|
+
if (outcome.kind === "rejected" || outcome.kind === "indeterminate" || outcome.kind === "published-with-error") {
|
|
70244
|
+
if (outcome.kind === "published-with-error") parkForkOwner(outcome.handle);
|
|
70245
|
+
if (result.ownership === "superseded" || !forkNavigationCurrent(expected)) return { kind: "success" };
|
|
70246
|
+
return {
|
|
70247
|
+
kind: "error",
|
|
70248
|
+
text: `${outcome.error.message} (${outcome.error.code})`
|
|
70249
|
+
};
|
|
70250
|
+
}
|
|
70251
|
+
if (result.ownership === "superseded" || !forkNavigationCurrent(expected)) {
|
|
70252
|
+
parkForkOwner(outcome.handle);
|
|
70253
|
+
return {
|
|
70254
|
+
kind: "success",
|
|
70255
|
+
text: `forked as ${outcome.handle.session.id}; navigation stayed on the newer session`
|
|
70256
|
+
};
|
|
70257
|
+
}
|
|
70258
|
+
forkedHandle = outcome.handle;
|
|
70259
|
+
if (!await adoptFork(outcome.handle, expected, onAdopted, pin)) return {
|
|
70260
|
+
kind: "success",
|
|
70261
|
+
text: `forked as ${outcome.handle.session.id}`
|
|
70262
|
+
};
|
|
70263
|
+
draftImages.clearUnpinned();
|
|
70264
|
+
draftFiles.clearUnpinned();
|
|
70265
|
+
return {
|
|
70266
|
+
kind: "success",
|
|
70267
|
+
text: `forked as ${outcome.handle.session.id}`
|
|
70268
|
+
};
|
|
70269
|
+
} catch (error) {
|
|
70270
|
+
if (forkedHandle !== void 0) parkForkOwner(forkedHandle);
|
|
70271
|
+
if (!forkNavigationCurrent(expected)) return { kind: "success" };
|
|
70272
|
+
return {
|
|
70273
|
+
kind: "error",
|
|
70274
|
+
text: `fork failed: ${safeErrorMessage$1(error)}`
|
|
70275
|
+
};
|
|
70276
|
+
} finally {
|
|
70277
|
+
settleFork();
|
|
70278
|
+
pendingForks.delete(pending);
|
|
70279
|
+
if (!pin.state.retirementOwnsRelease) pin.release();
|
|
70280
|
+
}
|
|
70281
|
+
};
|
|
69722
70282
|
let localShellController;
|
|
69723
70283
|
/** Stop the captured live Agent through the SessionWriter seam. The
|
|
69724
70284
|
* operation barrier keeps the async outcome inside the same session
|
|
@@ -69806,7 +70366,7 @@ function apply(ctx, config) {
|
|
|
69806
70366
|
});
|
|
69807
70367
|
if (lifecycleController.signal.aborted) {
|
|
69808
70368
|
startupStatus.clear();
|
|
69809
|
-
if (liveAgent !== void 0
|
|
70369
|
+
if (liveAgent !== void 0 || liveHandle !== void 0 || parkedDirectOwners.size > 0 || pendingForks.size > 0) await retireOwnedSession();
|
|
69810
70370
|
else diag.dispose();
|
|
69811
70371
|
return;
|
|
69812
70372
|
}
|
|
@@ -70099,13 +70659,24 @@ function apply(ctx, config) {
|
|
|
70099
70659
|
};
|
|
70100
70660
|
/** The display text of one client-local submission echo: the draft text
|
|
70101
70661
|
* with its attachment placeholders expanded to compact markers, so an
|
|
70102
|
-
* attachment-only submission is never an empty pending row.
|
|
70103
|
-
|
|
70662
|
+
* attachment-only submission is never an empty pending row. The SAME
|
|
70663
|
+
* expansion decides the foldability fact: a submission carrying any
|
|
70664
|
+
* attachment marker is not text-only and must render in full. */
|
|
70665
|
+
const localEcho = (text) => {
|
|
70104
70666
|
const parts = [];
|
|
70667
|
+
let foldableText = true;
|
|
70105
70668
|
for (const segment of expandAttachmentPlaceholders(text, draftImages, draftFiles)) if (segment.type === "text") parts.push(segment.text);
|
|
70106
|
-
else if (segment.type === "image")
|
|
70107
|
-
|
|
70108
|
-
|
|
70669
|
+
else if (segment.type === "image") {
|
|
70670
|
+
foldableText = false;
|
|
70671
|
+
parts.push(`🖼️ ${segment.image.name ?? "image"}`);
|
|
70672
|
+
} else {
|
|
70673
|
+
foldableText = false;
|
|
70674
|
+
parts.push(`📄 ${segment.file.name} · ${formatBytes(segment.file.byteLength)}`);
|
|
70675
|
+
}
|
|
70676
|
+
return {
|
|
70677
|
+
text: parts.join(" "),
|
|
70678
|
+
foldableText
|
|
70679
|
+
};
|
|
70109
70680
|
};
|
|
70110
70681
|
/**
|
|
70111
70682
|
* Own pending input must become VISIBLE even when the reader deliberately
|
|
@@ -70657,10 +71228,12 @@ function apply(ctx, config) {
|
|
|
70657
71228
|
* feedback and the durable row replaces it.
|
|
70658
71229
|
*/
|
|
70659
71230
|
const beginLocalSubmission = (requestId, text, placement, sessionId, generation, ackToken) => {
|
|
71231
|
+
const echo = localEcho(text);
|
|
70660
71232
|
pendingSubmissions.begin({
|
|
70661
71233
|
requestId,
|
|
70662
71234
|
placement,
|
|
70663
|
-
text:
|
|
71235
|
+
text: echo.text,
|
|
71236
|
+
foldableText: echo.foldableText,
|
|
70664
71237
|
createdAt: Date.now(),
|
|
70665
71238
|
...sessionId === void 0 ? {} : { sessionId },
|
|
70666
71239
|
generation
|
|
@@ -70835,7 +71408,6 @@ function apply(ctx, config) {
|
|
|
70835
71408
|
* this submission; bound for the command execution so a TUI-owned
|
|
70836
71409
|
* skill handler accepts it instead of re-deriving it. */
|
|
70837
71410
|
const dispatchViaSession = (text, persistHistory, delivery) => {
|
|
70838
|
-
const submitTurn = takeSubmitTurn();
|
|
70839
71411
|
const submittedAgent = liveAgent;
|
|
70840
71412
|
const submittedGeneration = sessionGeneration;
|
|
70841
71413
|
let submitTurnTransferred = false;
|
|
@@ -70861,6 +71433,7 @@ function apply(ctx, config) {
|
|
|
70861
71433
|
};
|
|
70862
71434
|
let commandHealthRef;
|
|
70863
71435
|
let planeAdvertised = false;
|
|
71436
|
+
const submitTurn = takeSubmitTurn();
|
|
70864
71437
|
runOwned("submit", () => runReservedSubmit({
|
|
70865
71438
|
reserve: (t) => {
|
|
70866
71439
|
try {
|
|
@@ -70975,28 +71548,42 @@ function apply(ctx, config) {
|
|
|
70975
71548
|
const commandPlaneLine = commandPlaneOwnsLine();
|
|
70976
71549
|
planeAdvertised = commandPlaneLine && wasAdvertisedAtSubmit;
|
|
70977
71550
|
const tuiOwnedCommand = parsedAtSubmit !== void 0 && (LOCAL_COMMANDS.has(parsedAtSubmit.name) || isSkillWrapperName?.(parsedAtSubmit.name) === true);
|
|
70978
|
-
|
|
70979
|
-
|
|
70980
|
-
|
|
70981
|
-
|
|
70982
|
-
|
|
70983
|
-
if (tuiOwnedCommand) return commands.execute(agent, toggled, submittedAttachments, signal).then((execution) => {
|
|
70984
|
-
return execution === void 0 ? {
|
|
71551
|
+
commandExecutionDepth += 1;
|
|
71552
|
+
let settled;
|
|
71553
|
+
try {
|
|
71554
|
+
settled = Promise.resolve(withCommandDelivery(delivery, () => {
|
|
71555
|
+
if (!commandPlaneLine || parsedAtSubmit === void 0) return Promise.resolve({
|
|
70985
71556
|
kind: "committed",
|
|
70986
71557
|
matched: false
|
|
70987
|
-
}
|
|
70988
|
-
|
|
70989
|
-
|
|
70990
|
-
|
|
70991
|
-
|
|
70992
|
-
|
|
70993
|
-
|
|
70994
|
-
|
|
70995
|
-
|
|
70996
|
-
|
|
70997
|
-
|
|
71558
|
+
});
|
|
71559
|
+
if (tuiOwnedCommand) return commands.execute(agent, toggled, submittedAttachments, signal).then((execution) => {
|
|
71560
|
+
return execution === void 0 ? {
|
|
71561
|
+
kind: "committed",
|
|
71562
|
+
matched: false
|
|
71563
|
+
} : {
|
|
71564
|
+
kind: "committed",
|
|
71565
|
+
matched: true,
|
|
71566
|
+
execution
|
|
71567
|
+
};
|
|
71568
|
+
});
|
|
71569
|
+
return operationBarrier.runWriter(agent.session.id, () => backend.hostCommand.execute({
|
|
71570
|
+
sessionId: agent.session.id,
|
|
71571
|
+
line: toggled,
|
|
71572
|
+
attachments: submittedAttachments,
|
|
71573
|
+
signal
|
|
71574
|
+
}));
|
|
70998
71575
|
}));
|
|
70999
|
-
})
|
|
71576
|
+
} catch (error) {
|
|
71577
|
+
commandExecutionDepth -= 1;
|
|
71578
|
+
throw error;
|
|
71579
|
+
}
|
|
71580
|
+
settled = settled.finally(settleCommandExecution);
|
|
71581
|
+
pendingSettlementWork.add(settled);
|
|
71582
|
+
const untrackSettlement = () => {
|
|
71583
|
+
pendingSettlementWork.delete(settled);
|
|
71584
|
+
};
|
|
71585
|
+
observeSettled(settled, untrackSettlement);
|
|
71586
|
+
return settled;
|
|
71000
71587
|
}, {
|
|
71001
71588
|
diag,
|
|
71002
71589
|
sessionId: () => agent.session.id,
|
|
@@ -71056,95 +71643,106 @@ function apply(ctx, config) {
|
|
|
71056
71643
|
if (sessionUnchanged({
|
|
71057
71644
|
agent,
|
|
71058
71645
|
generation
|
|
71059
|
-
}, liveAgent, sessionGeneration))
|
|
71060
|
-
|
|
71061
|
-
|
|
71062
|
-
|
|
71063
|
-
|
|
71064
|
-
|
|
71065
|
-
|
|
71066
|
-
|
|
71067
|
-
|
|
71068
|
-
|
|
71069
|
-
|
|
71070
|
-
|
|
71071
|
-
|
|
71072
|
-
|
|
71073
|
-
|
|
71074
|
-
|
|
71075
|
-
|
|
71076
|
-
|
|
71077
|
-
|
|
71078
|
-
|
|
71079
|
-
|
|
71080
|
-
|
|
71081
|
-
|
|
71082
|
-
|
|
71083
|
-
|
|
71084
|
-
|
|
71085
|
-
|
|
71086
|
-
|
|
71087
|
-
|
|
71088
|
-
|
|
71646
|
+
}, liveAgent, sessionGeneration)) {
|
|
71647
|
+
let nestedSettlement;
|
|
71648
|
+
runOwned("image submit", () => {
|
|
71649
|
+
const task = runReservedSubmit({
|
|
71650
|
+
reserve: () => fallbackPin,
|
|
71651
|
+
run: async () => {
|
|
71652
|
+
try {
|
|
71653
|
+
await operationBarrier.runWriter(agent.session.id, async () => {
|
|
71654
|
+
if (!localEchoInstalled) {
|
|
71655
|
+
beginLocalSubmission(submitRequestId, text, submissionPlacement("queue", agent.status === "running"), agent.session.id, generation, submitAckToken);
|
|
71656
|
+
localEchoInstalled = true;
|
|
71657
|
+
}
|
|
71658
|
+
const message = await prepareUserMessage(text, draftImages, submitDeps, { requestId: submitRequestId });
|
|
71659
|
+
if (cleanedUp) return;
|
|
71660
|
+
if (!sessionUnchanged({
|
|
71661
|
+
agent,
|
|
71662
|
+
generation
|
|
71663
|
+
}, liveAgent, sessionGeneration)) {
|
|
71664
|
+
const merged = mergeDraft(app.getDraft(), text);
|
|
71665
|
+
app.setEditorText(merged);
|
|
71666
|
+
settleLocalSubmission(submitRequestId);
|
|
71667
|
+
settleLocalSubmitAck("submit stale", {
|
|
71668
|
+
token: submitAckToken,
|
|
71669
|
+
terminal: true
|
|
71670
|
+
});
|
|
71671
|
+
app.notify(merged === text ? "the session changed while sending — try again" : "the draft changed while sending — review it before submitting again (the earlier text was preserved below)", "error");
|
|
71672
|
+
return;
|
|
71673
|
+
}
|
|
71674
|
+
submitLatencyTracker.mark(agent.session.id, "dispatch");
|
|
71675
|
+
const outcome = await backend.sessionWriter.prompt(agent.session.id, message, "queue");
|
|
71089
71676
|
if (cleanedUp) return;
|
|
71677
|
+
if (outcome.kind !== "committed") {
|
|
71678
|
+
if (outcome.kind === "indeterminate") {
|
|
71679
|
+
if (cleanedUp) return;
|
|
71680
|
+
settleLocalSubmission(submitRequestId);
|
|
71681
|
+
settleLocalSubmitAck("session write result indeterminate", {
|
|
71682
|
+
token: submitAckToken,
|
|
71683
|
+
terminal: true
|
|
71684
|
+
});
|
|
71685
|
+
app.notify("session write result is indeterminate — do not retry automatically", "error");
|
|
71686
|
+
return;
|
|
71687
|
+
}
|
|
71688
|
+
if (outcome.kind === "cancelled") throw cancellationError("session write cancelled");
|
|
71689
|
+
const failure = outcome.kind === "rejected" ? outcome.error.message : outcome.reason;
|
|
71690
|
+
throw new Error(failure);
|
|
71691
|
+
}
|
|
71692
|
+
consumeDraftAttachments(text, draftImages, draftFiles);
|
|
71693
|
+
});
|
|
71694
|
+
} catch (error) {
|
|
71695
|
+
if (cleanedUp) {
|
|
71696
|
+
fallbackPin();
|
|
71697
|
+
return;
|
|
71698
|
+
}
|
|
71699
|
+
if (error instanceof TransitionInProgressError) {
|
|
71700
|
+
fallbackPin();
|
|
71090
71701
|
settleLocalSubmission(submitRequestId);
|
|
71091
|
-
|
|
71702
|
+
refuseByTransitionFence(text, () => app.getDraft(), (t) => app.setEditorText(t), (m, k) => app.notify(m, k));
|
|
71703
|
+
settleLocalSubmitAck("submit refused by transition fence", {
|
|
71092
71704
|
token: submitAckToken,
|
|
71093
71705
|
terminal: true
|
|
71094
71706
|
});
|
|
71095
|
-
app.notify("session write result is indeterminate — do not retry automatically", "error");
|
|
71096
71707
|
return;
|
|
71097
71708
|
}
|
|
71098
|
-
|
|
71099
|
-
const failure = outcome.kind === "rejected" ? outcome.error.message : outcome.reason;
|
|
71100
|
-
throw new Error(failure);
|
|
71709
|
+
throw error;
|
|
71101
71710
|
}
|
|
71102
|
-
|
|
71711
|
+
},
|
|
71712
|
+
restore: (t) => restoreSubmissionDraft(t)
|
|
71713
|
+
}, text);
|
|
71714
|
+
nestedSettlement = task;
|
|
71715
|
+
const untrackNested = () => {
|
|
71716
|
+
if (nestedSettlement !== void 0) pendingSettlementWork.delete(nestedSettlement);
|
|
71717
|
+
};
|
|
71718
|
+
observeSettled(task, untrackNested);
|
|
71719
|
+
return task;
|
|
71720
|
+
}, {
|
|
71721
|
+
diag,
|
|
71722
|
+
sessionId: () => agent.session.id,
|
|
71723
|
+
onResult: () => {
|
|
71724
|
+
submitTurn.release();
|
|
71725
|
+
},
|
|
71726
|
+
onError: (error) => {
|
|
71727
|
+
submitTurn.release();
|
|
71728
|
+
settleLocalSubmission(submitRequestId);
|
|
71729
|
+
settleLocalSubmitAck("failure", {
|
|
71730
|
+
token: submitAckToken,
|
|
71731
|
+
terminal: true
|
|
71732
|
+
});
|
|
71733
|
+
notifySubmissionFailure(error);
|
|
71734
|
+
},
|
|
71735
|
+
onCancel: () => {
|
|
71736
|
+
submitTurn.release();
|
|
71737
|
+
settleLocalSubmission(submitRequestId);
|
|
71738
|
+
settleLocalSubmitAck("submit cancelled", {
|
|
71739
|
+
token: submitAckToken,
|
|
71740
|
+
terminal: true
|
|
71103
71741
|
});
|
|
71104
|
-
} catch (error) {
|
|
71105
|
-
if (cleanedUp) {
|
|
71106
|
-
fallbackPin();
|
|
71107
|
-
return;
|
|
71108
|
-
}
|
|
71109
|
-
if (error instanceof TransitionInProgressError) {
|
|
71110
|
-
fallbackPin();
|
|
71111
|
-
settleLocalSubmission(submitRequestId);
|
|
71112
|
-
refuseByTransitionFence(text, () => app.getDraft(), (t) => app.setEditorText(t), (m, k) => app.notify(m, k));
|
|
71113
|
-
settleLocalSubmitAck("submit refused by transition fence", {
|
|
71114
|
-
token: submitAckToken,
|
|
71115
|
-
terminal: true
|
|
71116
|
-
});
|
|
71117
|
-
return;
|
|
71118
|
-
}
|
|
71119
|
-
throw error;
|
|
71120
71742
|
}
|
|
71121
|
-
}
|
|
71122
|
-
|
|
71123
|
-
}
|
|
71124
|
-
diag,
|
|
71125
|
-
sessionId: () => agent.session.id,
|
|
71126
|
-
onResult: () => {
|
|
71127
|
-
submitTurn.release();
|
|
71128
|
-
},
|
|
71129
|
-
onError: (error) => {
|
|
71130
|
-
submitTurn.release();
|
|
71131
|
-
settleLocalSubmission(submitRequestId);
|
|
71132
|
-
settleLocalSubmitAck("failure", {
|
|
71133
|
-
token: submitAckToken,
|
|
71134
|
-
terminal: true
|
|
71135
|
-
});
|
|
71136
|
-
notifySubmissionFailure(error);
|
|
71137
|
-
},
|
|
71138
|
-
onCancel: () => {
|
|
71139
|
-
submitTurn.release();
|
|
71140
|
-
settleLocalSubmission(submitRequestId);
|
|
71141
|
-
settleLocalSubmitAck("submit cancelled", {
|
|
71142
|
-
token: submitAckToken,
|
|
71143
|
-
terminal: true
|
|
71144
|
-
});
|
|
71145
|
-
}
|
|
71146
|
-
});
|
|
71147
|
-
else {
|
|
71743
|
+
});
|
|
71744
|
+
if (nestedSettlement !== void 0) pendingSettlementWork.add(nestedSettlement);
|
|
71745
|
+
} else {
|
|
71148
71746
|
fallbackPin();
|
|
71149
71747
|
submitTurn.release();
|
|
71150
71748
|
const merged = mergeDraft(app.getDraft(), text);
|
|
@@ -73259,7 +73857,7 @@ function apply(ctx, config) {
|
|
|
73259
73857
|
lifecycleController.signal.throwIfAborted();
|
|
73260
73858
|
return requireCreated(await backend.sessionLifecycle.create({
|
|
73261
73859
|
sessionId: String(sessionId),
|
|
73262
|
-
|
|
73860
|
+
cwd: process.cwd(),
|
|
73263
73861
|
agentPreset: composition.agentPreset,
|
|
73264
73862
|
signal: lifecycleController.signal
|
|
73265
73863
|
}));
|
|
@@ -73425,9 +74023,9 @@ function apply(ctx, config) {
|
|
|
73425
74023
|
/**
|
|
73426
74024
|
* The conversation rewind picker (the ONE entry shared by the idle
|
|
73427
74025
|
* empty-editor double-Esc and `/rewind` — plan §22). Lists the completed
|
|
73428
|
-
* user turns of the live session; a selection
|
|
73429
|
-
*
|
|
73430
|
-
* the
|
|
74026
|
+
* user turns of the live session; a selection dispatches the semantic Host fork with the candidate's predecessor boundary
|
|
74027
|
+
* as an OWNED task with
|
|
74028
|
+
* the navigation identity gates. Sessionless (deferred start) it notifies
|
|
73431
74029
|
* and never creates a session.
|
|
73432
74030
|
*/
|
|
73433
74031
|
function openRewindPicker() {
|
|
@@ -73446,51 +74044,31 @@ function apply(ctx, config) {
|
|
|
73446
74044
|
return;
|
|
73447
74045
|
}
|
|
73448
74046
|
const sourceId = source.session.id;
|
|
73449
|
-
const
|
|
73450
|
-
|
|
73451
|
-
|
|
73452
|
-
|
|
73453
|
-
sessionPreset: (session) => session.id === sourceId ? currentPreset() : sessionPresetOf(ctx, session),
|
|
73454
|
-
compose,
|
|
73455
|
-
agents: lifecycleAgents,
|
|
73456
|
-
liveIdentity: () => ({
|
|
73457
|
-
sessionId: liveAgent?.session.id,
|
|
73458
|
-
generation: sessionGeneration
|
|
73459
|
-
}),
|
|
73460
|
-
transitionTo: (steps) => transitionTo(steps),
|
|
73461
|
-
replaceDraft: (text) => app.setDraft(text)
|
|
74047
|
+
const pickerIdentity = {
|
|
74048
|
+
sessionId: sourceId,
|
|
74049
|
+
generation: sessionGeneration,
|
|
74050
|
+
navigationEpoch
|
|
73462
74051
|
};
|
|
73463
74052
|
app.openPicker(candidates.map(rewindPickerItem), (value) => {
|
|
73464
74053
|
const candidate = candidates.find((item) => String(item.turnStartSeq) === value);
|
|
73465
74054
|
if (candidate === void 0) return;
|
|
73466
|
-
|
|
73467
|
-
|
|
73468
|
-
|
|
73469
|
-
|
|
73470
|
-
|
|
73471
|
-
generation: sourceGeneration
|
|
73472
|
-
}, sourceSelection);
|
|
73473
|
-
} finally {
|
|
73474
|
-
settlePendingQueueRecalls(false);
|
|
73475
|
-
}
|
|
73476
|
-
}));
|
|
73477
|
-
}, {
|
|
74055
|
+
let adopted = false;
|
|
74056
|
+
runOwned("conversation rewind", () => forkSession(sourceId, candidate.forkAtSeq, () => {
|
|
74057
|
+
adopted = true;
|
|
74058
|
+
app.setDraft(candidate.editorText);
|
|
74059
|
+
}, pickerIdentity), {
|
|
73478
74060
|
diag,
|
|
73479
74061
|
sessionId: () => sourceId,
|
|
73480
74062
|
onResult: (outcome) => {
|
|
73481
|
-
if (outcome.kind === "
|
|
73482
|
-
|
|
73483
|
-
app.notify(
|
|
74063
|
+
if (outcome.kind === "success" && adopted) {
|
|
74064
|
+
if (candidate.hasNonTextContent) app.notify(`rewound to turn ${candidate.turn}; original non-text content was not re-staged — review it before sending`, "error");
|
|
74065
|
+
else app.notify(`rewound to turn ${candidate.turn}`, "info");
|
|
73484
74066
|
return;
|
|
73485
74067
|
}
|
|
73486
|
-
if (outcome.kind === "
|
|
73487
|
-
app.notify(
|
|
73488
|
-
|
|
74068
|
+
if (outcome.kind === "error") {
|
|
74069
|
+
if (outcome.text === "the session changed before fork dispatch") app.notify("session changed — rewind cancelled", "info");
|
|
74070
|
+
else app.notify(outcome.text, "error");
|
|
73489
74071
|
}
|
|
73490
|
-
draftImages.clearUnpinned();
|
|
73491
|
-
draftFiles.clearUnpinned();
|
|
73492
|
-
if (outcome.hasNonTextContent) app.notify(`rewound to turn ${outcome.turn}; original non-text content was not re-staged — review it before sending`, "error");
|
|
73493
|
-
else app.notify(`rewound to turn ${outcome.turn}`, "info");
|
|
73494
74072
|
},
|
|
73495
74073
|
onError: (error) => {
|
|
73496
74074
|
app.notify(safeErrorMessage$1(error), "error");
|
|
@@ -73603,6 +74181,7 @@ function apply(ctx, config) {
|
|
|
73603
74181
|
}) : refresh(request);
|
|
73604
74182
|
},
|
|
73605
74183
|
switchSession,
|
|
74184
|
+
forkSession,
|
|
73606
74185
|
transitionTo,
|
|
73607
74186
|
currentPreset,
|
|
73608
74187
|
sessionBlank,
|
|
@@ -73968,4 +74547,4 @@ function apply(ctx, config) {
|
|
|
73968
74547
|
});
|
|
73969
74548
|
}
|
|
73970
74549
|
//#endregion
|
|
73971
|
-
export { shouldConsumeAdvertisedMiss as A,
|
|
74550
|
+
export { shouldConsumeAdvertisedMiss as A, PI_TUI_EXTENSIONS_SERVICE as B, normalizeSkillInvocation as C, resumeCommand as D, resolveSubmitDelivery as E, viewerActionCapability as F, describeKey as H, dshVersion as I, apply$1 as L, subagentJobViewHint as M, taskRowSelectionDisposition as N, runningProfile as O, teardownViewerForSessionSwap as P, inject$1 as R, name as S, resolveInitialCatalog as T, API_VERSION as V, interruptAgent as _, apply as a, isPlainExitPrompt as b, commandRejectsImages as c, contextRefreshKind as d, createViewerOpenToken as f, inject as g, foldQueueRows as h, SESSIONLESS_COMMANDS as i, subagentJobTranscriptId as j, settleCompactionSurface as k, compactingFromLog as l, foldCompactionEvent as m, HOST_COMMAND_CATALOG as n, busyAfterTurnBoundary as o, dangerCommand as p, LOCAL_COMMANDS as r, commandIsLocalForAttachments as s, Config as t, composeAgent as u, isBareCommandLine as v, recordedPreset as w, matchPendingSubagentCall as x, isLocalCommandLine as y, name$1 as z };
|