@xmoon76/dsh-pi-tui 0.1.3 → 0.1.5
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.md +1 -1
- package/dist/index.d.mts +187 -6
- package/dist/index.mjs +2527 -419
- package/package.json +5 -2
package/dist/index.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import { eastAsianWidth } from "get-east-asian-width";
|
|
|
29
29
|
import { performance } from "node:perf_hooks";
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
31
|
import { EventEmitter } from "events";
|
|
32
|
+
import { isUserInvocable } from "@deepseek-ai/dsh-skill";
|
|
32
33
|
//#region src/present.ts
|
|
33
34
|
/** Figma row titles per variant (design literals, not translatable copy). */
|
|
34
35
|
const VARIANT_TITLES = {
|
|
@@ -12607,33 +12608,56 @@ var TranscriptSearchComponent = class {
|
|
|
12607
12608
|
//#endregion
|
|
12608
12609
|
//#region src/question.ts
|
|
12609
12610
|
/**
|
|
12610
|
-
* The user-questions dialog flow (ask_user_question v2):
|
|
12611
|
-
*
|
|
12612
|
-
*
|
|
12613
|
-
*
|
|
12614
|
-
*
|
|
12611
|
+
* The user-questions dialog flow (ask_user_question v2): the component that
|
|
12612
|
+
* sits in the EDITOR SEAT (kimi's mountEditorReplacement pattern) while the
|
|
12613
|
+
* agent asks — one question at a time, drafts per question, whole batch
|
|
12614
|
+
* submitted at the end — the Web QuestionComposer semantics (per-question
|
|
12615
|
+
* paging, batch submit, skip) with pi/kimi keyboard ergonomics (↑↓
|
|
12616
|
+
* highlight, digits, real Input for free text, review page).
|
|
12615
12617
|
*
|
|
12616
|
-
* Pure component: the app layer owns the promise/abort plumbing
|
|
12617
|
-
* input here while the flow is active.
|
|
12618
|
+
* Pure component: the app layer owns the promise/abort plumbing, the seat
|
|
12619
|
+
* swap, and routes input here while the flow is active.
|
|
12618
12620
|
* @module @xmoon76/dsh-pi-tui/question
|
|
12619
12621
|
*/
|
|
12620
12622
|
/** The "type your own answer" row shown below options (pi isOther parity). */
|
|
12621
12623
|
const OTHER_ROW = "\0other";
|
|
12622
12624
|
/**
|
|
12623
|
-
*
|
|
12624
|
-
*
|
|
12625
|
-
*
|
|
12626
|
-
*
|
|
12625
|
+
* Default total physical-row budget of the question flow itself. The
|
|
12626
|
+
* QuestionFrame in tui-app.ts re-derives the budget from the terminal height
|
|
12627
|
+
* on every render (60% cap COLLAPSED, 8..24 content rows; up to 38 when
|
|
12628
|
+
* expanded) and pushes it through {@link QuestionFlow.setMaxRows}; 24 is the
|
|
12629
|
+
* fallback for direct renders.
|
|
12630
|
+
* The Frame wrapper adds its two border rows, and NOTHING clips the flow's
|
|
12631
|
+
* output in the editor-seat layout — so EVERYTHING (tabs, question, detail,
|
|
12632
|
+
* options with their descriptions, scroll marker, skipped note, hint) must
|
|
12633
|
+
* fit in the budget.
|
|
12627
12634
|
*/
|
|
12628
|
-
const
|
|
12635
|
+
const DEFAULT_BUDGET = 24;
|
|
12629
12636
|
/**
|
|
12630
|
-
*
|
|
12631
|
-
*
|
|
12632
|
-
*
|
|
12637
|
+
* Smallest supported content-row budget (a 16-row terminal caps the frame at
|
|
12638
|
+
* 10 rows). Below this the required rows (tabs, the question's first row,
|
|
12639
|
+
* the current option, the hint) cannot all coexist.
|
|
12633
12640
|
*/
|
|
12634
|
-
const
|
|
12635
|
-
/**
|
|
12636
|
-
|
|
12641
|
+
const MIN_SUPPORTED_BUDGET = 8;
|
|
12642
|
+
/**
|
|
12643
|
+
* Largest supported content-row budget: the EXPANDED frame cap (80% of the
|
|
12644
|
+
* terminal, at most 40 frame rows) minus the two frame borders. An explicit
|
|
12645
|
+
* body expand ('e' / marker click) grows the panel beyond the default 60%
|
|
12646
|
+
* cap — the user asked for the room; the budget math is linear and holds
|
|
12647
|
+
* for every budget >= MIN_SUPPORTED_BUDGET.
|
|
12648
|
+
*/
|
|
12649
|
+
const MAX_BUDGET = 38;
|
|
12650
|
+
/**
|
|
12651
|
+
* Cap for the WRAPPED page scrollport content (question + detail + options
|
|
12652
|
+
* with their descriptions). Beyond it the content ends in a `... more
|
|
12653
|
+
* content hidden` note — wrapping an unbounded 100KB detail every frame
|
|
12654
|
+
* would be wasteful. 256 rows keeps realistic questions AND all their
|
|
12655
|
+
* answerable options reachable (7 options × ~19 wrapped description rows
|
|
12656
|
+
* at 50 columns fits); the cap is a hard wall, so a page whose content
|
|
12657
|
+
* alone exceeds 256 rows ends in the note — that page is unreachable in
|
|
12658
|
+
* practice, and the note makes the cut non-silent.
|
|
12659
|
+
*/
|
|
12660
|
+
const MAX_CONTENT_ROWS = 256;
|
|
12637
12661
|
/**
|
|
12638
12662
|
* Budgeted push of `content` to `lines`, wrapping it to `width`: pushes at
|
|
12639
12663
|
* most `budget` PHYSICAL (wrapped) rows, appends a `... N more` marker when
|
|
@@ -12658,14 +12682,15 @@ function appendWrappedBudgeted(lines, firstPrefix, continuationPrefix, content,
|
|
|
12658
12682
|
return Math.max(0, budget - used);
|
|
12659
12683
|
}
|
|
12660
12684
|
/**
|
|
12661
|
-
*
|
|
12662
|
-
*
|
|
12663
|
-
*
|
|
12664
|
-
*
|
|
12665
|
-
*
|
|
12685
|
+
* Required-first push: the FIRST physical row always carries `content` —
|
|
12686
|
+
* never just a cut marker — while extra rows fill `budget` and a `... N
|
|
12687
|
+
* more` marker reports content that was itself cut, only when a row remains
|
|
12688
|
+
* after the content. Used for the question body and the highlighted option
|
|
12689
|
+
* label, whose first rows may never vanish (a 1-row budget shows the
|
|
12690
|
+
* content, not the marker).
|
|
12666
12691
|
* @returns the remaining budget (≥ 0).
|
|
12667
12692
|
*/
|
|
12668
|
-
function
|
|
12693
|
+
function appendContentFirst(lines, firstPrefix, continuationPrefix, content, width, budget) {
|
|
12669
12694
|
if (budget <= 0) return 0;
|
|
12670
12695
|
const prefixWidth = Math.max(visibleWidth(firstPrefix), visibleWidth(continuationPrefix));
|
|
12671
12696
|
const wrapped = wrapTextWithAnsi(content, Math.max(1, width - prefixWidth));
|
|
@@ -12723,7 +12748,47 @@ var QuestionFlow = class {
|
|
|
12723
12748
|
otherInput = new Input();
|
|
12724
12749
|
/** Review-page action highlight (0 = Submit, 1 = Cancel). */
|
|
12725
12750
|
submitIdx = 0;
|
|
12726
|
-
|
|
12751
|
+
/**
|
|
12752
|
+
* Current content-row budget (8..38). The editor-seat QuestionFrame in
|
|
12753
|
+
* tui-app.ts re-derives it from the terminal height on every render and
|
|
12754
|
+
* pushes it through {@link setMaxRows}; nothing clips the flow's output
|
|
12755
|
+
* in that layout, so every render must fit the budget exactly.
|
|
12756
|
+
*/
|
|
12757
|
+
budget = DEFAULT_BUDGET;
|
|
12758
|
+
_focused = false;
|
|
12759
|
+
/**
|
|
12760
|
+
* Scroll offset (content rows) into the UNIFIED page scrollport (question,
|
|
12761
|
+
* detail, then every option with its description, then the free-text row).
|
|
12762
|
+
* PageUp/PageDown move it; ↑↓/digits/click move the cursor and the render
|
|
12763
|
+
* follows it into view (pendingCursorScroll).
|
|
12764
|
+
*/
|
|
12765
|
+
bodyScroll = 0;
|
|
12766
|
+
/**
|
|
12767
|
+
* Expanded mode ('e' / marker click): the FRAME grows toward 80% (the
|
|
12768
|
+
* QuestionFrame reads isBodyExpanded each render), so the scrollport is
|
|
12769
|
+
* taller and more of the page — including option descriptions — is visible
|
|
12770
|
+
* at once. Everything stays reachable by scrolling either way.
|
|
12771
|
+
*/
|
|
12772
|
+
bodyExpanded = false;
|
|
12773
|
+
/** Cursor moved since the last render: the next render must scroll the
|
|
12774
|
+
* cursor's rows into view. */
|
|
12775
|
+
pendingCursorScroll = false;
|
|
12776
|
+
/** Hit map from the last render: content row -> option row key. Built each
|
|
12777
|
+
* render; drives fullscreen click-to-select. */
|
|
12778
|
+
hitMap = /* @__PURE__ */ new Map();
|
|
12779
|
+
/** Content row of the scroll marker in the last render (-1 = none);
|
|
12780
|
+
* clicking it toggles the expanded panel. */
|
|
12781
|
+
lastMarkerRow = -1;
|
|
12782
|
+
/** Scrollport height from the last render (scroll page math). */
|
|
12783
|
+
lastRegionHeight = 0;
|
|
12784
|
+
/** Wrapped page content length from the last render. */
|
|
12785
|
+
lastContentRows = 0;
|
|
12786
|
+
/** Content rows visible in the region from the last render (region minus
|
|
12787
|
+
* the marker row when overflowing). */
|
|
12788
|
+
lastVisibleRows = 0;
|
|
12789
|
+
/** Whether the page content overflows the scrollport on the last render —
|
|
12790
|
+
* 'e' (and the scroll verbs) are available when true. */
|
|
12791
|
+
lastExpandable = false;
|
|
12727
12792
|
constructor(questions, onDone, onCancel) {
|
|
12728
12793
|
this.questions = questions;
|
|
12729
12794
|
this.onDone = onDone;
|
|
@@ -12737,6 +12802,162 @@ var QuestionFlow = class {
|
|
|
12737
12802
|
this.otherInput.onEscape = () => this.exitOther();
|
|
12738
12803
|
this.syncEditMode();
|
|
12739
12804
|
}
|
|
12805
|
+
/**
|
|
12806
|
+
* Focus mirror for the editor-seat layout: the app focuses the wrapping
|
|
12807
|
+
* QuestionFrame, which forwards here. The free-text Input renders its
|
|
12808
|
+
* hardware cursor only while focused AND editing, so the mirror must sync
|
|
12809
|
+
* it — a plain field would leave the cursor permanently hidden.
|
|
12810
|
+
*/
|
|
12811
|
+
get focused() {
|
|
12812
|
+
return this._focused;
|
|
12813
|
+
}
|
|
12814
|
+
set focused(value) {
|
|
12815
|
+
this._focused = value;
|
|
12816
|
+
if (this.editingOther) this.otherInput.focused = value;
|
|
12817
|
+
}
|
|
12818
|
+
/**
|
|
12819
|
+
* Replace the content-row budget (the Frame wrapper adds its two border
|
|
12820
|
+
* rows on top). Called by QuestionFrame on every render immediately
|
|
12821
|
+
* before the flow renders, so no invalidation is needed; values outside
|
|
12822
|
+
* [MIN_SUPPORTED_BUDGET, MAX_BUDGET] are clamped.
|
|
12823
|
+
*/
|
|
12824
|
+
setMaxRows(rows) {
|
|
12825
|
+
this.budget = Math.max(MIN_SUPPORTED_BUDGET, Math.min(MAX_BUDGET, Math.floor(rows)));
|
|
12826
|
+
}
|
|
12827
|
+
/** Whether the panel is expanded (the frame grows toward 80% of the
|
|
12828
|
+
* terminal — QuestionFrame reads this every render). */
|
|
12829
|
+
isBodyExpanded() {
|
|
12830
|
+
return this.bodyExpanded;
|
|
12831
|
+
}
|
|
12832
|
+
/**
|
|
12833
|
+
* Reset the view (scroll + expanded + pending cursor follow) — called on
|
|
12834
|
+
* every tab change so each question starts at its top, collapsed.
|
|
12835
|
+
*/
|
|
12836
|
+
resetBodyView() {
|
|
12837
|
+
this.bodyScroll = 0;
|
|
12838
|
+
this.bodyExpanded = false;
|
|
12839
|
+
this.pendingCursorScroll = false;
|
|
12840
|
+
}
|
|
12841
|
+
/**
|
|
12842
|
+
* Page the unified scrollport. One page = the visible content rows. No-op
|
|
12843
|
+
* when the content fits the region.
|
|
12844
|
+
*/
|
|
12845
|
+
scrollBody(direction) {
|
|
12846
|
+
const visible = this.lastVisibleRows;
|
|
12847
|
+
if (visible <= 0 || this.lastContentRows <= visible) return;
|
|
12848
|
+
const maxScroll = this.lastContentRows - visible;
|
|
12849
|
+
this.bodyScroll = Math.max(0, Math.min(this.bodyScroll + direction * visible, maxScroll));
|
|
12850
|
+
}
|
|
12851
|
+
/** Toggle the expanded panel ('e' or a click on the scroll marker).
|
|
12852
|
+
* Expanding is only meaningful when the page content overflows the
|
|
12853
|
+
* scrollport (everything fits -> a taller panel shows nothing new). The
|
|
12854
|
+
* SCROLL POSITION is kept: the taller region reveals MORE rows where the
|
|
12855
|
+
* user is looking (e.g. the option descriptions they scrolled to), instead
|
|
12856
|
+
* of jumping back to the top — the render clamps the offset to the new
|
|
12857
|
+
* maxScroll. */
|
|
12858
|
+
toggleExpanded() {
|
|
12859
|
+
if (!this.bodyExpanded && !this.lastExpandable) return;
|
|
12860
|
+
this.bodyExpanded = !this.bodyExpanded;
|
|
12861
|
+
}
|
|
12862
|
+
/**
|
|
12863
|
+
* Primary-click routing (fullscreen): an option row selects it (single-
|
|
12864
|
+
* select advances, multi-select toggles, the "Type something." row enters
|
|
12865
|
+
* free-text), and the scroll marker toggles the expanded panel. The hit
|
|
12866
|
+
* map reflects the LAST rendered frame, which is what the user sees.
|
|
12867
|
+
*/
|
|
12868
|
+
clickRow(row) {
|
|
12869
|
+
if (row < 0 || this.tab >= this.questions.length) return;
|
|
12870
|
+
const key = this.hitMap.get(row);
|
|
12871
|
+
if (key === OTHER_ROW) {
|
|
12872
|
+
if (this.editingOther) return;
|
|
12873
|
+
const index = this.rows().findIndex((candidate) => candidate.key === OTHER_ROW);
|
|
12874
|
+
if (index >= 0) {
|
|
12875
|
+
this.cursor = index;
|
|
12876
|
+
this.pendingCursorScroll = true;
|
|
12877
|
+
this.enterOther();
|
|
12878
|
+
}
|
|
12879
|
+
return;
|
|
12880
|
+
}
|
|
12881
|
+
if (key !== void 0) {
|
|
12882
|
+
this.cursor = Number(key);
|
|
12883
|
+
this.pendingCursorScroll = true;
|
|
12884
|
+
this.confirm();
|
|
12885
|
+
return;
|
|
12886
|
+
}
|
|
12887
|
+
if (row === this.lastMarkerRow) this.toggleExpanded();
|
|
12888
|
+
}
|
|
12889
|
+
/**
|
|
12890
|
+
* Build the UNIFIED page scrollport content: the question + detail first
|
|
12891
|
+
* (required-first — row 0 is always the question's first physical row),
|
|
12892
|
+
* a separator, then every option row (label + description), then the
|
|
12893
|
+
* free-text row. Each option row maps to its key for click hit-testing.
|
|
12894
|
+
* Capped at MAX_CONTENT_ROWS with a trailing `... more content hidden`
|
|
12895
|
+
* note, so wrapping an unbounded detail/description stays cheap.
|
|
12896
|
+
*/
|
|
12897
|
+
buildPageContent(question, rows, draft, multi, width) {
|
|
12898
|
+
const lines = [];
|
|
12899
|
+
const hits = [];
|
|
12900
|
+
let left = MAX_CONTENT_ROWS;
|
|
12901
|
+
left = appendContentFirst(lines, `${color.primary("?")} `, " ", color.textStrong(question.question), width, left);
|
|
12902
|
+
hits.push(...new Array(lines.length).fill(void 0));
|
|
12903
|
+
if (question.detail !== void 0 && question.detail !== "") for (const line of question.detail.split("\n")) {
|
|
12904
|
+
if (left <= 1) {
|
|
12905
|
+
appendWrappedBudgeted(lines, " ", " ", color.textDim("... more content hidden"), width, left);
|
|
12906
|
+
break;
|
|
12907
|
+
}
|
|
12908
|
+
left = appendWrappedBudgeted(lines, " ", " ", color.textDim(line), width, left);
|
|
12909
|
+
}
|
|
12910
|
+
hits.push(...new Array(lines.length - hits.length).fill(void 0));
|
|
12911
|
+
lines.push("");
|
|
12912
|
+
hits.push(void 0);
|
|
12913
|
+
for (const row of rows) {
|
|
12914
|
+
const isCursor = rows[this.cursor] === row;
|
|
12915
|
+
const selected = row.key === OTHER_ROW ? draft.custom !== "" : draft.selected.has(question.options?.[Number(row.key)]?.label ?? "");
|
|
12916
|
+
const marker = multi || row.key === OTHER_ROW ? selected ? color.success("[✓]") : color.textDim("[ ]") : selected ? color.success(`[${Number(row.key) + 1}]`) : color.textDim(`[${Number(row.key) + 1}]`);
|
|
12917
|
+
const prefix = `${isCursor ? color.primary("→") : " "} ${marker} `;
|
|
12918
|
+
const indent = " ".repeat(visibleWidth(prefix));
|
|
12919
|
+
const badge = row.recommended ? ` ${color.primary("[recommended]")}` : "";
|
|
12920
|
+
const label = isCursor ? color.textStrong(row.label) : row.label;
|
|
12921
|
+
if (row.key === OTHER_ROW && this.editingOther) {
|
|
12922
|
+
const inputLine = this.otherInput.render(Math.max(1, width - visibleWidth(prefix)))[0] ?? "";
|
|
12923
|
+
const stripped = inputLine.startsWith("> ") ? inputLine.slice(2) : inputLine;
|
|
12924
|
+
lines.push(prefix + (this.otherInput.getValue() === "" ? color.textDim(row.label) : stripped));
|
|
12925
|
+
hits.push(OTHER_ROW);
|
|
12926
|
+
continue;
|
|
12927
|
+
}
|
|
12928
|
+
const labelWidth = Math.max(1, width - visibleWidth(prefix));
|
|
12929
|
+
for (const wrapped of wrapTextWithAnsi(`${label}${badge}`, labelWidth)) if (!this.pushPageRow(lines, hits, prefix + wrapped, row.key)) return {
|
|
12930
|
+
lines,
|
|
12931
|
+
hits
|
|
12932
|
+
};
|
|
12933
|
+
if (row.description !== void 0 && row.description !== "" && !(row.key === OTHER_ROW && this.editingOther)) {
|
|
12934
|
+
const descWidth = Math.max(1, width - visibleWidth(indent));
|
|
12935
|
+
for (const wrapped of wrapTextWithAnsi(color.textDim(row.description), descWidth)) if (!this.pushPageRow(lines, hits, indent + wrapped, row.key)) return {
|
|
12936
|
+
lines,
|
|
12937
|
+
hits
|
|
12938
|
+
};
|
|
12939
|
+
}
|
|
12940
|
+
}
|
|
12941
|
+
return {
|
|
12942
|
+
lines,
|
|
12943
|
+
hits
|
|
12944
|
+
};
|
|
12945
|
+
}
|
|
12946
|
+
/** Push one page-content row, capping at MAX_CONTENT_ROWS with a single
|
|
12947
|
+
* `... more content hidden` note (never a second one after the detail's).
|
|
12948
|
+
* @returns false when the cap was hit (callers stop building). */
|
|
12949
|
+
pushPageRow(lines, hits, line, key) {
|
|
12950
|
+
if (lines.length >= MAX_CONTENT_ROWS) {
|
|
12951
|
+
if (lines[lines.length - 1]?.includes("more content hidden") !== true) {
|
|
12952
|
+
lines.push(color.textDim("... more content hidden"));
|
|
12953
|
+
hits.push(void 0);
|
|
12954
|
+
}
|
|
12955
|
+
return false;
|
|
12956
|
+
}
|
|
12957
|
+
lines.push(line);
|
|
12958
|
+
hits.push(key);
|
|
12959
|
+
return true;
|
|
12960
|
+
}
|
|
12740
12961
|
/** The rows of the current question (options plus the free-text row). */
|
|
12741
12962
|
rows() {
|
|
12742
12963
|
const question = this.questions[this.tab];
|
|
@@ -12805,6 +13026,7 @@ var QuestionFlow = class {
|
|
|
12805
13026
|
}
|
|
12806
13027
|
/** Optionless questions edit text directly; options start in list mode. */
|
|
12807
13028
|
syncEditMode() {
|
|
13029
|
+
this.resetBodyView();
|
|
12808
13030
|
const question = this.questions[this.tab];
|
|
12809
13031
|
const optionless = question !== void 0 && (question.options?.length ?? 0) === 0;
|
|
12810
13032
|
this.editingOther = optionless;
|
|
@@ -12879,6 +13101,7 @@ var QuestionFlow = class {
|
|
|
12879
13101
|
if (this.editingOther) {
|
|
12880
13102
|
if (data === "\r" || data === "\n") this.commitOther(this.otherInput.getValue());
|
|
12881
13103
|
else if (data === "\x1B") this.exitOther();
|
|
13104
|
+
else if (data === "\x1B[5~" || data === "\x1B[6~") this.scrollBody(data === "\x1B[5~" ? -1 : 1);
|
|
12882
13105
|
else if (data === "\x1B[D") {
|
|
12883
13106
|
if (this.tab > 0) {
|
|
12884
13107
|
this.tab -= 1;
|
|
@@ -12921,16 +13144,27 @@ var QuestionFlow = class {
|
|
|
12921
13144
|
const row = rows[Number(digit[0]) - 1];
|
|
12922
13145
|
if (row !== void 0) {
|
|
12923
13146
|
this.cursor = rows.indexOf(row);
|
|
13147
|
+
this.pendingCursorScroll = true;
|
|
12924
13148
|
this.confirm();
|
|
12925
13149
|
}
|
|
12926
13150
|
return;
|
|
12927
13151
|
}
|
|
12928
13152
|
if (data === "\x1B[A" || data === "k") {
|
|
12929
13153
|
if (rows.length > 0) this.cursor = (this.cursor - 1 + rows.length) % rows.length;
|
|
13154
|
+
this.pendingCursorScroll = true;
|
|
12930
13155
|
return;
|
|
12931
13156
|
}
|
|
12932
13157
|
if (data === "\x1B[B" || data === "j") {
|
|
12933
13158
|
if (rows.length > 0) this.cursor = (this.cursor + 1) % rows.length;
|
|
13159
|
+
this.pendingCursorScroll = true;
|
|
13160
|
+
return;
|
|
13161
|
+
}
|
|
13162
|
+
if (data === "\x1B[5~" || data === "\x1B[6~") {
|
|
13163
|
+
this.scrollBody(data === "\x1B[5~" ? -1 : 1);
|
|
13164
|
+
return;
|
|
13165
|
+
}
|
|
13166
|
+
if (data === "e") {
|
|
13167
|
+
this.toggleExpanded();
|
|
12934
13168
|
return;
|
|
12935
13169
|
}
|
|
12936
13170
|
if (data === "\r" || data === "\n") {
|
|
@@ -12951,6 +13185,7 @@ var QuestionFlow = class {
|
|
|
12951
13185
|
this.tab += 1;
|
|
12952
13186
|
this.cursor = 0;
|
|
12953
13187
|
this.submitIdx = 0;
|
|
13188
|
+
this.syncEditMode();
|
|
12954
13189
|
} else {
|
|
12955
13190
|
this.tab = this.questions.length;
|
|
12956
13191
|
this.cursor = 0;
|
|
@@ -12971,6 +13206,8 @@ var QuestionFlow = class {
|
|
|
12971
13206
|
render(width) {
|
|
12972
13207
|
const safeWidth = Math.max(1, width);
|
|
12973
13208
|
const lines = [];
|
|
13209
|
+
this.hitMap.clear();
|
|
13210
|
+
this.lastMarkerRow = -1;
|
|
12974
13211
|
const tabs = this.questions.map((_, index) => {
|
|
12975
13212
|
const draft = this.drafts[index];
|
|
12976
13213
|
const answered = draft !== void 0 && (draft.selected.size > 0 || draft.custom !== "" || draft.skipped);
|
|
@@ -12983,7 +13220,7 @@ var QuestionFlow = class {
|
|
|
12983
13220
|
lines.push(tabs.join(" "));
|
|
12984
13221
|
lines.push("");
|
|
12985
13222
|
if (this.tab >= this.questions.length) {
|
|
12986
|
-
let reviewBudget =
|
|
13223
|
+
let reviewBudget = this.budget - lines.length - 2;
|
|
12987
13224
|
reviewBudget = appendWrappedBudgeted(lines, "", "", color.textStrong("Review your answer before submit"), safeWidth, reviewBudget);
|
|
12988
13225
|
if (reviewBudget > 0) {
|
|
12989
13226
|
lines.push("");
|
|
@@ -13010,98 +13247,75 @@ var QuestionFlow = class {
|
|
|
13010
13247
|
const question = this.questions[this.tab];
|
|
13011
13248
|
const draft = this.draft();
|
|
13012
13249
|
if (question === void 0 || draft === void 0) return lines;
|
|
13013
|
-
if (question.header !== void 0 && question.header !== "") lines.push(color.textDim(question.header));
|
|
13014
|
-
let bodyBudget = MAX_BODY_LINES;
|
|
13015
|
-
bodyBudget = appendWrappedBudgeted(lines, `${color.primary("?")} `, " ", color.textStrong(question.question), safeWidth, bodyBudget);
|
|
13016
|
-
if (question.detail !== void 0 && question.detail !== "") for (const line of question.detail.split("\n")) {
|
|
13017
|
-
if (bodyBudget <= 1) {
|
|
13018
|
-
appendWrappedBudgeted(lines, " ", " ", color.textDim("... more content hidden"), safeWidth, bodyBudget);
|
|
13019
|
-
break;
|
|
13020
|
-
}
|
|
13021
|
-
bodyBudget = appendWrappedBudgeted(lines, " ", " ", color.textDim(line), safeWidth, bodyBudget);
|
|
13022
|
-
}
|
|
13023
13250
|
const rows = this.rows();
|
|
13024
13251
|
const multi = question.multiSelect === true;
|
|
13025
|
-
|
|
13026
|
-
|
|
13027
|
-
|
|
13028
|
-
|
|
13029
|
-
|
|
13030
|
-
|
|
13031
|
-
|
|
13032
|
-
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
|
|
13037
|
-
|
|
13038
|
-
|
|
13039
|
-
|
|
13040
|
-
|
|
13041
|
-
|
|
13042
|
-
|
|
13043
|
-
|
|
13044
|
-
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
if (row === void 0) continue;
|
|
13050
|
-
const isCursor = index === this.cursor;
|
|
13051
|
-
if (!isCursor && (cursorRendered ? left <= 0 : left <= reserved)) continue;
|
|
13052
|
-
if (row.key === OTHER_ROW && this.editingOther) {
|
|
13053
|
-
const prefix = `${color.primary("→")} ${color.textDim(`[${index + 1}]`)} `;
|
|
13054
|
-
const inputLine = this.otherInput.render(Math.max(1, safeWidth - visibleWidth(prefix)))[0] ?? "";
|
|
13055
|
-
const stripped = inputLine.startsWith("> ") ? inputLine.slice(2) : inputLine;
|
|
13056
|
-
left -= 1;
|
|
13057
|
-
lines.push(prefix + stripped);
|
|
13058
|
-
if (isCursor) cursorRendered = true;
|
|
13059
|
-
continue;
|
|
13060
|
-
}
|
|
13061
|
-
const selected = row.key === OTHER_ROW ? draft.custom !== "" : draft.selected.has(question.options?.[Number(row.key)]?.label ?? "");
|
|
13062
|
-
const marker = multi || row.key === OTHER_ROW ? selected ? color.success("[✓]") : color.textDim("[ ]") : selected ? color.success(`[${Number(row.key) + 1}]`) : color.textDim(`[${Number(row.key) + 1}]`);
|
|
13063
|
-
const prefix = `${isCursor ? color.primary("→") : " "} ${marker} `;
|
|
13064
|
-
const badge = row.recommended ? ` ${color.primary("[recommended]")}` : "";
|
|
13065
|
-
const label = isCursor ? color.textStrong(row.label) : row.label;
|
|
13066
|
-
const indent = " ".repeat(visibleWidth(prefix));
|
|
13067
|
-
const available = isCursor || cursorRendered ? left : left - reserved;
|
|
13068
|
-
const beforeLabel = lines.length;
|
|
13069
|
-
if (isCursor) appendCurrentLabel(lines, prefix, indent, `${label}${badge}`, safeWidth, available);
|
|
13070
|
-
else appendWrappedBudgeted(lines, prefix, indent, `${label}${badge}`, safeWidth, available);
|
|
13071
|
-
left -= lines.length - beforeLabel;
|
|
13072
|
-
if (row.description !== void 0 && row.description !== "" && left > 0) {
|
|
13073
|
-
const descCap = isCursor || cursorRendered ? left : left - reserved;
|
|
13074
|
-
const beforeDescription = lines.length;
|
|
13075
|
-
appendWrappedBudgeted(lines, indent, indent, color.textDim(row.description), safeWidth, descCap);
|
|
13076
|
-
left -= lines.length - beforeDescription;
|
|
13077
|
-
}
|
|
13078
|
-
if (isCursor) cursorRendered = true;
|
|
13252
|
+
const skippedRow = draft.skipped ? 1 : 0;
|
|
13253
|
+
const optionless = rows.length === 0 || rows.length === 1 && rows[0]?.key === OTHER_ROW;
|
|
13254
|
+
const tail = optionless ? 3 + skippedRow : 2 + skippedRow;
|
|
13255
|
+
const header = question.header;
|
|
13256
|
+
if (header !== void 0 && header !== "" && this.budget - lines.length - tail >= 2) lines.push(color.textDim(header));
|
|
13257
|
+
const content = this.buildPageContent(question, rows, draft, multi, safeWidth);
|
|
13258
|
+
const regionHeight = Math.max(1, this.budget - lines.length - tail);
|
|
13259
|
+
const overflow = content.lines.length > regionHeight;
|
|
13260
|
+
const visible = overflow && regionHeight >= 2 ? regionHeight - 1 : regionHeight;
|
|
13261
|
+
this.lastRegionHeight = regionHeight;
|
|
13262
|
+
this.lastContentRows = content.lines.length;
|
|
13263
|
+
this.lastVisibleRows = visible;
|
|
13264
|
+
this.lastExpandable = overflow && regionHeight >= 2;
|
|
13265
|
+
const maxScroll = Math.max(0, content.lines.length - visible);
|
|
13266
|
+
this.bodyScroll = Math.min(this.bodyScroll, maxScroll);
|
|
13267
|
+
if (this.pendingCursorScroll) {
|
|
13268
|
+
this.pendingCursorScroll = false;
|
|
13269
|
+
const cursorKey = rows[this.cursor]?.key;
|
|
13270
|
+
if (cursorKey !== void 0) {
|
|
13271
|
+
const first = content.hits.indexOf(cursorKey);
|
|
13272
|
+
if (first >= 0) {
|
|
13273
|
+
if (first < this.bodyScroll) this.bodyScroll = first;
|
|
13274
|
+
else if (first >= this.bodyScroll + visible) this.bodyScroll = first - visible + 1;
|
|
13275
|
+
this.bodyScroll = Math.max(0, Math.min(this.bodyScroll, maxScroll));
|
|
13079
13276
|
}
|
|
13080
|
-
if (showingRow === 1 && left > 0) left = appendWrappedBudgeted(lines, " ", " ", `showing ${start + 1}-${end} of ${rows.length}`, safeWidth, left);
|
|
13081
13277
|
}
|
|
13082
|
-
}
|
|
13278
|
+
}
|
|
13279
|
+
for (let index = this.bodyScroll; index < this.bodyScroll + visible && index < content.lines.length; index++) {
|
|
13280
|
+
const hit = content.hits[index];
|
|
13281
|
+
if (hit !== void 0) this.hitMap.set(lines.length, hit);
|
|
13282
|
+
lines.push(content.lines[index]);
|
|
13283
|
+
}
|
|
13284
|
+
if (overflow && regionHeight >= 2) {
|
|
13285
|
+
const above = this.bodyScroll;
|
|
13286
|
+
const below = content.lines.length - (this.bodyScroll + visible);
|
|
13287
|
+
const marker = above > 0 && below > 0 ? `↑ ${above} up · ↓ ${below} more lines` : above > 0 ? `↑ ${above} up` : `↓ ${below} more lines`;
|
|
13288
|
+
this.lastMarkerRow = lines.length;
|
|
13289
|
+
lines.push(color.textDim(marker));
|
|
13290
|
+
} else this.lastMarkerRow = -1;
|
|
13291
|
+
if (optionless) {
|
|
13083
13292
|
const inputLine = this.otherInput.render(Math.max(1, safeWidth - 2))[0] ?? "";
|
|
13084
13293
|
const stripped = inputLine.startsWith("> ") ? inputLine.slice(2) : inputLine;
|
|
13085
|
-
lines.push(` ${stripped}`);
|
|
13294
|
+
lines.push(this.otherInput.getValue() === "" ? color.textDim(" Type your answer…") : ` ${stripped}`);
|
|
13086
13295
|
}
|
|
13087
13296
|
if (draft.skipped) lines.push(color.textDim("(skipped)"));
|
|
13088
13297
|
lines.push("");
|
|
13089
13298
|
const optionCount = Math.min(rows.length, 9);
|
|
13299
|
+
const scrollable = this.lastExpandable;
|
|
13090
13300
|
const hintParts = [
|
|
13091
13301
|
"↑↓ select",
|
|
13092
13302
|
optionCount > 0 ? `1-${optionCount} choose` : "",
|
|
13093
13303
|
multi ? "↵ toggle" : "↵ confirm",
|
|
13304
|
+
scrollable ? "pgup/pgdn scroll" : "",
|
|
13305
|
+
!this.editingOther ? this.bodyExpanded ? "e collapse" : scrollable ? "e expand" : "" : "",
|
|
13094
13306
|
this.questions.length > 1 ? "←→ switch" : "",
|
|
13095
13307
|
"s skip",
|
|
13096
13308
|
"esc cancel"
|
|
13097
13309
|
].filter((part) => part !== "");
|
|
13310
|
+
const cancel = "esc cancel";
|
|
13311
|
+
const verbs = hintParts.filter((part) => part !== cancel);
|
|
13098
13312
|
let hint = "";
|
|
13099
|
-
for (const part of
|
|
13313
|
+
for (const part of verbs) {
|
|
13100
13314
|
const next = hint === "" ? part : `${hint} · ${part}`;
|
|
13101
|
-
if (visibleWidth(next) > safeWidth) break;
|
|
13315
|
+
if (visibleWidth(`${next} · ${cancel}`) > safeWidth) break;
|
|
13102
13316
|
hint = next;
|
|
13103
13317
|
}
|
|
13104
|
-
lines.push(color.textDim(hint === "" ?
|
|
13318
|
+
lines.push(color.textDim(hint === "" ? cancel : `${hint} · ${cancel}`));
|
|
13105
13319
|
return lines;
|
|
13106
13320
|
}
|
|
13107
13321
|
};
|
|
@@ -13610,13 +13824,16 @@ function preview(text, lines) {
|
|
|
13610
13824
|
const CONTEXT_BAR_WIDTH = 12;
|
|
13611
13825
|
/**
|
|
13612
13826
|
* Rounded-frame wrapper for overlay content: `╭─╮` border in the border
|
|
13613
|
-
* token, one cell of padding, width sized to the content.
|
|
13614
|
-
*
|
|
13827
|
+
* token, one cell of padding, width sized to the content. With `fillWidth`
|
|
13828
|
+
* the frame keeps the overlay's full width instead of hugging the widest
|
|
13829
|
+
* content row. Keyboard input forwards to the wrapped component.
|
|
13615
13830
|
*/
|
|
13616
13831
|
var Frame = class {
|
|
13617
13832
|
child;
|
|
13618
|
-
|
|
13833
|
+
fillWidth;
|
|
13834
|
+
constructor(child, fillWidth = false) {
|
|
13619
13835
|
this.child = child;
|
|
13836
|
+
this.fillWidth = fillWidth;
|
|
13620
13837
|
}
|
|
13621
13838
|
invalidate() {
|
|
13622
13839
|
this.child.invalidate?.();
|
|
@@ -13630,7 +13847,7 @@ var Frame = class {
|
|
|
13630
13847
|
render(width) {
|
|
13631
13848
|
const inner = Math.max(1, Math.floor(width) - 4);
|
|
13632
13849
|
const lines = this.child.render(inner).map((line) => truncateToWidth(line, inner, "…"));
|
|
13633
|
-
const contentWidth = Math.min(inner, Math.max(1, ...lines.map((line) => visibleWidth(line))));
|
|
13850
|
+
const contentWidth = this.fillWidth ? inner : Math.min(inner, Math.max(1, ...lines.map((line) => visibleWidth(line))));
|
|
13634
13851
|
const frameWidth = contentWidth + 4;
|
|
13635
13852
|
const b = color.border;
|
|
13636
13853
|
const out = [b(`╭${"─".repeat(frameWidth - 2)}╮`)];
|
|
@@ -13643,6 +13860,51 @@ var Frame = class {
|
|
|
13643
13860
|
return out;
|
|
13644
13861
|
}
|
|
13645
13862
|
};
|
|
13863
|
+
/**
|
|
13864
|
+
* Frame for the question flow in the EDITOR SEAT: it re-derives the flow's
|
|
13865
|
+
* row budget from the terminal height on EVERY render (60% cap, 8..24
|
|
13866
|
+
* content rows — the flow's render output IS its height in the seat layout,
|
|
13867
|
+
* nothing clips it), so an active resize or a queued flow presented later
|
|
13868
|
+
* always budgets against the current terminal. It also forwards focus to the
|
|
13869
|
+
* flow so its free-text Input keeps the hardware cursor (a plain Frame would
|
|
13870
|
+
* swallow the focus flag).
|
|
13871
|
+
*/
|
|
13872
|
+
var QuestionFrame = class extends Frame {
|
|
13873
|
+
flow;
|
|
13874
|
+
heightOf;
|
|
13875
|
+
/** Rendered height of the last frame (fullscreen click hit-testing). */
|
|
13876
|
+
lastRows = 0;
|
|
13877
|
+
/** Terminal height the last render used (click staleness guard). */
|
|
13878
|
+
lastTermRows = 0;
|
|
13879
|
+
constructor(flow, heightOf) {
|
|
13880
|
+
super(flow, true);
|
|
13881
|
+
this.flow = flow;
|
|
13882
|
+
this.heightOf = heightOf;
|
|
13883
|
+
}
|
|
13884
|
+
render(width) {
|
|
13885
|
+
const rows = Math.max(1, this.heightOf());
|
|
13886
|
+
this.lastTermRows = rows;
|
|
13887
|
+
const frameRows = this.flow.isBodyExpanded() ? Math.max(10, Math.min(40, Math.floor(rows * .8))) : Math.max(10, Math.min(26, Math.floor(rows * .6)));
|
|
13888
|
+
this.flow.setMaxRows(frameRows - 2);
|
|
13889
|
+
const out = super.render(width);
|
|
13890
|
+
this.lastRows = out.length;
|
|
13891
|
+
return out;
|
|
13892
|
+
}
|
|
13893
|
+
/** The frame's height from the last render (0 before the first one). */
|
|
13894
|
+
get rows() {
|
|
13895
|
+
return this.lastRows;
|
|
13896
|
+
}
|
|
13897
|
+
/** The terminal height the last render used. */
|
|
13898
|
+
get termRows() {
|
|
13899
|
+
return this.lastTermRows;
|
|
13900
|
+
}
|
|
13901
|
+
get focused() {
|
|
13902
|
+
return this.flow.focused;
|
|
13903
|
+
}
|
|
13904
|
+
set focused(value) {
|
|
13905
|
+
this.flow.focused = value;
|
|
13906
|
+
}
|
|
13907
|
+
};
|
|
13646
13908
|
/** The session head card: identity facts, wrapped to the available width so
|
|
13647
13909
|
* nothing is truncated, framed with a box whose width matches the editor's
|
|
13648
13910
|
* border below it (a fixed-width rule looked misaligned next to the frame). */
|
|
@@ -13712,6 +13974,134 @@ var Spacer = class {
|
|
|
13712
13974
|
return [""];
|
|
13713
13975
|
}
|
|
13714
13976
|
};
|
|
13977
|
+
/**
|
|
13978
|
+
* Bullet + continuation-indent wrapper that keeps its child LIVE, so a
|
|
13979
|
+
* terminal resize re-renders the child at the new width instead of
|
|
13980
|
+
* re-wrapping a frozen render (the 5a76526 regression: assistant/user
|
|
13981
|
+
* messages were flattened to a static Text at build time, so markdown
|
|
13982
|
+
* tables could never reflow and border lines wrapped as plain text on
|
|
13983
|
+
* narrow windows). The bullet leads the FIRST line; wrapped continuation
|
|
13984
|
+
* lines indent under it (kimi prefix+indent parity).
|
|
13985
|
+
*
|
|
13986
|
+
* The prefixed output keeps a REFERENCE-STABLE cache: when the child
|
|
13987
|
+
* returns the same array instance (its own text+width cache hit) at the
|
|
13988
|
+
* same width, the wrapper returns the same prefixed array — so the fork's
|
|
13989
|
+
* per-frame processed-line reuse (fork AGENTS.md divergence 5) keeps
|
|
13990
|
+
* hitting on steady frames instead of re-normalizing every line.
|
|
13991
|
+
*/
|
|
13992
|
+
var BulletedComponent = class {
|
|
13993
|
+
child;
|
|
13994
|
+
prefix;
|
|
13995
|
+
prefixWidth;
|
|
13996
|
+
indent;
|
|
13997
|
+
lastChild;
|
|
13998
|
+
lastWidth = -1;
|
|
13999
|
+
cached;
|
|
14000
|
+
constructor(child, prefix) {
|
|
14001
|
+
this.child = child;
|
|
14002
|
+
this.prefix = prefix;
|
|
14003
|
+
this.prefixWidth = visibleWidth(prefix);
|
|
14004
|
+
this.indent = " ".repeat(this.prefixWidth);
|
|
14005
|
+
}
|
|
14006
|
+
invalidate() {
|
|
14007
|
+
this.child.invalidate?.();
|
|
14008
|
+
}
|
|
14009
|
+
dispose() {
|
|
14010
|
+
this.child.dispose?.();
|
|
14011
|
+
}
|
|
14012
|
+
render(width) {
|
|
14013
|
+
const inner = Math.max(1, width - this.prefixWidth);
|
|
14014
|
+
const child = this.child.render(inner);
|
|
14015
|
+
if (child === this.lastChild && width === this.lastWidth && this.cached !== void 0) return this.cached;
|
|
14016
|
+
this.lastChild = child;
|
|
14017
|
+
this.lastWidth = width;
|
|
14018
|
+
this.cached = child.map((line, index) => (index === 0 ? this.prefix : this.indent) + line);
|
|
14019
|
+
return this.cached;
|
|
14020
|
+
}
|
|
14021
|
+
};
|
|
14022
|
+
/**
|
|
14023
|
+
* Longest prefix of `text` whose WRAPPED height fits `budget` rows at
|
|
14024
|
+
* `width`, with an ellipsis marking a cut — the approval dialog's height
|
|
14025
|
+
* budget must count wrapped rows, not raw lines, because a single long
|
|
14026
|
+
* line can wrap across many display rows. Wrapped height is monotonic in
|
|
14027
|
+
* the prefix length, so a binary search bounds the wrap calls. The
|
|
14028
|
+
* ellipsis reserves its own row when truncating (a full last row would
|
|
14029
|
+
* otherwise push it onto a new row and overflow the budget).
|
|
14030
|
+
* @param text - the candidate text ('' yields '').
|
|
14031
|
+
* @param width - the wrap width.
|
|
14032
|
+
* @param budget - the row budget; 0 or negative yields '…' for non-empty.
|
|
14033
|
+
* @returns the fitted text and whether it was truncated.
|
|
14034
|
+
*/
|
|
14035
|
+
function capWrappedToHeight(text, width, budget) {
|
|
14036
|
+
if (text === "") return {
|
|
14037
|
+
text: "",
|
|
14038
|
+
truncated: false
|
|
14039
|
+
};
|
|
14040
|
+
if (budget <= 0) return {
|
|
14041
|
+
text: "",
|
|
14042
|
+
truncated: true
|
|
14043
|
+
};
|
|
14044
|
+
const fits = (candidate, rows) => wrapTextWithAnsi(candidate, width).length <= rows;
|
|
14045
|
+
if (fits(text, budget)) return {
|
|
14046
|
+
text,
|
|
14047
|
+
truncated: false
|
|
14048
|
+
};
|
|
14049
|
+
if (budget === 1) return {
|
|
14050
|
+
text: truncateToWidth(text, width, "…"),
|
|
14051
|
+
truncated: true
|
|
14052
|
+
};
|
|
14053
|
+
const target = budget - 1;
|
|
14054
|
+
let low = 0;
|
|
14055
|
+
let high = text.length;
|
|
14056
|
+
while (low < high) {
|
|
14057
|
+
const mid = Math.ceil((low + high) / 2);
|
|
14058
|
+
if (fits(text.slice(0, mid), target)) low = mid;
|
|
14059
|
+
else high = mid - 1;
|
|
14060
|
+
}
|
|
14061
|
+
return {
|
|
14062
|
+
text: `${text.slice(0, low)}…`,
|
|
14063
|
+
truncated: true
|
|
14064
|
+
};
|
|
14065
|
+
}
|
|
14066
|
+
/**
|
|
14067
|
+
* Fit `text` into `budget` wrapped rows for the approval dialog, ending with
|
|
14068
|
+
* a dimmed `... N more` marker row when the content is cut. The marker rides
|
|
14069
|
+
* INSIDE the budget (content rows cap at budget−1, the marker itself is
|
|
14070
|
+
* width-cropped so it can never wrap), so a section can never silently
|
|
14071
|
+
* overflow the dialog's maxHeight — same marker semantics as the
|
|
14072
|
+
* question flow's `appendWrappedBudgeted`. A single-row budget keeps
|
|
14073
|
+
* the old width-cropped ellipsis (a bare marker row would waste the row).
|
|
14074
|
+
* @returns the display text (rows joined with '\n') and the hidden row count.
|
|
14075
|
+
*/
|
|
14076
|
+
function capWrappedToMarker(text, width, budget) {
|
|
14077
|
+
if (text === "" || budget <= 0) return {
|
|
14078
|
+
text: "",
|
|
14079
|
+
hidden: 0
|
|
14080
|
+
};
|
|
14081
|
+
const total = wrapTextWithAnsi(text, width).length;
|
|
14082
|
+
if (total <= budget) return {
|
|
14083
|
+
text,
|
|
14084
|
+
hidden: 0
|
|
14085
|
+
};
|
|
14086
|
+
if (budget === 1) return {
|
|
14087
|
+
text: truncateToWidth(text, width, "…"),
|
|
14088
|
+
hidden: total - 1
|
|
14089
|
+
};
|
|
14090
|
+
const target = budget - 1;
|
|
14091
|
+
let low = 0;
|
|
14092
|
+
let high = text.length;
|
|
14093
|
+
while (low < high) {
|
|
14094
|
+
const mid = Math.ceil((low + high) / 2);
|
|
14095
|
+
if (wrapTextWithAnsi(text.slice(0, mid), width).length <= target) low = mid;
|
|
14096
|
+
else high = mid - 1;
|
|
14097
|
+
}
|
|
14098
|
+
const hidden = total - target;
|
|
14099
|
+
const marker = truncateToWidth(color.textDim(`... ${hidden} more line${hidden > 1 ? "s" : ""}`), width, "…");
|
|
14100
|
+
return {
|
|
14101
|
+
text: `${text.slice(0, low)}\n${marker}`,
|
|
14102
|
+
hidden
|
|
14103
|
+
};
|
|
14104
|
+
}
|
|
13715
14105
|
/** The live job-output viewer body: a title line + refreshable text panel. */
|
|
13716
14106
|
var OutputViewerPanel = class {
|
|
13717
14107
|
title;
|
|
@@ -13751,6 +14141,13 @@ var TuiApp = class TuiApp {
|
|
|
13751
14141
|
terminal;
|
|
13752
14142
|
tui;
|
|
13753
14143
|
editor;
|
|
14144
|
+
/**
|
|
14145
|
+
* The editor's SEAT (kimi's editorContainer): holds the editor normally,
|
|
14146
|
+
* the QuestionFrame while a question flow is active — so the dialog
|
|
14147
|
+
* renders in the editor's row position (full width, above the footer)
|
|
14148
|
+
* instead of a centered modal that covered the transcript.
|
|
14149
|
+
*/
|
|
14150
|
+
editorSeat;
|
|
13754
14151
|
header;
|
|
13755
14152
|
messagesView;
|
|
13756
14153
|
footer;
|
|
@@ -13789,6 +14186,8 @@ var TuiApp = class TuiApp {
|
|
|
13789
14186
|
dock;
|
|
13790
14187
|
/** Active background tasks for the dock/footer lines (label + status). */
|
|
13791
14188
|
dockTasks = [];
|
|
14189
|
+
/** Live child subagents (continuable or running one-shot) for the dock/footer lines (never jobs records). */
|
|
14190
|
+
dockAgents = [];
|
|
13792
14191
|
/**
|
|
13793
14192
|
* The queued-input pane below the todo panel (kimi QueuePane parity):
|
|
13794
14193
|
* a border rule plus one `❯ text` row per pending message and a dim hint.
|
|
@@ -13810,6 +14209,19 @@ var TuiApp = class TuiApp {
|
|
|
13810
14209
|
}
|
|
13811
14210
|
/** Fullscreen (alt-screen) instance; absent in regular mode. */
|
|
13812
14211
|
fullscreen;
|
|
14212
|
+
/** One shared in-flight autodetect; concurrent callers coalesce onto it
|
|
14213
|
+
* (overlapping OSC 11 queries would mis-pair replies by FIFO order). */
|
|
14214
|
+
autoDetectInFlight;
|
|
14215
|
+
/** The LATEST shouldApply guard of the in-flight autodetect; consulted at
|
|
14216
|
+
* settle time so a late result can never override a newer explicit
|
|
14217
|
+
* choice; reset when the flight settles. */
|
|
14218
|
+
autoDetectGuard;
|
|
14219
|
+
/** Whether the terminal's live scheme reports are being tracked. */
|
|
14220
|
+
followingTerminalTheme = false;
|
|
14221
|
+
/** Terminal-scheme listeners, fanned out from every screen's reports. */
|
|
14222
|
+
terminalSchemeListeners = /* @__PURE__ */ new Set();
|
|
14223
|
+
/** Per-screen scheme-report registrations, rebuilt on screen switches. */
|
|
14224
|
+
schemeDisposers = [];
|
|
13813
14225
|
/** The mounted transcript-search overlay, while one is open. */
|
|
13814
14226
|
searchOverlay;
|
|
13815
14227
|
/** The search input component, while one is open (for match counts). */
|
|
@@ -13874,6 +14286,13 @@ var TuiApp = class TuiApp {
|
|
|
13874
14286
|
externalEditorInFlight = false;
|
|
13875
14287
|
/** The live session's auto-generated title, shown in the header when set. */
|
|
13876
14288
|
sessionTitleText = "";
|
|
14289
|
+
/** The read-only subagent viewer: while set, the editor bar shows a
|
|
14290
|
+
* placeholder, the editor border switches to the accent color, and the
|
|
14291
|
+
* header carries a persistent badge — the transient notify line is not
|
|
14292
|
+
* the only signal. */
|
|
14293
|
+
viewerMode;
|
|
14294
|
+
/** The real draft preserved while the viewer covers the editor bar. */
|
|
14295
|
+
draftBeforeViewer;
|
|
13877
14296
|
constructor(terminal, events, options = {}) {
|
|
13878
14297
|
if (events.openExternalEditor !== void 0 && events.runOwned === void 0) throw new Error("openExternalEditor requires runOwned (the owned-task entry — AGENTS.md)");
|
|
13879
14298
|
this.terminal = terminal;
|
|
@@ -13899,13 +14318,15 @@ var TuiApp = class TuiApp {
|
|
|
13899
14318
|
this.queuePane = new Text("", 0, 0);
|
|
13900
14319
|
this.working = new WorkingIndicator(() => this.requestRender(), options.workingIntervalMs === void 0 ? {} : { intervalMs: options.workingIntervalMs });
|
|
13901
14320
|
this.footer = new Text("", 0, 0);
|
|
14321
|
+
this.editorSeat = new Container();
|
|
14322
|
+
this.editorSeat.addChild(this.editor);
|
|
13902
14323
|
this.tui.addChild(this.header);
|
|
13903
14324
|
this.tui.addChild(this.messagesView);
|
|
13904
14325
|
this.tui.addChild(this.dock);
|
|
13905
14326
|
this.tui.addChild(this.todoPanel);
|
|
13906
14327
|
this.tui.addChild(this.queuePane);
|
|
13907
14328
|
this.tui.addChild(this.working);
|
|
13908
|
-
this.tui.addChild(this.
|
|
14329
|
+
this.tui.addChild(this.editorSeat);
|
|
13909
14330
|
this.tui.addChild(this.footer);
|
|
13910
14331
|
this.tui.setFocus(this.editor);
|
|
13911
14332
|
this.tui.addInputListener((data) => this.routeInput(data));
|
|
@@ -13941,6 +14362,8 @@ var TuiApp = class TuiApp {
|
|
|
13941
14362
|
this.clearNotify();
|
|
13942
14363
|
this.working.dispose();
|
|
13943
14364
|
this.cancelQuestionFlows();
|
|
14365
|
+
for (const dispose of this.schemeDisposers) dispose();
|
|
14366
|
+
this.schemeDisposers = [];
|
|
13944
14367
|
this.tui.stop();
|
|
13945
14368
|
this.fullscreen?.stop();
|
|
13946
14369
|
this.fullscreen = void 0;
|
|
@@ -13950,6 +14373,7 @@ var TuiApp = class TuiApp {
|
|
|
13950
14373
|
if (isKeyRelease(data) || isKeyRepeat(data)) return void 0;
|
|
13951
14374
|
if (this.activeQuestions !== void 0) return this.handleQuestionKey(data);
|
|
13952
14375
|
if (this.activeApproval !== void 0) return this.handleApprovalKey(data);
|
|
14376
|
+
if (this.viewerMode !== void 0 && !this.overlayHost.hasOverlayEntries && !matchesKey(data, "escape") && !matchesKey(data, "ctrl+o")) return { consume: true };
|
|
13953
14377
|
if (this.searchOverlay !== void 0) {
|
|
13954
14378
|
if (matchesKey(data, "escape")) {
|
|
13955
14379
|
this.closeTranscriptSearch();
|
|
@@ -13983,6 +14407,17 @@ var TuiApp = class TuiApp {
|
|
|
13983
14407
|
this.events.onDequeue?.();
|
|
13984
14408
|
return { consume: true };
|
|
13985
14409
|
}
|
|
14410
|
+
if (matchesKey(data, "ctrl+enter")) {
|
|
14411
|
+
if (this.overlayHost.hasOverlayEntries) return void 0;
|
|
14412
|
+
if (this.events.onQueueSubmit === void 0) return void 0;
|
|
14413
|
+
const text = this.editor.getText();
|
|
14414
|
+
if (text.trim() === "") return void 0;
|
|
14415
|
+
this.rememberInput(text);
|
|
14416
|
+
this.clearNotify();
|
|
14417
|
+
this.editor.setText("");
|
|
14418
|
+
this.events.onQueueSubmit(text);
|
|
14419
|
+
return { consume: true };
|
|
14420
|
+
}
|
|
13986
14421
|
if (matchesKey(data, "escape")) {
|
|
13987
14422
|
if (this.overlayHost.hasOverlayEntries) return void 0;
|
|
13988
14423
|
if (this.events.onSingleEscape?.() === true) return { consume: true };
|
|
@@ -14047,6 +14482,14 @@ var TuiApp = class TuiApp {
|
|
|
14047
14482
|
* compositor interleaves stacked boxes line by line (two different-width
|
|
14048
14483
|
* dialogs would garble into one frame), and modal hiding is done here so
|
|
14049
14484
|
* the vendored fork stays pristine.
|
|
14485
|
+
*
|
|
14486
|
+
* While a question flow owns the seat (a logical capturing modal), a NEW
|
|
14487
|
+
* overlay joins the question's suspension set instead of appearing on top:
|
|
14488
|
+
* it is hidden, the question's directly suspended handles become ITS
|
|
14489
|
+
* dependents (kept hidden, restored only when it closes), and the overlay
|
|
14490
|
+
* itself becomes the question's frontmost suspended handle — so reverse
|
|
14491
|
+
* modal order survives the question. Question input keeps first priority
|
|
14492
|
+
* either way (routeInput).
|
|
14050
14493
|
* @param component - the overlay content.
|
|
14051
14494
|
* @param options - overlay sizing/positioning.
|
|
14052
14495
|
* @returns the handle; hide() also forgets the handle.
|
|
@@ -14054,6 +14497,27 @@ var TuiApp = class TuiApp {
|
|
|
14054
14497
|
showOverlayOnHost(component, options) {
|
|
14055
14498
|
const handle = this.overlayHost.showOverlay(component, options);
|
|
14056
14499
|
this.overlayHandles.add(handle);
|
|
14500
|
+
const question = this.activeQuestions;
|
|
14501
|
+
if (question !== void 0) {
|
|
14502
|
+
for (const other of this.overlayHandles) if (other !== handle && !other.isHidden()) {
|
|
14503
|
+
other.setHidden(true);
|
|
14504
|
+
question.suspendedOverlays.add(other);
|
|
14505
|
+
}
|
|
14506
|
+
if (options?.nonCapturing !== true) {
|
|
14507
|
+
const dependents = /* @__PURE__ */ new Set();
|
|
14508
|
+
for (const other of question.suspendedOverlays) dependents.add(other);
|
|
14509
|
+
if (dependents.size > 0) {
|
|
14510
|
+
for (const other of dependents) question.suspendedOverlays.delete(other);
|
|
14511
|
+
this.overlayDependents.set(handle, dependents);
|
|
14512
|
+
}
|
|
14513
|
+
}
|
|
14514
|
+
handle.setHidden(true);
|
|
14515
|
+
question.suspendedOverlays.add(handle);
|
|
14516
|
+
return {
|
|
14517
|
+
...handle,
|
|
14518
|
+
hide: () => this.closeOverlayHandle(handle)
|
|
14519
|
+
};
|
|
14520
|
+
}
|
|
14057
14521
|
if (options?.nonCapturing !== true) {
|
|
14058
14522
|
const hidden = /* @__PURE__ */ new Set();
|
|
14059
14523
|
for (const other of this.overlayHandles) if (other !== handle && !other.isHidden()) {
|
|
@@ -14064,15 +14528,33 @@ var TuiApp = class TuiApp {
|
|
|
14064
14528
|
}
|
|
14065
14529
|
return {
|
|
14066
14530
|
...handle,
|
|
14067
|
-
hide: () =>
|
|
14068
|
-
this.overlayDependents.get(handle)?.forEach((other) => other.setHidden(false));
|
|
14069
|
-
this.overlayDependents.delete(handle);
|
|
14070
|
-
this.overlayHandles.delete(handle);
|
|
14071
|
-
handle.hide();
|
|
14072
|
-
}
|
|
14531
|
+
hide: () => this.closeOverlayHandle(handle)
|
|
14073
14532
|
};
|
|
14074
14533
|
}
|
|
14075
14534
|
/**
|
|
14535
|
+
* Question-aware close for one tracked overlay handle (the wrapper's
|
|
14536
|
+
* hide). Without an active question this matches the historical behavior:
|
|
14537
|
+
* the handle's dependents are unhidden, the graph is cleaned, and the
|
|
14538
|
+
* overlay is removed. While a question owns the seat, the handle leaves
|
|
14539
|
+
* the question's suspension set, every dependency set drops it (no parent
|
|
14540
|
+
* retains a dead child), and its still-mounted dependents remain hidden
|
|
14541
|
+
* and become DIRECTLY owned by the question — they must not flash back
|
|
14542
|
+
* while the question is still up.
|
|
14543
|
+
*/
|
|
14544
|
+
closeOverlayHandle(handle) {
|
|
14545
|
+
const question = this.activeQuestions;
|
|
14546
|
+
if (question !== void 0) question.suspendedOverlays.delete(handle);
|
|
14547
|
+
for (const dependents of this.overlayDependents.values()) dependents.delete(handle);
|
|
14548
|
+
const owned = this.overlayDependents.get(handle);
|
|
14549
|
+
if (owned !== void 0) {
|
|
14550
|
+
this.overlayDependents.delete(handle);
|
|
14551
|
+
if (question !== void 0) for (const dependent of owned) question.suspendedOverlays.add(dependent);
|
|
14552
|
+
else for (const dependent of owned) dependent.setHidden(false);
|
|
14553
|
+
}
|
|
14554
|
+
this.overlayHandles.delete(handle);
|
|
14555
|
+
handle.hide();
|
|
14556
|
+
}
|
|
14557
|
+
/**
|
|
14076
14558
|
* Launch the external editor with the current draft. The TUI stops first
|
|
14077
14559
|
* (raw mode released) and restarts after the editor returns; a fullscreen
|
|
14078
14560
|
* mode is not restored (the editor session ends in regular mode).
|
|
@@ -14189,6 +14671,20 @@ var TuiApp = class TuiApp {
|
|
|
14189
14671
|
this.rebuildMessages();
|
|
14190
14672
|
}
|
|
14191
14673
|
/**
|
|
14674
|
+
* Drop SETTLED local cards (completed `!`/`!!` shell runs); running cards
|
|
14675
|
+
* survive so a live stream is never dismissed by a concurrent submit.
|
|
14676
|
+
* Local cards are a live view, not a record: context runs clear on submit
|
|
14677
|
+
* success (the transcript's user row takes over), local-only runs on the
|
|
14678
|
+
* next user submission (dispatchUserInput / steerNow).
|
|
14679
|
+
*/
|
|
14680
|
+
clearSettledLocalMessages() {
|
|
14681
|
+
const running = this.localMessages.filter((message) => message.kind === "tool" && message.status === "running");
|
|
14682
|
+
if (running.length === this.localMessages.length) return;
|
|
14683
|
+
this.localMessages.length = 0;
|
|
14684
|
+
this.localMessages.push(...running);
|
|
14685
|
+
this.rebuildMessages();
|
|
14686
|
+
}
|
|
14687
|
+
/**
|
|
14192
14688
|
* Toggle between regular (terminal scrollback) and fullscreen (alt screen).
|
|
14193
14689
|
* Overlays live on the active screen, so the switch hides every mounted
|
|
14194
14690
|
* overlay; a pending approval prompt is re-rendered on the new screen.
|
|
@@ -14211,6 +14707,8 @@ var TuiApp = class TuiApp {
|
|
|
14211
14707
|
pending?.handle?.hide();
|
|
14212
14708
|
for (const handle of this.overlayHandles) handle.hide();
|
|
14213
14709
|
this.overlayHandles.clear();
|
|
14710
|
+
this.overlayDependents.clear();
|
|
14711
|
+
if (this.activeQuestions !== void 0) this.activeQuestions.suspendedOverlays.clear();
|
|
14214
14712
|
if (enabled) {
|
|
14215
14713
|
const alt = new TuiAltScreen(this.terminal, void 0, void 0, { onCellClick: (x, y) => this.handleFullscreenClick(x, y) });
|
|
14216
14714
|
this.fullscreenScroll = new ScrollView(this.messagesView, {
|
|
@@ -14244,7 +14742,7 @@ var TuiApp = class TuiApp {
|
|
|
14244
14742
|
shrink: 0
|
|
14245
14743
|
},
|
|
14246
14744
|
{
|
|
14247
|
-
component: this.
|
|
14745
|
+
component: this.editorSeat,
|
|
14248
14746
|
shrink: 0
|
|
14249
14747
|
},
|
|
14250
14748
|
{
|
|
@@ -14258,6 +14756,7 @@ var TuiApp = class TuiApp {
|
|
|
14258
14756
|
alt.start();
|
|
14259
14757
|
alt.setFocus(this.editor);
|
|
14260
14758
|
this.fullscreen = alt;
|
|
14759
|
+
this.refreshSchemeRegistrations();
|
|
14261
14760
|
} else {
|
|
14262
14761
|
this.fullscreen?.stop();
|
|
14263
14762
|
this.fullscreen = void 0;
|
|
@@ -14265,9 +14764,12 @@ var TuiApp = class TuiApp {
|
|
|
14265
14764
|
this.tui.start();
|
|
14266
14765
|
this.tui.requestRender(true);
|
|
14267
14766
|
this.tui.setFocus(this.editor);
|
|
14767
|
+
this.refreshSchemeRegistrations();
|
|
14268
14768
|
}
|
|
14269
14769
|
this.events.onFullscreenChange?.(enabled);
|
|
14270
14770
|
if (pending !== void 0) this.renderApprovalDialog(pending);
|
|
14771
|
+
const question = this.activeQuestions;
|
|
14772
|
+
if (question?.frame !== void 0) (this.fullscreen ?? this.tui).setFocus(question.frame);
|
|
14271
14773
|
}
|
|
14272
14774
|
/**
|
|
14273
14775
|
* Open the transcript-search overlay (Ctrl+Shift+F) and focus its input.
|
|
@@ -14394,6 +14896,23 @@ var TuiApp = class TuiApp {
|
|
|
14394
14896
|
* The global Ctrl+O fold still wins, so keyboard behavior is unchanged.
|
|
14395
14897
|
*/
|
|
14396
14898
|
handleFullscreenClick(x, y) {
|
|
14899
|
+
const question = this.activeQuestions;
|
|
14900
|
+
if (question?.frame !== void 0) {
|
|
14901
|
+
if (this.terminal.rows !== question.frame.termRows) return;
|
|
14902
|
+
const width = this.terminal.columns;
|
|
14903
|
+
const height = this.terminal.rows;
|
|
14904
|
+
const footerHeight = this.footer.render(width).length;
|
|
14905
|
+
const seatHeight = question.frame.rows;
|
|
14906
|
+
const seatBottom = height - footerHeight;
|
|
14907
|
+
const seatTop = seatBottom - seatHeight;
|
|
14908
|
+
if (y >= seatTop && y < seatBottom) {
|
|
14909
|
+
if (x >= 2 && x <= width - 3) {
|
|
14910
|
+
question.flow.clickRow(y - seatTop - 1);
|
|
14911
|
+
this.requestRender();
|
|
14912
|
+
}
|
|
14913
|
+
return;
|
|
14914
|
+
}
|
|
14915
|
+
}
|
|
14397
14916
|
const width = this.terminal.columns;
|
|
14398
14917
|
const rowInScroll = y - this.header.render(width).length;
|
|
14399
14918
|
if (rowInScroll < 0) return;
|
|
@@ -14467,11 +14986,46 @@ var TuiApp = class TuiApp {
|
|
|
14467
14986
|
/**
|
|
14468
14987
|
* Replace the editor draft. The runner restores a submission that the
|
|
14469
14988
|
* divergence guard blocked, so the user's text survives for a retry.
|
|
14989
|
+
* While the subagent viewer covers the editor, the write goes to the
|
|
14990
|
+
* preserved draft (the placeholder bar stays up; the draft is restored
|
|
14991
|
+
* on exit).
|
|
14470
14992
|
*/
|
|
14471
14993
|
setEditorText(text) {
|
|
14994
|
+
if (this.viewerMode !== void 0) {
|
|
14995
|
+
this.draftBeforeViewer = text;
|
|
14996
|
+
return;
|
|
14997
|
+
}
|
|
14472
14998
|
this.editor.setText(text);
|
|
14473
14999
|
}
|
|
14474
15000
|
/**
|
|
15001
|
+
* Enter or leave the read-only subagent viewer. While viewing, the
|
|
15002
|
+
* editor bar is covered by a placeholder (the real draft is preserved
|
|
15003
|
+
* and restored on exit), the editor border switches to the accent color,
|
|
15004
|
+
* and the header shows a persistent `[viewing subagent]` badge — so the
|
|
15005
|
+
* mode reads at a glance instead of relying on the transient notify.
|
|
15006
|
+
* @param mode - the child identity + label; `undefined` leaves the viewer.
|
|
15007
|
+
*/
|
|
15008
|
+
setViewerMode(mode) {
|
|
15009
|
+
if (mode === void 0) {
|
|
15010
|
+
if (this.viewerMode === void 0) return;
|
|
15011
|
+
this.viewerMode = void 0;
|
|
15012
|
+
this.editor.borderColor = this.editorBorder;
|
|
15013
|
+
this.editor.setText(this.draftBeforeViewer ?? "");
|
|
15014
|
+
this.draftBeforeViewer = void 0;
|
|
15015
|
+
this.editor.invalidate();
|
|
15016
|
+
this.renderHeader();
|
|
15017
|
+
this.requestRender();
|
|
15018
|
+
return;
|
|
15019
|
+
}
|
|
15020
|
+
if (this.viewerMode === void 0) this.draftBeforeViewer = this.editor.getText();
|
|
15021
|
+
this.viewerMode = mode;
|
|
15022
|
+
this.editor.borderColor = color.accent;
|
|
15023
|
+
this.editor.setText(`viewing subagent: ${mode.label} — read-only · Esc returns`);
|
|
15024
|
+
this.editor.invalidate();
|
|
15025
|
+
this.renderHeader();
|
|
15026
|
+
this.requestRender();
|
|
15027
|
+
}
|
|
15028
|
+
/**
|
|
14475
15029
|
* Set the session head rendered above the transcript: the session identity,
|
|
14476
15030
|
* model, version, preset, and cwd, wrapped to the terminal width with a
|
|
14477
15031
|
* full-width rule beneath. Replaces any previous head.
|
|
@@ -14568,16 +15122,8 @@ var TuiApp = class TuiApp {
|
|
|
14568
15122
|
}
|
|
14569
15123
|
/** Render one transcript message as a pi-tui component. */
|
|
14570
15124
|
renderMessage(message, boundary) {
|
|
14571
|
-
if (message.kind === "user") {
|
|
14572
|
-
|
|
14573
|
-
const indent = " ".repeat(visibleWidth(prefix));
|
|
14574
|
-
return new Text(new Text(message.text, 0, 0).render(Math.max(1, this.terminal.columns - visibleWidth(prefix))).map((line, index) => (index === 0 ? prefix : indent) + line).join("\n"), 0, 0);
|
|
14575
|
-
}
|
|
14576
|
-
if (message.kind === "assistant") {
|
|
14577
|
-
const prefix = `${color.primary("🐋")} `;
|
|
14578
|
-
const indent = " ".repeat(visibleWidth(prefix));
|
|
14579
|
-
return new Text(new Markdown(message.text, 0, 0, markdownTheme).render(Math.max(1, this.terminal.columns - visibleWidth(prefix))).map((line, index) => (index === 0 ? prefix : indent) + line).join("\n"), 0, 0);
|
|
14580
|
-
}
|
|
15125
|
+
if (message.kind === "user") return new BulletedComponent(new Text(message.text, 0, 0), `${color.roleUser("❯")} `);
|
|
15126
|
+
if (message.kind === "assistant") return new BulletedComponent(new Markdown(message.text, 0, 0, markdownTheme), `${color.primary("🐋")} `);
|
|
14581
15127
|
if (message.kind === "thinking") {
|
|
14582
15128
|
if (message.turn >= boundary || this.expandedOverride.get(message) === true) return new Text(color.textDimItalic(`🐳 ${message.text}`), 0, 0);
|
|
14583
15129
|
const lines = message.text.split("\n").filter((line) => line !== "");
|
|
@@ -14649,6 +15195,34 @@ var TuiApp = class TuiApp {
|
|
|
14649
15195
|
return card;
|
|
14650
15196
|
}
|
|
14651
15197
|
/**
|
|
15198
|
+
* The expanded card's terminal-command row: `$ cmd` (bash) or `PS> cmd`
|
|
15199
|
+
* (pwsh) in the shell prompt token, dimmed (kimi ShellExecution parity —
|
|
15200
|
+
* the folded card shows the command; the expanded body must not drop
|
|
15201
|
+
* it). Multi-line commands render every line under the prompt. No-op for
|
|
15202
|
+
* an empty command.
|
|
15203
|
+
* @param card - the card container to fill.
|
|
15204
|
+
* @param command - the command text.
|
|
15205
|
+
* @param prompt - the shell prompt token (`$ ` or `PS> `).
|
|
15206
|
+
*/
|
|
15207
|
+
addTerminalCommandRow(card, command, prompt) {
|
|
15208
|
+
if (command === "") return;
|
|
15209
|
+
const styledPrompt = color.shellMode(prompt);
|
|
15210
|
+
const lines = command.split("\n");
|
|
15211
|
+
for (const [index, line] of lines.entries()) {
|
|
15212
|
+
const prefix = index === 0 ? styledPrompt : " ".repeat(visibleWidth(styledPrompt));
|
|
15213
|
+
card.addChild(new Text(`${prefix}${color.textDim(line)}`, 0, 0));
|
|
15214
|
+
}
|
|
15215
|
+
}
|
|
15216
|
+
/** The bash/pwsh command from the raw args ('' when not a terminal call). */
|
|
15217
|
+
terminalCommand(name, argsRaw) {
|
|
15218
|
+
const call = parseCallPreview(name, argsRaw);
|
|
15219
|
+
return call?.kind === "bash" ? call.command : "";
|
|
15220
|
+
}
|
|
15221
|
+
/** The shell prompt token for a terminal tool (pwsh renders `PS> `). */
|
|
15222
|
+
shellPrompt(name) {
|
|
15223
|
+
return name === "pwsh" ? "PS> " : "$ ";
|
|
15224
|
+
}
|
|
15225
|
+
/**
|
|
14652
15226
|
* Render one expanded tool card's body. When the runner wired a presenter,
|
|
14653
15227
|
* the body follows the tool's own render intent (presentResult): a read
|
|
14654
15228
|
* card shows numbered lines plus the relativized path and total line
|
|
@@ -14689,12 +15263,18 @@ var TuiApp = class TuiApp {
|
|
|
14689
15263
|
this.renderDiffBody(card, callView.diffs, explicitlyExpanded);
|
|
14690
15264
|
return;
|
|
14691
15265
|
}
|
|
15266
|
+
if (callView.card === "terminal") {
|
|
15267
|
+
this.addTerminalCommandRow(card, callView.title, this.shellPrompt(message.name));
|
|
15268
|
+
return;
|
|
15269
|
+
}
|
|
14692
15270
|
if (callView.card === "generic" && callView.rawInput !== void 0) {
|
|
15271
|
+
this.addTerminalCommandRow(card, this.terminalCommand(message.name, message.args), this.shellPrompt(message.name));
|
|
14693
15272
|
const raw = typeof callView.rawInput === "string" ? callView.rawInput : JSON.stringify(callView.rawInput, null, 2);
|
|
14694
15273
|
card.addChild(new Text(color.textDim(raw), 0, 0));
|
|
14695
15274
|
return;
|
|
14696
15275
|
}
|
|
14697
15276
|
}
|
|
15277
|
+
this.addTerminalCommandRow(card, this.terminalCommand(message.name, message.args), this.shellPrompt(message.name));
|
|
14698
15278
|
return;
|
|
14699
15279
|
}
|
|
14700
15280
|
if (message.result === "" && (message.resultBlocks?.length ?? 0) === 0) return;
|
|
@@ -14722,6 +15302,7 @@ var TuiApp = class TuiApp {
|
|
|
14722
15302
|
}
|
|
14723
15303
|
return;
|
|
14724
15304
|
case "terminal": {
|
|
15305
|
+
this.addTerminalCommandRow(card, this.terminalCommand(message.name, message.args), this.shellPrompt(message.name));
|
|
14725
15306
|
if (resultView.output !== void 0 && resultView.output !== "") for (const line of resultView.output.split("\n")) card.addChild(new Text(color.textDim(line), 0, 0));
|
|
14726
15307
|
const exit = resultView.exitCode !== void 0 ? `[exit ${resultView.exitCode}]` : resultView.signal !== void 0 ? `[signal ${resultView.signal}]` : "";
|
|
14727
15308
|
if (exit !== "") card.addChild(new Text(color.textDim(exit), 0, 0));
|
|
@@ -14755,7 +15336,10 @@ var TuiApp = class TuiApp {
|
|
|
14755
15336
|
}
|
|
14756
15337
|
}
|
|
14757
15338
|
if (isDiffResult(message.name, message.result)) for (const line of renderDiffLines(message.result)) card.addChild(new Text(line, 0, 0));
|
|
14758
|
-
else
|
|
15339
|
+
else {
|
|
15340
|
+
this.addTerminalCommandRow(card, this.terminalCommand(message.name, message.args), this.shellPrompt(message.name));
|
|
15341
|
+
card.addChild(new Text(color.textDim(message.result), 0, 0));
|
|
15342
|
+
}
|
|
14759
15343
|
}
|
|
14760
15344
|
/**
|
|
14761
15345
|
* Fill a tool card with a diff body: LCS-aligned clustered lines, capped
|
|
@@ -14846,8 +15430,9 @@ var TuiApp = class TuiApp {
|
|
|
14846
15430
|
* switch only has to re-run this. */
|
|
14847
15431
|
renderHeader() {
|
|
14848
15432
|
const badge = this.planMode ? ` ${color.warning("[plan]")}` : "";
|
|
14849
|
-
const
|
|
14850
|
-
this.
|
|
15433
|
+
const viewerBadge = this.viewerMode === void 0 ? "" : ` ${color.accent("[viewing subagent]")}`;
|
|
15434
|
+
const title = this.viewerMode !== void 0 ? ` · ${color.textMuted(this.viewerMode.label)}` : this.sessionTitleText === "" ? "" : ` · ${color.textMuted(this.sessionTitleText)}`;
|
|
15435
|
+
this.header.setText(`🐋 dsh-pi-tui${title}${this.todoText}${badge}${viewerBadge}`);
|
|
14851
15436
|
this.requestRender();
|
|
14852
15437
|
}
|
|
14853
15438
|
/**
|
|
@@ -14871,7 +15456,20 @@ var TuiApp = class TuiApp {
|
|
|
14871
15456
|
*/
|
|
14872
15457
|
setTasks(tasks) {
|
|
14873
15458
|
this.dockTasks = tasks;
|
|
14874
|
-
this.tasksActive = tasks.length > 0;
|
|
15459
|
+
this.tasksActive = tasks.length > 0 || this.dockAgents.length > 0;
|
|
15460
|
+
this.renderDock();
|
|
15461
|
+
this.renderFooter();
|
|
15462
|
+
}
|
|
15463
|
+
/**
|
|
15464
|
+
* Replace the live child-subagent list for the dock lines and the
|
|
15465
|
+
* footer badge. Continuable children and foreground one-shot children
|
|
15466
|
+
* never register jobs records (AGENTS.md), so they arm the ↓/Ctrl+J
|
|
15467
|
+
* trigger through this channel.
|
|
15468
|
+
* @param agents - live children (id + label + activity), empty to hide.
|
|
15469
|
+
*/
|
|
15470
|
+
setAgents(agents) {
|
|
15471
|
+
this.dockAgents = agents;
|
|
15472
|
+
this.tasksActive = this.dockTasks.length > 0 || agents.length > 0;
|
|
14875
15473
|
this.renderDock();
|
|
14876
15474
|
this.renderFooter();
|
|
14877
15475
|
}
|
|
@@ -14911,15 +15509,35 @@ var TuiApp = class TuiApp {
|
|
|
14911
15509
|
}
|
|
14912
15510
|
/**
|
|
14913
15511
|
* Replace the editor draft wholesale (the Alt+↑ dequeue path pulls every
|
|
14914
|
-
* queued message back into the editor for editing).
|
|
15512
|
+
* queued message back into the editor for editing). While the subagent
|
|
15513
|
+
* viewer covers the editor, the write goes to the preserved draft.
|
|
14915
15514
|
*/
|
|
14916
15515
|
setDraft(text) {
|
|
15516
|
+
if (this.viewerMode !== void 0) {
|
|
15517
|
+
this.draftBeforeViewer = text;
|
|
15518
|
+
return;
|
|
15519
|
+
}
|
|
14917
15520
|
this.editor.setText(text);
|
|
14918
15521
|
this.requestRender();
|
|
14919
15522
|
}
|
|
14920
|
-
/** The editor's current draft text (the Alt+↑ dequeue merge reads it)
|
|
15523
|
+
/** The editor's current draft text (the Alt+↑ dequeue merge reads it);
|
|
15524
|
+
* while the viewer covers the editor, the preserved draft is returned. */
|
|
14921
15525
|
getDraft() {
|
|
14922
|
-
return this.editor.getText();
|
|
15526
|
+
return this.viewerMode !== void 0 && this.draftBeforeViewer !== void 0 ? this.draftBeforeViewer : this.editor.getText();
|
|
15527
|
+
}
|
|
15528
|
+
/**
|
|
15529
|
+
* Headless-test hook: current overlay tracking-graph sizes. The graph
|
|
15530
|
+
* (overlayHandles / overlayDependents / the active question's suspension)
|
|
15531
|
+
* is behaviorally invisible — stale entries only leak memory — so the
|
|
15532
|
+
* headless suite asserts its sizes directly (e.g. the fullscreen teardown
|
|
15533
|
+
* must leave it empty instead of retaining dead handles).
|
|
15534
|
+
*/
|
|
15535
|
+
overlayGraphState() {
|
|
15536
|
+
return {
|
|
15537
|
+
handles: this.overlayHandles.size,
|
|
15538
|
+
dependents: this.overlayDependents.size,
|
|
15539
|
+
suspended: this.activeQuestions?.suspendedOverlays.size ?? 0
|
|
15540
|
+
};
|
|
14923
15541
|
}
|
|
14924
15542
|
/**
|
|
14925
15543
|
* Rebuild the persistent dock strip above the todo panel: todo summary and
|
|
@@ -14952,6 +15570,14 @@ var TuiApp = class TuiApp {
|
|
|
14952
15570
|
}
|
|
14953
15571
|
if (this.dockTasks.length > 3) lines.push(color.textDim(` … ${this.dockTasks.length - 3} more`));
|
|
14954
15572
|
}
|
|
15573
|
+
if (this.dockAgents.length > 0) {
|
|
15574
|
+
const shown = this.dockAgents.slice(0, 3);
|
|
15575
|
+
for (const agent of shown) {
|
|
15576
|
+
const label = agent.label.length > 40 ? `${agent.label.slice(0, 40)}…` : agent.label;
|
|
15577
|
+
lines.push(color.textDim(`🤖 ${agent.id} · ${label}`));
|
|
15578
|
+
}
|
|
15579
|
+
if (this.dockAgents.length > 3) lines.push(color.textDim(` … ${this.dockAgents.length - 3} more`));
|
|
15580
|
+
}
|
|
14955
15581
|
this.dock.setText(lines.join("\n"));
|
|
14956
15582
|
this.requestRender();
|
|
14957
15583
|
}
|
|
@@ -14969,12 +15595,17 @@ var TuiApp = class TuiApp {
|
|
|
14969
15595
|
/** Rebuild the two footer lines from the current status and plan badge. */
|
|
14970
15596
|
renderFooter() {
|
|
14971
15597
|
const context = this.status.contextTokens !== void 0 && this.status.contextWindow !== void 0 && this.status.contextWindow > 0 ? contextBar(this.status.contextTokens, this.status.contextWindow) : "";
|
|
15598
|
+
const permissionBadge = this.status.permission === "danger-full-access" ? color.warning("[yolo]") : this.status.permission === "read-only" ? color.textMuted("[read-only]") : this.status.permission === "workspace-write" ? color.text("[workspace-write]") : this.status.permission === "custom" ? color.warning("[custom]") : "";
|
|
15599
|
+
const badgeParts = [];
|
|
15600
|
+
if (this.dockTasks.length > 0) badgeParts.push(`${this.dockTasks.length} task${this.dockTasks.length === 1 ? "" : "s"} running`);
|
|
15601
|
+
if (this.dockAgents.length > 0) badgeParts.push(`${this.dockAgents.length} agent${this.dockAgents.length === 1 ? "" : "s"}`);
|
|
15602
|
+
const taskBadge = badgeParts.length === 0 ? "" : color.primary(`[${badgeParts.join(" · ")}${this.editor.getText().trim() === "" ? " · ↓ view" : ""}]`);
|
|
14972
15603
|
const line1 = [
|
|
14973
|
-
|
|
15604
|
+
permissionBadge,
|
|
14974
15605
|
this.planMode ? color.warning("[plan]") : "",
|
|
14975
15606
|
this.status.goal === void 0 || this.status.goal === "" ? "" : color.primary(this.status.goal),
|
|
14976
15607
|
this.status.model === "" ? "" : `[${this.status.model}]`,
|
|
14977
|
-
|
|
15608
|
+
taskBadge,
|
|
14978
15609
|
this.status.cwd,
|
|
14979
15610
|
this.status.branch === "" ? "" : this.status.branch,
|
|
14980
15611
|
context,
|
|
@@ -15121,25 +15752,85 @@ var TuiApp = class TuiApp {
|
|
|
15121
15752
|
* FORCE_COLOR=0 / CI) stay dark without querying; a terminal that never
|
|
15122
15753
|
* answers falls back to COLORFGBG (VT100/xterm convention); a terminal
|
|
15123
15754
|
* with neither leaves the current theme untouched.
|
|
15755
|
+
*
|
|
15756
|
+
* The query targets the screen that OWNS the terminal input handler (the
|
|
15757
|
+
* alt screen in fullscreen mode): a query on the stopped main screen
|
|
15758
|
+
* would have its reply swallowed by the alt screen's OSC 11 consumer and
|
|
15759
|
+
* time out, silently turning `auto` into a no-op.
|
|
15760
|
+
*
|
|
15761
|
+
* Concurrent calls COALESCE onto one shared in-flight query — overlapping
|
|
15762
|
+
* OSC 11 queries have no sequence ids, so replies would mis-pair by FIFO
|
|
15763
|
+
* order. The LATEST {@link AutoDetectOptions.shouldApply} guard is
|
|
15764
|
+
* consulted at settle time, so a late result can never override a newer
|
|
15765
|
+
* explicit theme choice.
|
|
15766
|
+
* @param options.shouldApply - returns false to drop the settled result
|
|
15767
|
+
* instead of applying it (default: always apply).
|
|
15124
15768
|
*/
|
|
15125
|
-
async autoDetectTheme() {
|
|
15769
|
+
async autoDetectTheme(options) {
|
|
15126
15770
|
if (themeOptOut()) return;
|
|
15127
|
-
|
|
15771
|
+
if (options?.shouldApply !== void 0) this.autoDetectGuard = options.shouldApply;
|
|
15772
|
+
if (this.autoDetectInFlight === void 0) this.autoDetectInFlight = this.runAutoDetect().finally(() => {
|
|
15773
|
+
this.autoDetectInFlight = void 0;
|
|
15774
|
+
this.autoDetectGuard = void 0;
|
|
15775
|
+
});
|
|
15776
|
+
return this.autoDetectInFlight;
|
|
15777
|
+
}
|
|
15778
|
+
async runAutoDetect() {
|
|
15779
|
+
const rgb = await this.overlayHost.queryTerminalBackgroundColor({ timeoutMs: 800 });
|
|
15780
|
+
const guard = this.autoDetectGuard;
|
|
15781
|
+
const apply = () => guard === void 0 || guard();
|
|
15128
15782
|
if (rgb !== void 0) {
|
|
15129
|
-
this.applyTheme(detectThemeFromBackground(rgb));
|
|
15783
|
+
if (apply()) this.applyTheme(detectThemeFromBackground(rgb));
|
|
15130
15784
|
return;
|
|
15131
15785
|
}
|
|
15132
15786
|
const fromEnv = detectThemeFromColorFgBg();
|
|
15133
|
-
if (fromEnv !== void 0) this.applyTheme(fromEnv);
|
|
15787
|
+
if (fromEnv !== void 0 && apply()) this.applyTheme(fromEnv);
|
|
15788
|
+
}
|
|
15789
|
+
/**
|
|
15790
|
+
* Follow the terminal's live colour-scheme reports while `following` is
|
|
15791
|
+
* true. Enabling sends one DSR 996 query — xterm-class terminals only
|
|
15792
|
+
* START reporting after being asked — and reported schemes fan out to the
|
|
15793
|
+
* {@link onTerminalThemeChange} listeners (which guard whether to apply,
|
|
15794
|
+
* typically against the persisted `auto` preference). Disabling stops
|
|
15795
|
+
* tracking. Idempotent.
|
|
15796
|
+
* @param following - whether to track and apply live scheme reports.
|
|
15797
|
+
*/
|
|
15798
|
+
trackTerminalTheme(following) {
|
|
15799
|
+
if (following === this.followingTerminalTheme) return;
|
|
15800
|
+
this.followingTerminalTheme = following;
|
|
15801
|
+
if (!following) return;
|
|
15802
|
+
this.overlayHost.queryTerminalColorScheme({ timeoutMs: 800 }).then((scheme) => {
|
|
15803
|
+
if (scheme === void 0 || !this.followingTerminalTheme) return;
|
|
15804
|
+
for (const listener of [...this.terminalSchemeListeners]) listener(scheme);
|
|
15805
|
+
}).catch(() => {});
|
|
15134
15806
|
}
|
|
15135
15807
|
/**
|
|
15136
15808
|
* Register a live terminal-theme listener (colour-scheme reports). The
|
|
15137
|
-
*
|
|
15809
|
+
* listener is registered on EVERY screen: reports arrive only at the
|
|
15810
|
+
* screen that owns the terminal input handler (the alt screen in
|
|
15811
|
+
* fullscreen mode), so the registrations never double-fire. The caller
|
|
15812
|
+
* guards whether to apply (typically: only while the persisted preference
|
|
15813
|
+
* is `auto`).
|
|
15138
15814
|
* @param listener - receives the detected palette family.
|
|
15139
15815
|
* @returns a disposer.
|
|
15140
15816
|
*/
|
|
15141
15817
|
onTerminalThemeChange(listener) {
|
|
15142
|
-
|
|
15818
|
+
this.terminalSchemeListeners.add(listener);
|
|
15819
|
+
this.refreshSchemeRegistrations();
|
|
15820
|
+
return () => {
|
|
15821
|
+
this.terminalSchemeListeners.delete(listener);
|
|
15822
|
+
this.refreshSchemeRegistrations();
|
|
15823
|
+
};
|
|
15824
|
+
}
|
|
15825
|
+
/** (Re)register the scheme-report fan-out on every screen. */
|
|
15826
|
+
refreshSchemeRegistrations() {
|
|
15827
|
+
for (const dispose of this.schemeDisposers) dispose();
|
|
15828
|
+
this.schemeDisposers = [];
|
|
15829
|
+
const screens = [this.tui];
|
|
15830
|
+
if (this.fullscreen !== void 0) screens.push(this.fullscreen);
|
|
15831
|
+
for (const screen of screens) this.schemeDisposers.push(screen.onTerminalColorSchemeChange((scheme) => {
|
|
15832
|
+
for (const listener of [...this.terminalSchemeListeners]) listener(scheme);
|
|
15833
|
+
}));
|
|
15143
15834
|
}
|
|
15144
15835
|
/**
|
|
15145
15836
|
* Queue an approval prompt and resolve when the user decides. Requests
|
|
@@ -15181,19 +15872,31 @@ var TuiApp = class TuiApp {
|
|
|
15181
15872
|
}
|
|
15182
15873
|
/** Build and mount the approval dialog for one prompt on the active screen. */
|
|
15183
15874
|
renderApprovalDialog(pending) {
|
|
15875
|
+
const width = this.terminal.columns;
|
|
15876
|
+
const maxHeight = Math.max(8, Math.min(16, this.terminal.rows - 2));
|
|
15877
|
+
const contentWidth = Math.max(1, width - 8);
|
|
15878
|
+
const titleShown = truncateToWidth(`Approve ${pending.request.toolName}?`, contentWidth, "…");
|
|
15879
|
+
const dangerShown = pending.request.danger === true ? truncateToWidth("⚠ DANGEROUS COMMAND — confirm carefully", contentWidth, "…") : "";
|
|
15880
|
+
const hintShown = capWrappedToHeight("[y] allow once [n] reject [esc/ctrl+c] cancel", contentWidth, Math.max(0, maxHeight - (1 + (dangerShown === "" ? 0 : 1) + 1 + 2 + 2))).text;
|
|
15881
|
+
const hintWrapped = hintShown === "" ? 0 : wrapTextWithAnsi(hintShown, contentWidth).length;
|
|
15882
|
+
const chrome = 1 + (dangerShown === "" ? 0 : 1) + 1 + hintWrapped + 2 + 2;
|
|
15883
|
+
const reasonBudget = Math.max(0, maxHeight - chrome);
|
|
15884
|
+
const reasonShown = capWrappedToMarker(pending.request.reason ?? "", contentWidth, reasonBudget).text;
|
|
15885
|
+
const reasonWrapped = reasonShown === "" ? 0 : wrapTextWithAnsi(reasonShown, contentWidth).length;
|
|
15886
|
+
const previewBudget = Math.max(0, maxHeight - chrome - reasonWrapped);
|
|
15184
15887
|
const dialog = new Box(1, 1);
|
|
15185
|
-
dialog.addChild(new Text(
|
|
15186
|
-
if (
|
|
15187
|
-
if (pending.request.arguments !== void 0 && pending.request.arguments !== "") {
|
|
15188
|
-
const
|
|
15189
|
-
dialog.addChild(new Text(color.textDim(
|
|
15190
|
-
}
|
|
15191
|
-
if (
|
|
15192
|
-
dialog.addChild(new Text(""));
|
|
15193
|
-
dialog.addChild(new Text(
|
|
15888
|
+
dialog.addChild(new Text(titleShown, 1, 0));
|
|
15889
|
+
if (dangerShown !== "") dialog.addChild(new Text(color.error(dangerShown), 1, 0));
|
|
15890
|
+
if (pending.request.arguments !== void 0 && pending.request.arguments !== "" && previewBudget > 0) {
|
|
15891
|
+
const previewShown = capWrappedToMarker(pending.request.arguments.split("\n").slice(0, 6).join("\n"), contentWidth, previewBudget).text;
|
|
15892
|
+
if (previewShown !== "") dialog.addChild(new Text(color.textDim(previewShown), 1, 0));
|
|
15893
|
+
}
|
|
15894
|
+
if (reasonShown !== "") dialog.addChild(new Text(reasonShown, 1, 0));
|
|
15895
|
+
dialog.addChild(new Text(" ", 1, 0));
|
|
15896
|
+
dialog.addChild(new Text(hintShown, 1, 0));
|
|
15194
15897
|
pending.handle = this.showOverlayOnHost(new Frame(dialog), {
|
|
15195
|
-
width
|
|
15196
|
-
maxHeight
|
|
15898
|
+
width,
|
|
15899
|
+
maxHeight
|
|
15197
15900
|
});
|
|
15198
15901
|
}
|
|
15199
15902
|
/** Route a key while a prompt is showing; every key is consumed. */
|
|
@@ -15252,6 +15955,7 @@ var TuiApp = class TuiApp {
|
|
|
15252
15955
|
...question.multiSelect !== void 0 ? { multiSelect: question.multiSelect } : {},
|
|
15253
15956
|
...question.intent !== void 0 ? { intent: question.intent } : {}
|
|
15254
15957
|
})), (answers) => this.settleQuestions(state, answers), () => this.settleQuestions(state, void 0)),
|
|
15958
|
+
suspendedOverlays: /* @__PURE__ */ new Set(),
|
|
15255
15959
|
resolve,
|
|
15256
15960
|
reject,
|
|
15257
15961
|
signal
|
|
@@ -15269,13 +15973,20 @@ var TuiApp = class TuiApp {
|
|
|
15269
15973
|
else this.questionQueue.push(state);
|
|
15270
15974
|
});
|
|
15271
15975
|
}
|
|
15272
|
-
/** Mount one flow
|
|
15976
|
+
/** Mount one flow into the editor seat and make it the active one. */
|
|
15273
15977
|
presentQuestion(state) {
|
|
15274
15978
|
this.activeQuestions = state;
|
|
15275
|
-
|
|
15276
|
-
|
|
15277
|
-
|
|
15278
|
-
}
|
|
15979
|
+
for (const handle of this.overlayHandles) if (!handle.isHidden()) {
|
|
15980
|
+
handle.setHidden(true);
|
|
15981
|
+
state.suspendedOverlays.add(handle);
|
|
15982
|
+
}
|
|
15983
|
+
const frame = new QuestionFrame(state.flow, () => this.terminal.rows);
|
|
15984
|
+
state.frame = frame;
|
|
15985
|
+
this.editorSeat.clear();
|
|
15986
|
+
this.editorSeat.addChild(frame);
|
|
15987
|
+
const screen = this.fullscreen ?? this.tui;
|
|
15988
|
+
screen.setFocus(frame);
|
|
15989
|
+
screen.requestRender();
|
|
15279
15990
|
}
|
|
15280
15991
|
/** Abort one flow (its signal fired). The ACTIVE flow settles rejected
|
|
15281
15992
|
* and the queue advances; a QUEUED flow is removed from the queue and
|
|
@@ -15293,12 +16004,6 @@ var TuiApp = class TuiApp {
|
|
|
15293
16004
|
if (index !== -1) this.questionQueue.splice(index, 1);
|
|
15294
16005
|
state.reject(cancellationError("question flow cancelled"));
|
|
15295
16006
|
}
|
|
15296
|
-
/** Show the next queued flow, if any (called after the active one settles). */
|
|
15297
|
-
nextQuestion() {
|
|
15298
|
-
const next = this.questionQueue.shift();
|
|
15299
|
-
if (next === void 0) return;
|
|
15300
|
-
this.presentQuestion(next);
|
|
15301
|
-
}
|
|
15302
16007
|
/** Cancel every pending flow: each promise rejects exactly once. */
|
|
15303
16008
|
cancelQuestionFlows() {
|
|
15304
16009
|
for (const state of [...this.questionQueue]) this.abortQuestion(state);
|
|
@@ -15316,13 +16021,36 @@ var TuiApp = class TuiApp {
|
|
|
15316
16021
|
settleQuestions(state, answers) {
|
|
15317
16022
|
if (this.activeQuestions !== state || state.settled === true) return;
|
|
15318
16023
|
state.settled = true;
|
|
15319
|
-
this.activeQuestions = void 0;
|
|
15320
|
-
state.handle?.hide();
|
|
15321
16024
|
if (state.onAbort !== void 0 && state.signal !== void 0) state.signal.removeEventListener("abort", state.onAbort);
|
|
15322
|
-
this.
|
|
16025
|
+
const next = this.questionQueue.shift();
|
|
16026
|
+
if (next !== void 0) {
|
|
16027
|
+
next.suspendedOverlays = state.suspendedOverlays;
|
|
16028
|
+
state.suspendedOverlays = /* @__PURE__ */ new Set();
|
|
16029
|
+
const frame = new QuestionFrame(next.flow, () => this.terminal.rows);
|
|
16030
|
+
next.frame = frame;
|
|
16031
|
+
this.editorSeat.clear();
|
|
16032
|
+
this.editorSeat.addChild(frame);
|
|
16033
|
+
this.activeQuestions = next;
|
|
16034
|
+
const screen = this.fullscreen ?? this.tui;
|
|
16035
|
+
screen.setFocus(frame);
|
|
16036
|
+
screen.requestRender();
|
|
16037
|
+
this.settle(state, answers);
|
|
16038
|
+
return;
|
|
16039
|
+
}
|
|
16040
|
+
this.editorSeat.clear();
|
|
16041
|
+
this.editorSeat.addChild(this.editor);
|
|
16042
|
+
this.activeQuestions = void 0;
|
|
16043
|
+
const screen = this.fullscreen ?? this.tui;
|
|
16044
|
+
screen.setFocus(this.editor);
|
|
16045
|
+
for (const handle of state.suspendedOverlays) if (this.overlayHandles.has(handle)) handle.setHidden(false);
|
|
16046
|
+
state.suspendedOverlays.clear();
|
|
16047
|
+
screen.requestRender();
|
|
16048
|
+
this.settle(state, answers);
|
|
16049
|
+
}
|
|
16050
|
+
/** Resolve or reject the settled promise (exactly once, by construction). */
|
|
16051
|
+
settle(state, answers) {
|
|
15323
16052
|
if (answers === void 0) state.reject(cancellationError("question flow cancelled"));
|
|
15324
16053
|
else state.resolve(answers);
|
|
15325
|
-
this.nextQuestion();
|
|
15326
16054
|
}
|
|
15327
16055
|
};
|
|
15328
16056
|
const dim = color.textDim;
|
|
@@ -15336,6 +16064,101 @@ function startProcessTui(events, options = {}) {
|
|
|
15336
16064
|
return app;
|
|
15337
16065
|
}
|
|
15338
16066
|
//#endregion
|
|
16067
|
+
//#region src/tasks-browser.ts
|
|
16068
|
+
/**
|
|
16069
|
+
* Task-browser row model — the merged view over the two background surfaces.
|
|
16070
|
+
*
|
|
16071
|
+
* Source A — the jobs registry (`ctx.jobs`): bash jobs and one-shot
|
|
16072
|
+
* subagent jobs. A job row is STATUS-ONLY:
|
|
16073
|
+
* - a bash job's output has a single read cursor owned by the model's
|
|
16074
|
+
* `job_output`; consuming it from the UI would swallow the main
|
|
16075
|
+
* session's result;
|
|
16076
|
+
* - a one-shot subagent job record carries no child session id, so it can
|
|
16077
|
+
* never be matched to its child transcript — label/order/time are never
|
|
16078
|
+
* identity (see subagentJobTranscriptId).
|
|
16079
|
+
*
|
|
16080
|
+
* Source B — the subagent registry (`ctx.subagents.listChildren`):
|
|
16081
|
+
* live child subagents. Continuable children are separate durable
|
|
16082
|
+
* conversations that deliver no result to the parent, so Enter may open
|
|
16083
|
+
* the read-only transcript viewer directly. A RUNNING one-shot child
|
|
16084
|
+
* (the parent's pending foreground tool call, which registers no job
|
|
16085
|
+
* record) is merged the same way, so the ↓ trigger stays armed while a
|
|
16086
|
+
* foreground delegation is in flight. A finished one-shot child is not
|
|
16087
|
+
* merged — its work is over and `/subagents` remains its surface.
|
|
16088
|
+
*
|
|
16089
|
+
* Deliberate overlap: a running BACKGROUND one-shot has BOTH a job record
|
|
16090
|
+
* and a child record with no cross-reference, so it may appear twice — the
|
|
16091
|
+
* job row (status-only) and the child row (transcript viewable). Dedup is
|
|
16092
|
+
* impossible without the missing childSessionId on the job record; the
|
|
16093
|
+
* viewable child row is strictly more useful, so the overlap is accepted.
|
|
16094
|
+
*
|
|
16095
|
+
* Pure and injectable so row typing, ordering, and descriptions are
|
|
16096
|
+
* unit-testable without any dsh service.
|
|
16097
|
+
* @module @xmoon76/dsh-pi-tui/tasks-browser
|
|
16098
|
+
*/
|
|
16099
|
+
/** Picker-value prefix for a subagent row. */
|
|
16100
|
+
const AGENT_ROW_PREFIX = "agent:";
|
|
16101
|
+
/** Picker-value prefix for a job row. */
|
|
16102
|
+
const JOB_ROW_PREFIX = "job:";
|
|
16103
|
+
/** Picker group label for continuable subagents. */
|
|
16104
|
+
const SUBAGENT_GROUP = "subagents";
|
|
16105
|
+
/** Picker group label for jobs. */
|
|
16106
|
+
const JOB_GROUP = "jobs";
|
|
16107
|
+
const isActiveJob = (status) => status === "running" || status === "stopping";
|
|
16108
|
+
/**
|
|
16109
|
+
* Ordering shared with kimi's tasks browser: active rows first, terminal
|
|
16110
|
+
* rows last. Jobs keep their registry order within each class (active by
|
|
16111
|
+
* startedAt, terminal newest-finish first); subagents keep the registry's
|
|
16112
|
+
* createdAt order (running before inactive).
|
|
16113
|
+
*/
|
|
16114
|
+
function buildTaskRows(jobs, agents) {
|
|
16115
|
+
const sortedAgents = [...agents.filter((agent) => agent.kind === "child" && (agent.mode === "continuable" || agent.mode === "one-shot" && agent.activity === "running")).map((agent) => ({
|
|
16116
|
+
kind: "subagent",
|
|
16117
|
+
value: `${AGENT_ROW_PREFIX}${agent.id}`,
|
|
16118
|
+
childId: agent.id,
|
|
16119
|
+
label: agent.label ?? agent.id,
|
|
16120
|
+
activity: agent.activity ?? "inactive",
|
|
16121
|
+
hasChildren: agent.hasChildren ?? false
|
|
16122
|
+
}))].sort((a, b) => {
|
|
16123
|
+
if (a.activity !== b.activity) return a.activity === "running" ? -1 : 1;
|
|
16124
|
+
return 0;
|
|
16125
|
+
});
|
|
16126
|
+
const sortedJobs = [...jobs].sort((a, b) => {
|
|
16127
|
+
const aTerminal = !isActiveJob(a.status);
|
|
16128
|
+
if (aTerminal !== !isActiveJob(b.status)) return aTerminal ? 1 : -1;
|
|
16129
|
+
if (!aTerminal) return a.startedAt - b.startedAt;
|
|
16130
|
+
return (b.finishedAt ?? b.startedAt) - (a.finishedAt ?? a.startedAt);
|
|
16131
|
+
}).map((job) => ({
|
|
16132
|
+
kind: "job",
|
|
16133
|
+
value: `${JOB_ROW_PREFIX}${job.id}`,
|
|
16134
|
+
jobId: job.id,
|
|
16135
|
+
jobKind: job.kind,
|
|
16136
|
+
label: job.label,
|
|
16137
|
+
status: job.status,
|
|
16138
|
+
detail: job.detail,
|
|
16139
|
+
startedAt: job.startedAt,
|
|
16140
|
+
finishedAt: job.finishedAt
|
|
16141
|
+
}));
|
|
16142
|
+
return [...sortedAgents, ...sortedJobs];
|
|
16143
|
+
}
|
|
16144
|
+
/** The one-line picker label for a row. */
|
|
16145
|
+
function taskRowLabel(row) {
|
|
16146
|
+
if (row.kind === "job") return `${row.jobKind} · ${row.label}`;
|
|
16147
|
+
return `subagent · ${row.label}`;
|
|
16148
|
+
}
|
|
16149
|
+
/** The picker group a row belongs to. */
|
|
16150
|
+
function rowGroup(row) {
|
|
16151
|
+
return row.kind === "job" ? JOB_GROUP : SUBAGENT_GROUP;
|
|
16152
|
+
}
|
|
16153
|
+
/** The picker description line for a row. */
|
|
16154
|
+
function describeTaskRow(row, now) {
|
|
16155
|
+
if (row.kind === "job") {
|
|
16156
|
+
const elapsed = Math.max(0, Math.floor((now - row.startedAt) / 1e3));
|
|
16157
|
+
return `${row.status}${row.detail === void 0 ? "" : ` — ${row.detail}`} · ${elapsed}s`;
|
|
16158
|
+
}
|
|
16159
|
+
return `${row.activity}${row.hasChildren ? " · has children" : ""}`;
|
|
16160
|
+
}
|
|
16161
|
+
//#endregion
|
|
15339
16162
|
//#region src/model-menu.ts
|
|
15340
16163
|
/**
|
|
15341
16164
|
* In-place submenu components for `/model`: the model list and the
|
|
@@ -15616,23 +16439,279 @@ async function loadSessionTitles(query, persistence, ids, signal) {
|
|
|
15616
16439
|
return titles;
|
|
15617
16440
|
}
|
|
15618
16441
|
//#endregion
|
|
15619
|
-
//#region src/
|
|
16442
|
+
//#region src/skill-catalog.ts
|
|
15620
16443
|
/**
|
|
15621
|
-
* The
|
|
15622
|
-
*
|
|
15623
|
-
*
|
|
15624
|
-
*
|
|
15625
|
-
*
|
|
15626
|
-
*
|
|
15627
|
-
*
|
|
15628
|
-
*
|
|
16444
|
+
* The standing-scope skill catalog adapter: the SINGLE narrow seam between
|
|
16445
|
+
* the TUI and the dsh skill services (the non-public-API isolation
|
|
16446
|
+
* contract, plan appendix B).
|
|
16447
|
+
*
|
|
16448
|
+
* Everything the TUI knows about dsh-skill and agent-presets lives here:
|
|
16449
|
+
* - structural service types (never upstream classes) with OPTIONAL
|
|
16450
|
+
* capability members — `snapshot` and `standingKeyFor` are detected at
|
|
16451
|
+
* call time, so an upstream change degrades instead of breaking;
|
|
16452
|
+
* - `readHumanSkillCatalog()`: snapshot-first collector (list() fallback),
|
|
16453
|
+
* official `isUserInvocable` filter, field validation, stable sort,
|
|
16454
|
+
* deep freeze, distinct abort propagation;
|
|
16455
|
+
* - `resolveColdSkillTarget()`: capability-gated cold target selection
|
|
16456
|
+
* (standing scope → rosterless global → degraded global with a notice);
|
|
16457
|
+
* - `resolveLiveSkillTarget()`: the agent's own registry + agent scope.
|
|
16458
|
+
*
|
|
16459
|
+
* Failure contract: ordinary provider/composition failures surface as
|
|
16460
|
+
* degraded targets or rethrown errors for the coordinator to classify;
|
|
16461
|
+
* aborts ALWAYS propagate as aborts. No failure may hang or crash the TUI —
|
|
16462
|
+
* the worst outcome is "the corresponding commands are missing".
|
|
16463
|
+
* @module @xmoon76/dsh-pi-tui/skill-catalog
|
|
15629
16464
|
*/
|
|
15630
|
-
/**
|
|
15631
|
-
*
|
|
15632
|
-
*
|
|
15633
|
-
*
|
|
16465
|
+
/**
|
|
16466
|
+
* Subscribe to the dsh-skill invalidation notification. The event carries
|
|
16467
|
+
* no scope or cwd — consumers refetch for their own cwd and scope — and the
|
|
16468
|
+
* subscription itself is optional: an unavailable `on` or a throwing
|
|
16469
|
+
* subscribe degrades to NO subscription (owner switches and `/reload` still
|
|
16470
|
+
* refresh). The CALLER owns the listener's failure totality (route the
|
|
16471
|
+
* refresh through `runOwned`).
|
|
16472
|
+
* @param ctx - the context surface exposing the event bus.
|
|
16473
|
+
* @param listener - the notification listener (must be synchronous and
|
|
16474
|
+
* never throw).
|
|
15634
16475
|
*/
|
|
15635
|
-
function
|
|
16476
|
+
function subscribeSkillsChange(ctx, listener) {
|
|
16477
|
+
ctx.on?.("skills/change", listener);
|
|
16478
|
+
}
|
|
16479
|
+
/**
|
|
16480
|
+
* Whether one summary passes the OFFICIAL user-invocation policy — the
|
|
16481
|
+
* adapter's exported guard, used by every human entry point (direct
|
|
16482
|
+
* wrappers, `/skill`, the picker, and final body loads). A malformed
|
|
16483
|
+
* `invocation` (missing, non-object, non-boolean flag) is treated as NOT
|
|
16484
|
+
* user-invocable: the policy defaults are never reinterpreted, and a
|
|
16485
|
+
* hostile entry can neither throw nor sneak into a human surface.
|
|
16486
|
+
*/
|
|
16487
|
+
function isUserInvocableSkill(skill) {
|
|
16488
|
+
const invocation = skill.invocation;
|
|
16489
|
+
if (typeof invocation !== "object" || invocation === null) return false;
|
|
16490
|
+
if (typeof invocation.userInvocable !== "boolean") return false;
|
|
16491
|
+
return isUserInvocable(skill);
|
|
16492
|
+
}
|
|
16493
|
+
/** Copy one summary, keeping ONLY the supported display fields. An entry
|
|
16494
|
+
* with a non-string name or description is rejected (never copied, never
|
|
16495
|
+
* trusted); a non-string `whenToUse` is dropped. */
|
|
16496
|
+
function toHumanSummary(skill) {
|
|
16497
|
+
if (typeof skill.name !== "string" || skill.name === "") return void 0;
|
|
16498
|
+
if (typeof skill.description !== "string") return void 0;
|
|
16499
|
+
return Object.freeze({
|
|
16500
|
+
name: skill.name,
|
|
16501
|
+
description: skill.description,
|
|
16502
|
+
...typeof skill.whenToUse === "string" && skill.whenToUse !== "" ? { whenToUse: skill.whenToUse } : {}
|
|
16503
|
+
});
|
|
16504
|
+
}
|
|
16505
|
+
/** Whether one summary passes the OFFICIAL user-invocation policy. A
|
|
16506
|
+
* malformed `invocation` (missing, non-object, non-boolean flag) is treated
|
|
16507
|
+
* as NOT user-invocable — the policy defaults are never reinterpreted, and
|
|
16508
|
+
* a hostile entry can neither throw nor sneak into the human catalog. */
|
|
16509
|
+
function userInvocable(skill) {
|
|
16510
|
+
return isUserInvocableSkill(skill);
|
|
16511
|
+
}
|
|
16512
|
+
/**
|
|
16513
|
+
* Read one human skill catalog from a registry: snapshot-first, official
|
|
16514
|
+
* policy filter, validated detached copies, stable name sort, deep freeze.
|
|
16515
|
+
* @param registry - the registry to read (the adapter's structural type).
|
|
16516
|
+
* @param options - cwd/scope/signal for the read.
|
|
16517
|
+
* @returns the detached catalog.
|
|
16518
|
+
* @throws on cancellation (ALWAYS, distinctly) and on non-abort read
|
|
16519
|
+
* failures (the coordinator classifies them: retain last-good + diag).
|
|
16520
|
+
*/
|
|
16521
|
+
async function readHumanSkillCatalog(registry, options) {
|
|
16522
|
+
options.signal?.throwIfAborted();
|
|
16523
|
+
const observed = typeof registry.snapshot === "function" ? await registry.snapshot(options) : {
|
|
16524
|
+
skills: await registry.list?.(options) ?? [],
|
|
16525
|
+
complete: true
|
|
16526
|
+
};
|
|
16527
|
+
options.signal?.throwIfAborted();
|
|
16528
|
+
const skills = observed.skills.filter(userInvocable).map(toHumanSummary).filter((summary) => summary !== void 0).sort((left, right) => left.name < right.name ? -1 : 1);
|
|
16529
|
+
return Object.freeze({
|
|
16530
|
+
skills: Object.freeze(skills),
|
|
16531
|
+
complete: observed.complete === true
|
|
16532
|
+
});
|
|
16533
|
+
}
|
|
16534
|
+
/**
|
|
16535
|
+
* Resolve the COLD (sessionless) skill target with capability detection:
|
|
16536
|
+
* - a preset roster with `standingKeyFor` → the standing scope of the
|
|
16537
|
+
* effective preset;
|
|
16538
|
+
* - no roster / no `standingKeyFor` / a standing resolution failure → the
|
|
16539
|
+
* global layer (with a one-shot degradation notice on failure);
|
|
16540
|
+
* - no skill registry at all → no target (nothing to read, no notice).
|
|
16541
|
+
* @param ctx - the context surface.
|
|
16542
|
+
* @param presetId - the effective preset id, or undefined for the default.
|
|
16543
|
+
* @param cwd - the workspace cwd for provider selection.
|
|
16544
|
+
*/
|
|
16545
|
+
async function resolveColdSkillTarget(ctx, presetId, cwd) {
|
|
16546
|
+
const registry = ctx.get("skills");
|
|
16547
|
+
if (registry === void 0) return {};
|
|
16548
|
+
const presets = ctx.get("agentPresets");
|
|
16549
|
+
if (presets === void 0 || typeof presets.standingKeyFor !== "function") return { target: {
|
|
16550
|
+
kind: "cold-global",
|
|
16551
|
+
registry,
|
|
16552
|
+
cwd,
|
|
16553
|
+
scope: void 0
|
|
16554
|
+
} };
|
|
16555
|
+
try {
|
|
16556
|
+
return { target: {
|
|
16557
|
+
kind: "cold-standing",
|
|
16558
|
+
registry,
|
|
16559
|
+
cwd,
|
|
16560
|
+
scope: await presets.standingKeyFor(presetId)
|
|
16561
|
+
} };
|
|
16562
|
+
} catch (error) {
|
|
16563
|
+
const message = safeErrorMessage(error);
|
|
16564
|
+
return {
|
|
16565
|
+
target: {
|
|
16566
|
+
kind: "cold-global",
|
|
16567
|
+
registry,
|
|
16568
|
+
cwd,
|
|
16569
|
+
scope: void 0
|
|
16570
|
+
},
|
|
16571
|
+
degraded: `skill catalog unavailable for preset "${presetId ?? "default"}": ${message}`
|
|
16572
|
+
};
|
|
16573
|
+
}
|
|
16574
|
+
}
|
|
16575
|
+
/**
|
|
16576
|
+
* Resolve the LIVE skill target: the registry the agent actually sees
|
|
16577
|
+
* (its preset's scoped instance when the preset mounts one, else the host
|
|
16578
|
+
* registry), with the AGENT OBJECT as the view scope.
|
|
16579
|
+
* @param ctx - the context surface.
|
|
16580
|
+
* @param agent - the live agent.
|
|
16581
|
+
* @param cwd - the session workspace cwd.
|
|
16582
|
+
* @returns the live target, or undefined when no registry is reachable.
|
|
16583
|
+
*/
|
|
16584
|
+
function resolveLiveSkillTarget(ctx, agent, cwd) {
|
|
16585
|
+
const registry = ctx.get("agentPresets")?.serviceFor?.(agent, "skills") ?? ctx.get("skills");
|
|
16586
|
+
if (registry === void 0) return void 0;
|
|
16587
|
+
return {
|
|
16588
|
+
kind: "live",
|
|
16589
|
+
registry,
|
|
16590
|
+
cwd,
|
|
16591
|
+
scope: agent
|
|
16592
|
+
};
|
|
16593
|
+
}
|
|
16594
|
+
//#endregion
|
|
16595
|
+
//#region src/surface-catalog.ts
|
|
16596
|
+
/**
|
|
16597
|
+
* The in-process global-layer command view. The public type requires an
|
|
16598
|
+
* `Agent`, but `commands.list(undefined)` resolves the global layer only
|
|
16599
|
+
* (ScopedLayers merges no overlays for an undefined key); the current TUI
|
|
16600
|
+
* already depends on this in-process behavior. The cast is isolated HERE so
|
|
16601
|
+
* the undefined key never reaches a remote RPC path, and the helper is the
|
|
16602
|
+
* single seam to replace if upstream ever ships a typed global-list API.
|
|
16603
|
+
* @param commands - the commands service.
|
|
16604
|
+
* @returns the global-layer descriptors (name-sorted by the registry).
|
|
16605
|
+
*/
|
|
16606
|
+
function listGlobalCommands(commands) {
|
|
16607
|
+
return commands.list(void 0);
|
|
16608
|
+
}
|
|
16609
|
+
/** Copy one descriptor into a fresh frozen summary (never borrowed). */
|
|
16610
|
+
function commandSummaryOf(descriptor) {
|
|
16611
|
+
return Object.freeze({
|
|
16612
|
+
name: descriptor.name,
|
|
16613
|
+
description: descriptor.description,
|
|
16614
|
+
...descriptor.input === void 0 ? {} : { input: Object.freeze({ hint: descriptor.input.hint }) }
|
|
16615
|
+
});
|
|
16616
|
+
}
|
|
16617
|
+
/** Whether two descriptors expose identical display fields (origin-blind:
|
|
16618
|
+
* an identical scoped entry needs no override because the visible result and
|
|
16619
|
+
* the real-agent execution are the same either way). */
|
|
16620
|
+
function sameCommand(left, right) {
|
|
16621
|
+
return left.name === right.name && left.description === right.description && left.input?.hint === right.input?.hint;
|
|
16622
|
+
}
|
|
16623
|
+
/**
|
|
16624
|
+
* Read one agent's effective surface catalog: global + scoped commands and
|
|
16625
|
+
* human-invocable skills, fully detached and frozen.
|
|
16626
|
+
*
|
|
16627
|
+
* Provider isolation (per the probe contract):
|
|
16628
|
+
* - lifecycle/refresh cancellation terminates the WHOLE read and propagates;
|
|
16629
|
+
* - an ordinary provider failure empties only that field and records a
|
|
16630
|
+
* detached issue; other providers continue;
|
|
16631
|
+
* - a missing service is that provider's successful empty result (no issue).
|
|
16632
|
+
*
|
|
16633
|
+
* This function only READS: it registers nothing, mutates nothing, and never
|
|
16634
|
+
* touches a runner's live agent. Probe and live agent share this collector
|
|
16635
|
+
* so the two surfaces cannot drift apart.
|
|
16636
|
+
* @param agent - the agent whose effective view to read.
|
|
16637
|
+
* @param signal - lifecycle/refresh cancellation.
|
|
16638
|
+
* @param ctx - the context surface resolving the services.
|
|
16639
|
+
* @returns a frozen, detached snapshot.
|
|
16640
|
+
*/
|
|
16641
|
+
async function readSurfaceCatalog(agent, signal, ctx) {
|
|
16642
|
+
signal.throwIfAborted();
|
|
16643
|
+
const issues = [];
|
|
16644
|
+
let commands = [];
|
|
16645
|
+
let scopedCommands = [];
|
|
16646
|
+
const commandsService = ctx.get("commands");
|
|
16647
|
+
if (commandsService !== void 0) try {
|
|
16648
|
+
const global = listGlobalCommands(commandsService);
|
|
16649
|
+
const globalBy = new Map(global.map((descriptor) => [descriptor.name, descriptor]));
|
|
16650
|
+
const effective = commandsService.list(agent);
|
|
16651
|
+
const scoped = [];
|
|
16652
|
+
for (const descriptor of effective) {
|
|
16653
|
+
const summary = commandSummaryOf(descriptor);
|
|
16654
|
+
const baseline = globalBy.get(descriptor.name);
|
|
16655
|
+
if (baseline === void 0 || !sameCommand(descriptor, baseline)) scoped.push(summary);
|
|
16656
|
+
}
|
|
16657
|
+
commands = sortCommands(effective.map(commandSummaryOf));
|
|
16658
|
+
scopedCommands = sortCommands(scoped);
|
|
16659
|
+
} catch (error) {
|
|
16660
|
+
issues.push({
|
|
16661
|
+
provider: "commands",
|
|
16662
|
+
message: safeErrorMessage(error)
|
|
16663
|
+
});
|
|
16664
|
+
}
|
|
16665
|
+
let skills = [];
|
|
16666
|
+
const skillTarget = resolveLiveSkillTarget(ctx, agent, agent.session.header.cwd ?? process.cwd());
|
|
16667
|
+
if (skillTarget !== void 0) try {
|
|
16668
|
+
signal.throwIfAborted();
|
|
16669
|
+
const catalog = await readHumanSkillCatalog(skillTarget.registry, {
|
|
16670
|
+
cwd: skillTarget.cwd,
|
|
16671
|
+
scope: skillTarget.scope,
|
|
16672
|
+
signal
|
|
16673
|
+
});
|
|
16674
|
+
skills = catalog.skills;
|
|
16675
|
+
if (catalog.complete !== true) issues.push({
|
|
16676
|
+
provider: "skills",
|
|
16677
|
+
message: "incomplete skill observation"
|
|
16678
|
+
});
|
|
16679
|
+
} catch (error) {
|
|
16680
|
+
if (signal.aborted) throw error;
|
|
16681
|
+
issues.push({
|
|
16682
|
+
provider: "skills",
|
|
16683
|
+
message: safeErrorMessage(error)
|
|
16684
|
+
});
|
|
16685
|
+
}
|
|
16686
|
+
return Object.freeze({
|
|
16687
|
+
commands,
|
|
16688
|
+
scopedCommands,
|
|
16689
|
+
skills,
|
|
16690
|
+
issues: Object.freeze(issues.map((issue) => Object.freeze({ ...issue })))
|
|
16691
|
+
});
|
|
16692
|
+
}
|
|
16693
|
+
/** Name-stable sort for command summaries (copies, never mutates input). */
|
|
16694
|
+
function sortCommands(commands) {
|
|
16695
|
+
return Object.freeze([...commands].sort((left, right) => left.name < right.name ? -1 : 1));
|
|
16696
|
+
}
|
|
16697
|
+
//#endregion
|
|
16698
|
+
//#region src/commands.ts
|
|
16699
|
+
/**
|
|
16700
|
+
* The TUI-owned slash commands (/exit /settings /sessions /skill /model
|
|
16701
|
+
* /new /tasks /preset /subagents /search /title /rename /copy /export
|
|
16702
|
+
* /fork /status /login /logout /help), extracted from the runner's
|
|
16703
|
+
* monolithic apply() so the registration surface is testable and the runner
|
|
16704
|
+
* closure shrinks. Every command reads the live runner state through the
|
|
16705
|
+
* {@link TuiCommandRunner} interface, whose accessors re-read the current
|
|
16706
|
+
* agent/settings on every access (sessions can swap the live agent).
|
|
16707
|
+
* @module @xmoon76/dsh-pi-tui/commands
|
|
16708
|
+
*/
|
|
16709
|
+
/** A balanced completed-turn prefix for forking: the log up to (and including)
|
|
16710
|
+
* the last `turn/end`. Undefined when no turn has completed yet.
|
|
16711
|
+
* @param events - the session log.
|
|
16712
|
+
* @returns the fork seed events, or undefined.
|
|
16713
|
+
*/
|
|
16714
|
+
function forkSeed(events) {
|
|
15636
16715
|
for (let index = events.length - 1; index >= 0; index -= 1) if (events[index]?.type === "turn/end") return events.slice(0, index + 1);
|
|
15637
16716
|
}
|
|
15638
16717
|
/** Shorten a session id for read-only display rows, capped at 28 characters. */
|
|
@@ -15647,16 +16726,62 @@ function metaOf(cwd, presetId) {
|
|
|
15647
16726
|
};
|
|
15648
16727
|
}
|
|
15649
16728
|
/**
|
|
16729
|
+
* Display copy for the four shipped agent presets, fixed in English — the
|
|
16730
|
+
* web surface's `BUILT_IN_PRESET_KEYS` mapping (`dsh-client-ui-agent-preset`),
|
|
16731
|
+
* TUI-side. The EFFECTIVE roster root is the dsh install's own
|
|
16732
|
+
* `config/agent-presets`: the dsh CLI's profile composition replaces this
|
|
16733
|
+
* bundle's shipped root with that one at boot (the `composeProfile`
|
|
16734
|
+
* agent-presets overlay), and its preset.yml language is not ours to
|
|
16735
|
+
* control. Mapping the known ids keeps the picker English regardless of
|
|
16736
|
+
* what the files say; everything else renders file metadata.
|
|
16737
|
+
*/
|
|
16738
|
+
const BUILT_IN_PRESET_COPY = {
|
|
16739
|
+
standard: {
|
|
16740
|
+
name: "Standard mode",
|
|
16741
|
+
description: "Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows."
|
|
16742
|
+
},
|
|
16743
|
+
code: {
|
|
16744
|
+
name: "Code mode",
|
|
16745
|
+
description: "All Standard mode capabilities, with tools exposed through the Code Mode SDK so the model can combine multi-step operations in one TypeScript program."
|
|
16746
|
+
},
|
|
16747
|
+
minimal: {
|
|
16748
|
+
name: "Minimal mode",
|
|
16749
|
+
description: "Two-tool coding agent with persistent bash and str_replace_editor."
|
|
16750
|
+
},
|
|
16751
|
+
cordis: {
|
|
16752
|
+
name: "Creator mode",
|
|
16753
|
+
description: "Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance."
|
|
16754
|
+
}
|
|
16755
|
+
};
|
|
16756
|
+
/** Resolve one roster row's display copy: fixed English for a shipped
|
|
16757
|
+
* (system-trust) preset id, otherwise the preset's file metadata. */
|
|
16758
|
+
function presetDisplayText(preset) {
|
|
16759
|
+
const builtIn = preset.trust === "system" ? BUILT_IN_PRESET_COPY[preset.id] : void 0;
|
|
16760
|
+
if (builtIn !== void 0) return {
|
|
16761
|
+
name: builtIn.name,
|
|
16762
|
+
description: builtIn.description
|
|
16763
|
+
};
|
|
16764
|
+
return {
|
|
16765
|
+
name: preset.name ?? preset.id,
|
|
16766
|
+
...preset.description === void 0 ? {} : { description: preset.description }
|
|
16767
|
+
};
|
|
16768
|
+
}
|
|
16769
|
+
/**
|
|
15650
16770
|
* Register the TUI-owned slash commands on the commands service. The
|
|
15651
16771
|
* completion list is refreshed after every registration so TUI-owned
|
|
15652
16772
|
* commands appear in the editor's tab list. Registration is sessionless:
|
|
15653
16773
|
* the commands service's global layer needs no agent, so the whole surface
|
|
15654
16774
|
* is available before the first session exists (deferred start).
|
|
16775
|
+
*
|
|
16776
|
+
* When an `initial` catalog was prefetched (resume snapshot or cold
|
|
16777
|
+
* standing-scope skills), it installs SYNCHRONOUSLY at the end of
|
|
16778
|
+
* registration — direct skill wrappers plus the completion merge — so the
|
|
16779
|
+
* first input is served by the complete catalog with zero async I/O in
|
|
16780
|
+
* between.
|
|
15655
16781
|
* @param runner - the live runner surface.
|
|
15656
|
-
* @
|
|
15657
|
-
* the first session exists, since the skill catalog scope is the agent).
|
|
16782
|
+
* @param initial - optional prefetched catalogs installed synchronously.
|
|
15658
16783
|
*/
|
|
15659
|
-
function registerTuiCommands(runner) {
|
|
16784
|
+
function registerTuiCommands(runner, initial) {
|
|
15660
16785
|
const { ctx, app } = runner;
|
|
15661
16786
|
const cwd = runner.cwd;
|
|
15662
16787
|
const signal = runner.signal;
|
|
@@ -15700,12 +16825,60 @@ function registerTuiCommands(runner) {
|
|
|
15700
16825
|
if (agent === void 0) throw new Error("session could not be created");
|
|
15701
16826
|
return agent;
|
|
15702
16827
|
};
|
|
15703
|
-
|
|
15704
|
-
|
|
16828
|
+
/** The advertised names of the currently installed completion list. */
|
|
16829
|
+
let claims = /* @__PURE__ */ new Set();
|
|
16830
|
+
/**
|
|
16831
|
+
* Install one completion list (sorted, claims refreshed). The single
|
|
16832
|
+
* synchronous seam every catalog commit funnels through.
|
|
16833
|
+
*/
|
|
16834
|
+
const installCompletions = (entries) => {
|
|
16835
|
+
const sorted = [...entries].sort((left, right) => left.name < right.name ? -1 : 1);
|
|
16836
|
+
app.setCommandCompletions(sorted.map((command) => ({
|
|
15705
16837
|
name: command.name,
|
|
15706
16838
|
description: command.description,
|
|
15707
16839
|
argumentHint: command.input?.hint
|
|
15708
16840
|
})), runner.sessionCwd(), fdPath);
|
|
16841
|
+
claims = new Set(sorted.map((command) => command.name));
|
|
16842
|
+
};
|
|
16843
|
+
/** The saved probed scoped overrides (see installSurfaceSnapshot). */
|
|
16844
|
+
let savedScopedCommands = [];
|
|
16845
|
+
/**
|
|
16846
|
+
* The sessionless completion view: the CURRENT global layer (fresh read —
|
|
16847
|
+
* TUI built-ins, global plugins and installed skill wrappers all flow in)
|
|
16848
|
+
* overlaid with the saved scoped overrides from the latest snapshot.
|
|
16849
|
+
*/
|
|
16850
|
+
const mergeGlobalAndSavedScoped = () => {
|
|
16851
|
+
const byName = /* @__PURE__ */ new Map();
|
|
16852
|
+
for (const descriptor of listGlobalCommands(commands)) byName.set(descriptor.name, commandSummaryOf(descriptor));
|
|
16853
|
+
for (const scoped of savedScopedCommands) byName.set(scoped.name, scoped);
|
|
16854
|
+
return [...byName.values()];
|
|
16855
|
+
};
|
|
16856
|
+
/**
|
|
16857
|
+
* Refresh completions from the registry: the LIVE agent's effective view
|
|
16858
|
+
* when one exists (global + its scoped shadows), else the global layer
|
|
16859
|
+
* overlaid with the saved scoped overrides (sessionless merge). The agent
|
|
16860
|
+
* may be undefined before the first session exists: in-process
|
|
16861
|
+
* `commands.list(undefined)` safely returns the global layer only (the
|
|
16862
|
+
* remote RPC path's lookup guard does not apply in-process).
|
|
16863
|
+
*/
|
|
16864
|
+
const refreshCompletions = () => {
|
|
16865
|
+
const liveAgent = runner.liveAgent;
|
|
16866
|
+
installCompletions(liveAgent === void 0 ? mergeGlobalAndSavedScoped() : commands.list(liveAgent).map(commandSummaryOf));
|
|
16867
|
+
};
|
|
16868
|
+
let commandCommitDepth = 0;
|
|
16869
|
+
let commandCommitDirty = false;
|
|
16870
|
+
/** Run one bulk command commit; registry changes inside are coalesced. */
|
|
16871
|
+
const withCommandCommit = (phase) => {
|
|
16872
|
+
commandCommitDepth += 1;
|
|
16873
|
+
try {
|
|
16874
|
+
phase();
|
|
16875
|
+
} finally {
|
|
16876
|
+
commandCommitDepth -= 1;
|
|
16877
|
+
if (commandCommitDepth === 0 && commandCommitDirty) {
|
|
16878
|
+
commandCommitDirty = false;
|
|
16879
|
+
refreshCompletions();
|
|
16880
|
+
}
|
|
16881
|
+
}
|
|
15709
16882
|
};
|
|
15710
16883
|
refreshCompletions();
|
|
15711
16884
|
const exitHandler = () => {
|
|
@@ -15730,6 +16903,7 @@ function registerTuiCommands(runner) {
|
|
|
15730
16903
|
const tuiSettings = runner.tuiSettings;
|
|
15731
16904
|
const theme = tuiSettings?.get().theme ?? "auto";
|
|
15732
16905
|
const themeValue = theme.startsWith("custom:") ? theme.slice(7) : theme;
|
|
16906
|
+
let lastThemeChoice = themeValue;
|
|
15733
16907
|
const settings = ctx.get("settings");
|
|
15734
16908
|
const permissionNames = [...ctx.get("permissionPresets")?.names ?? []];
|
|
15735
16909
|
let defaultPermission;
|
|
@@ -15784,6 +16958,13 @@ function registerTuiCommands(runner) {
|
|
|
15784
16958
|
currentValue: app.getFooterPreset(),
|
|
15785
16959
|
values: ["full", "compact"]
|
|
15786
16960
|
},
|
|
16961
|
+
{
|
|
16962
|
+
id: "busy-enter",
|
|
16963
|
+
label: "Enter while busy",
|
|
16964
|
+
description: "Steer injects the draft into the running turn; Ctrl+Enter uses the other behavior",
|
|
16965
|
+
currentValue: tuiSettings?.get().busyEnter ?? "queue",
|
|
16966
|
+
values: ["queue", "steer"]
|
|
16967
|
+
},
|
|
15787
16968
|
{
|
|
15788
16969
|
id: "fullscreen",
|
|
15789
16970
|
label: "Fullscreen",
|
|
@@ -15837,12 +17018,19 @@ function registerTuiCommands(runner) {
|
|
|
15837
17018
|
}]), { notify: true });
|
|
15838
17019
|
} else if (id === "theme") {
|
|
15839
17020
|
if (value === "auto" || value === "dark" || value === "light" || customThemeNames().includes(value)) {
|
|
15840
|
-
|
|
15841
|
-
|
|
15842
|
-
|
|
17021
|
+
lastThemeChoice = value;
|
|
17022
|
+
if (value === "auto") {
|
|
17023
|
+
detach("theme autodetect", () => app.autoDetectTheme({ shouldApply: () => lastThemeChoice === "auto" }));
|
|
17024
|
+
app.trackTerminalTheme(true);
|
|
17025
|
+
} else if (value === "dark" || value === "light") {
|
|
17026
|
+
app.applyTheme(value);
|
|
17027
|
+
app.trackTerminalTheme(false);
|
|
17028
|
+
} else {
|
|
15843
17029
|
const palette = loadCustomTheme(value);
|
|
15844
|
-
if (palette !== void 0)
|
|
15845
|
-
|
|
17030
|
+
if (palette !== void 0) {
|
|
17031
|
+
app.applyPalette(palette);
|
|
17032
|
+
app.trackTerminalTheme(false);
|
|
17033
|
+
} else {
|
|
15846
17034
|
app.notify(`theme ${value} not found`, "error");
|
|
15847
17035
|
return;
|
|
15848
17036
|
}
|
|
@@ -15865,6 +17053,14 @@ function registerTuiCommands(runner) {
|
|
|
15865
17053
|
footer: value
|
|
15866
17054
|
}), { notify: true });
|
|
15867
17055
|
}
|
|
17056
|
+
} else if (id === "busy-enter") {
|
|
17057
|
+
if (value === "queue" || value === "steer") {
|
|
17058
|
+
const settings = tuiSettings;
|
|
17059
|
+
if (settings !== void 0) detach("settings busy enter write", () => settings.replace({
|
|
17060
|
+
...settings.get(),
|
|
17061
|
+
busyEnter: value
|
|
17062
|
+
}), { notify: true });
|
|
17063
|
+
}
|
|
15868
17064
|
} else if (id === "fullscreen") {
|
|
15869
17065
|
if (value === "off" || value === "on") app.setFullscreen(value === "on");
|
|
15870
17066
|
}
|
|
@@ -15945,29 +17141,44 @@ function registerTuiCommands(runner) {
|
|
|
15945
17141
|
return openSessionPicker(invocation, "resume");
|
|
15946
17142
|
}
|
|
15947
17143
|
});
|
|
15948
|
-
const skillService = (agent) => {
|
|
15949
|
-
return ctx.get("agentPresets")?.serviceFor(agent, "skills") ?? ctx.get("skills");
|
|
15950
|
-
};
|
|
15951
17144
|
/** The workspace the live session runs in; fallback to the TUI's cwd. */
|
|
15952
17145
|
const sessionCwd = (agent) => agent.session.header.cwd ?? cwd;
|
|
17146
|
+
const skillTarget = (agent) => resolveLiveSkillTarget(ctx, agent, sessionCwd(agent));
|
|
17147
|
+
/**
|
|
17148
|
+
* The execution boundary for loading one skill into the live session;
|
|
17149
|
+
* shared by /skill and the per-skill slash commands. The skill is fetched
|
|
17150
|
+
* from the CURRENT agent's registry and its invocation policy is RE-CHECKED
|
|
17151
|
+
* here — a summary that passed the cold/live filter is never execution
|
|
17152
|
+
* authorization. A model-only skill is refused with an explicit error and
|
|
17153
|
+
* never injected.
|
|
17154
|
+
*/
|
|
15953
17155
|
const loadSkill = async (agent, name) => {
|
|
15954
|
-
const
|
|
15955
|
-
if (
|
|
17156
|
+
const target = skillTarget(agent);
|
|
17157
|
+
if (target === void 0) return {
|
|
15956
17158
|
kind: "error",
|
|
15957
17159
|
text: "skill service unavailable"
|
|
15958
17160
|
};
|
|
15959
|
-
const skill = await
|
|
15960
|
-
cwd:
|
|
15961
|
-
scope:
|
|
17161
|
+
const skill = await target.registry.get?.(name, {
|
|
17162
|
+
cwd: target.cwd,
|
|
17163
|
+
scope: target.scope
|
|
15962
17164
|
});
|
|
15963
17165
|
if (skill === void 0) return {
|
|
15964
17166
|
kind: "error",
|
|
15965
17167
|
text: "unknown skill \"" + name + "\""
|
|
15966
17168
|
};
|
|
17169
|
+
if (!isUserInvocableSkill(skill)) return {
|
|
17170
|
+
kind: "error",
|
|
17171
|
+
text: `skill "${name}" is not invocable by the user`
|
|
17172
|
+
};
|
|
17173
|
+
if (typeof skill.name !== "string" || skill.name === "" || typeof skill.description !== "string") return {
|
|
17174
|
+
kind: "error",
|
|
17175
|
+
text: `skill "${name}" returned a malformed definition`
|
|
17176
|
+
};
|
|
17177
|
+
const body = typeof skill.content === "string" && skill.content !== "" ? skill.content : skill.description;
|
|
15967
17178
|
agent.inject(createUserMessage({
|
|
15968
17179
|
content: [{
|
|
15969
17180
|
type: "text",
|
|
15970
|
-
text: "Skill loaded by the user: **" + skill.name + "**\n\n" +
|
|
17181
|
+
text: "Skill loaded by the user: **" + skill.name + "**\n\n" + body
|
|
15971
17182
|
}],
|
|
15972
17183
|
source: {
|
|
15973
17184
|
kind: "plugin",
|
|
@@ -15980,21 +17191,30 @@ function registerTuiCommands(runner) {
|
|
|
15980
17191
|
};
|
|
15981
17192
|
};
|
|
15982
17193
|
const skillDisposers = /* @__PURE__ */ new Map();
|
|
15983
|
-
|
|
15984
|
-
|
|
17194
|
+
/**
|
|
17195
|
+
* Synchronously replace every direct skill wrapper from a snapshot. Pure
|
|
17196
|
+
* install: no catalog fetch, no await — the callers (the initial snapshot
|
|
17197
|
+
* commit, the live refresh, the coordinator) provide the data. The target
|
|
17198
|
+
* set is computed FIRST (current global/effective view minus the wrappers
|
|
17199
|
+
* this surface owns, plus the snapshot's scoped overrides), then the old
|
|
17200
|
+
* wrappers are disposed and the new ones registered in one synchronous
|
|
17201
|
+
* commit.
|
|
17202
|
+
* @param skills - the human-invocable summaries to install.
|
|
17203
|
+
* @param scopedNames - the snapshot's scoped command names: a preset-scoped
|
|
17204
|
+
* command must block a same-name wrapper even though the global view
|
|
17205
|
+
* cannot see it yet (sessionless install).
|
|
17206
|
+
* @returns the number of wrappers installed.
|
|
17207
|
+
*/
|
|
17208
|
+
const replaceSkillCommands = (skills, scopedNames) => {
|
|
17209
|
+
const owned = new Set(skillDisposers.keys());
|
|
17210
|
+
const taken = /* @__PURE__ */ new Set();
|
|
17211
|
+
const view = commands.list(runner.liveAgent);
|
|
17212
|
+
for (const command of view) if (!owned.has(command.name)) taken.add(command.name);
|
|
17213
|
+
for (const name of scopedNames) taken.add(name);
|
|
15985
17214
|
for (const dispose of skillDisposers.values()) dispose();
|
|
15986
17215
|
skillDisposers.clear();
|
|
15987
|
-
|
|
15988
|
-
|
|
15989
|
-
const skills = skillService(liveAgent);
|
|
15990
|
-
if (skills === void 0) return 0;
|
|
15991
|
-
const taken = new Set(commands.list(liveAgent).map((command) => command.name));
|
|
15992
|
-
const catalog = await skills.list({
|
|
15993
|
-
cwd: sessionCwd(liveAgent),
|
|
15994
|
-
scope: liveAgent
|
|
15995
|
-
});
|
|
15996
|
-
if (generation !== runner.sessionGeneration) return 0;
|
|
15997
|
-
for (const skill of catalog) {
|
|
17216
|
+
let count = 0;
|
|
17217
|
+
for (const skill of skills) {
|
|
15998
17218
|
if (taken.has(skill.name)) continue;
|
|
15999
17219
|
try {
|
|
16000
17220
|
const dispose = commands.register({
|
|
@@ -16006,34 +17226,83 @@ function registerTuiCommands(runner) {
|
|
|
16006
17226
|
}
|
|
16007
17227
|
});
|
|
16008
17228
|
skillDisposers.set(skill.name, dispose);
|
|
17229
|
+
count += 1;
|
|
16009
17230
|
} catch {}
|
|
16010
17231
|
}
|
|
16011
|
-
|
|
16012
|
-
return catalog.length;
|
|
17232
|
+
return count;
|
|
16013
17233
|
};
|
|
16014
|
-
|
|
17234
|
+
/**
|
|
17235
|
+
* Install one whole snapshot on the surface in ONE synchronous commit:
|
|
17236
|
+
* replace the direct skill wrappers, save the scoped overrides, and merge
|
|
17237
|
+
* the completions (fresh global view + saved overrides). No await between
|
|
17238
|
+
* the pieces, so a first input can never observe a half-installed catalog.
|
|
17239
|
+
* A FAILED skills provider keeps the current wrappers (transitions or the
|
|
17240
|
+
* previous catalog): they re-validate against the current agent at
|
|
17241
|
+
* execution time, so a submitted skill name can never fall through to a
|
|
17242
|
+
* plain model message while the catalog is unavailable.
|
|
17243
|
+
*/
|
|
17244
|
+
const installSurfaceSnapshot = (snapshot) => {
|
|
17245
|
+
const scopedNames = new Set(snapshot.scopedCommands.map((command) => command.name));
|
|
17246
|
+
const skillsFailed = snapshot.issues.some((issue) => issue.provider === "skills");
|
|
17247
|
+
withCommandCommit(() => {
|
|
17248
|
+
if (!skillsFailed) replaceSkillCommands(snapshot.skills, scopedNames);
|
|
17249
|
+
savedScopedCommands = snapshot.scopedCommands;
|
|
17250
|
+
installCompletions(mergeGlobalAndSavedScoped());
|
|
17251
|
+
});
|
|
17252
|
+
};
|
|
17253
|
+
/**
|
|
17254
|
+
* The revalidating transition (target/owner change): scoped previews clear
|
|
17255
|
+
* so new inputs complete against the current global view only, and every
|
|
17256
|
+
* old skill wrapper becomes a revalidating transition command whose
|
|
17257
|
+
* handler re-fetches from the CURRENT live agent and re-checks the policy
|
|
17258
|
+
* at execution time. The names survive so an already-submitted skill
|
|
17259
|
+
* command can never become a plain model message mid-switch.
|
|
17260
|
+
*/
|
|
17261
|
+
const enterCatalogTransition = () => {
|
|
17262
|
+
const names = [...skillDisposers.keys()];
|
|
17263
|
+
withCommandCommit(() => {
|
|
17264
|
+
for (const dispose of skillDisposers.values()) dispose();
|
|
17265
|
+
skillDisposers.clear();
|
|
17266
|
+
savedScopedCommands = [];
|
|
17267
|
+
for (const name of names) try {
|
|
17268
|
+
const dispose = commands.register({
|
|
17269
|
+
name,
|
|
17270
|
+
description: `[skill: revalidating] ${name}`,
|
|
17271
|
+
handler: async () => {
|
|
17272
|
+
const agent = await requireAgent();
|
|
17273
|
+
return loadSkill(agent, name);
|
|
17274
|
+
}
|
|
17275
|
+
});
|
|
17276
|
+
skillDisposers.set(name, dispose);
|
|
17277
|
+
} catch {}
|
|
17278
|
+
installCompletions(mergeGlobalAndSavedScoped());
|
|
17279
|
+
});
|
|
17280
|
+
};
|
|
17281
|
+
/** Whether one command name is advertised by the CURRENT completion list
|
|
17282
|
+
* (the claim captured at submit time, before any session creation). */
|
|
17283
|
+
const wasAdvertised = (name) => claims.has(name);
|
|
16015
17284
|
commands.register({
|
|
16016
17285
|
name: "skill",
|
|
16017
17286
|
description: "Load a skill into the session context",
|
|
16018
17287
|
input: { hint: "<name>" },
|
|
16019
17288
|
handler: async (invocation) => {
|
|
16020
17289
|
const liveAgent = await requireAgent();
|
|
16021
|
-
const
|
|
16022
|
-
if (
|
|
17290
|
+
const target = skillTarget(liveAgent);
|
|
17291
|
+
if (target === void 0) return {
|
|
16023
17292
|
kind: "error",
|
|
16024
17293
|
text: "skill service unavailable"
|
|
16025
17294
|
};
|
|
16026
17295
|
const name = invocation.rawInput.trim();
|
|
16027
17296
|
if (name !== "") return loadSkill(liveAgent, name);
|
|
16028
|
-
const catalog = await
|
|
16029
|
-
cwd:
|
|
16030
|
-
scope:
|
|
17297
|
+
const catalog = await readHumanSkillCatalog(target.registry, {
|
|
17298
|
+
cwd: target.cwd,
|
|
17299
|
+
scope: target.scope
|
|
16031
17300
|
});
|
|
16032
|
-
if (catalog.length === 0) return {
|
|
17301
|
+
if (catalog.skills.length === 0) return {
|
|
16033
17302
|
kind: "error",
|
|
16034
17303
|
text: "no skills available"
|
|
16035
17304
|
};
|
|
16036
|
-
app.openSettings(catalog.map((skill) => ({
|
|
17305
|
+
app.openSettings(catalog.skills.map((skill) => ({
|
|
16037
17306
|
id: skill.name,
|
|
16038
17307
|
label: skill.name,
|
|
16039
17308
|
description: skill.description,
|
|
@@ -16049,25 +17318,57 @@ function registerTuiCommands(runner) {
|
|
|
16049
17318
|
});
|
|
16050
17319
|
commands.register({
|
|
16051
17320
|
name: "reload",
|
|
16052
|
-
description: "Reload TUI settings and refresh skill
|
|
17321
|
+
description: "Reload TUI settings and refresh the live command/skill catalog",
|
|
16053
17322
|
handler: async () => {
|
|
16054
|
-
let
|
|
16055
|
-
|
|
16056
|
-
|
|
16057
|
-
|
|
17323
|
+
let catalogText = "";
|
|
17324
|
+
const liveAgent = runner.liveAgent;
|
|
17325
|
+
if (liveAgent !== void 0) {
|
|
17326
|
+
const outcome = await runner.refreshCatalog({
|
|
17327
|
+
source: "reload",
|
|
17328
|
+
target: {
|
|
17329
|
+
kind: "agent",
|
|
17330
|
+
key: runner.sessionGeneration
|
|
17331
|
+
},
|
|
17332
|
+
agent: liveAgent
|
|
17333
|
+
});
|
|
17334
|
+
if (outcome.kind === "applied") {
|
|
17335
|
+
catalogText = `${outcome.snapshot.commands.length} commands \u00b7 ${outcome.snapshot.skills.length} skills`;
|
|
17336
|
+
if (outcome.snapshot.issues.length > 0) catalogText += ` \u00b7 partial: ${outcome.snapshot.issues.map((issue) => issue.provider).join(", ")} unavailable`;
|
|
17337
|
+
} else if (outcome.kind === "failed") catalogText = `catalog refresh failed: ${outcome.error}`;
|
|
17338
|
+
else catalogText = "catalog refresh superseded";
|
|
17339
|
+
} else {
|
|
17340
|
+
const outcome = await runner.refreshCatalog({
|
|
17341
|
+
source: "reload",
|
|
17342
|
+
target: {
|
|
17343
|
+
kind: "preset",
|
|
17344
|
+
presetId: runner.effectivePresetId
|
|
17345
|
+
}
|
|
17346
|
+
});
|
|
17347
|
+
if (outcome.kind === "applied") {
|
|
17348
|
+
catalogText = `${outcome.snapshot.skills.length} human skills`;
|
|
17349
|
+
if (outcome.notice !== void 0) catalogText += ` \u00b7 ${outcome.notice}`;
|
|
17350
|
+
} else if (outcome.kind === "failed") catalogText = `catalog refresh failed: ${outcome.error}`;
|
|
17351
|
+
else catalogText = "catalog refresh superseded";
|
|
17352
|
+
}
|
|
16058
17353
|
const settings = runner.tuiSettings;
|
|
16059
17354
|
if (settings !== void 0) {
|
|
16060
17355
|
const doc = settings.get();
|
|
16061
|
-
|
|
16062
|
-
|
|
16063
|
-
|
|
16064
|
-
|
|
17356
|
+
const reloadTheme = doc.theme;
|
|
17357
|
+
if (reloadTheme === "auto") {
|
|
17358
|
+
detach("theme autodetect", () => app.autoDetectTheme({ shouldApply: () => reloadTheme === "auto" }));
|
|
17359
|
+
app.trackTerminalTheme(true);
|
|
17360
|
+
} else if (reloadTheme === "dark" || reloadTheme === "light") {
|
|
17361
|
+
app.applyTheme(reloadTheme);
|
|
17362
|
+
app.trackTerminalTheme(false);
|
|
17363
|
+
} else if (reloadTheme.startsWith("custom:")) {
|
|
17364
|
+
const palette = loadCustomTheme(reloadTheme.slice(7));
|
|
16065
17365
|
if (palette !== void 0) app.applyPalette(palette);
|
|
17366
|
+
app.trackTerminalTheme(false);
|
|
16066
17367
|
}
|
|
16067
17368
|
app.setFooterPreset(doc.footer === "compact" ? "compact" : "full");
|
|
16068
17369
|
app.setFullscreen(doc.fullscreen === "on");
|
|
16069
17370
|
}
|
|
16070
|
-
app.notify(`reloaded — ${
|
|
17371
|
+
app.notify(`reloaded — ${catalogText} \u00b7 settings reapplied`, "info");
|
|
16071
17372
|
return { kind: "success" };
|
|
16072
17373
|
}
|
|
16073
17374
|
});
|
|
@@ -16128,7 +17429,7 @@ function registerTuiCommands(runner) {
|
|
|
16128
17429
|
description: "Start a fresh session in this workspace",
|
|
16129
17430
|
handler: async () => {
|
|
16130
17431
|
const liveAgent = runner.liveAgent;
|
|
16131
|
-
const composition = await runner.compose();
|
|
17432
|
+
const composition = await runner.compose(runner.pendingPreset);
|
|
16132
17433
|
const presetId = composition.agentPreset;
|
|
16133
17434
|
const next = await runner.agents.create({
|
|
16134
17435
|
sessionId: SessionId(`session-${randomUUID()}`),
|
|
@@ -16293,13 +17594,13 @@ function registerTuiCommands(runner) {
|
|
|
16293
17594
|
description: "Show or switch the session agent preset",
|
|
16294
17595
|
input: { hint: "[status|<id>|default [<id>]]" },
|
|
16295
17596
|
handler: async (invocation) => {
|
|
16296
|
-
const liveAgent = await requireAgent();
|
|
16297
17597
|
const presets = ctx.get("agentPresets");
|
|
16298
17598
|
if (presets === void 0) return {
|
|
16299
17599
|
kind: "error",
|
|
16300
17600
|
text: "agent presets unavailable in this deployment"
|
|
16301
17601
|
};
|
|
16302
|
-
const
|
|
17602
|
+
const liveAgent = runner.liveAgent;
|
|
17603
|
+
const current = liveAgent === void 0 ? void 0 : presets.composedPreset(liveAgent.ctx) ?? resolveSessionPreset(liveAgent.session);
|
|
16303
17604
|
const matched = invocation.rawInput.trim().match(/^(\S+)(?:\s+(.*))?$/);
|
|
16304
17605
|
const verb = matched?.[1] ?? "";
|
|
16305
17606
|
const rest = matched?.[2]?.trim() ?? "";
|
|
@@ -16318,24 +17619,95 @@ function registerTuiCommands(runner) {
|
|
|
16318
17619
|
kind: "success",
|
|
16319
17620
|
text: `default preset: ${settings.get(ns)?.default ?? presets.defaultId}`
|
|
16320
17621
|
};
|
|
16321
|
-
|
|
16322
|
-
|
|
16323
|
-
|
|
16324
|
-
|
|
16325
|
-
|
|
17622
|
+
try {
|
|
17623
|
+
await settings.mutate(ns, [{
|
|
17624
|
+
op: "set",
|
|
17625
|
+
path: ["default"],
|
|
17626
|
+
value: rest
|
|
17627
|
+
}]);
|
|
17628
|
+
} catch (error) {
|
|
17629
|
+
return {
|
|
17630
|
+
kind: "error",
|
|
17631
|
+
text: safeErrorMessage(error)
|
|
17632
|
+
};
|
|
17633
|
+
}
|
|
17634
|
+
if (runner.effectivePresetId === void 0) {
|
|
17635
|
+
const outcome = await runner.refreshCatalog({
|
|
17636
|
+
source: "preset",
|
|
17637
|
+
target: {
|
|
17638
|
+
kind: "preset",
|
|
17639
|
+
presetId: rest
|
|
17640
|
+
}
|
|
17641
|
+
});
|
|
17642
|
+
if (outcome.kind === "applied" && outcome.notice !== void 0) app.notify(outcome.notice, "error");
|
|
17643
|
+
}
|
|
16326
17644
|
return {
|
|
16327
17645
|
kind: "success",
|
|
16328
17646
|
text: `default preset set: ${rest}`
|
|
16329
17647
|
};
|
|
16330
17648
|
}
|
|
16331
|
-
|
|
16332
|
-
|
|
17649
|
+
const applyPresetSelection = async (id) => {
|
|
17650
|
+
if (liveAgent === void 0) {
|
|
17651
|
+
const resolved = await presets.resolve(id);
|
|
17652
|
+
runner.pendingPreset = resolved.id;
|
|
17653
|
+
const outcome = await runner.refreshCatalog({
|
|
17654
|
+
source: "preset",
|
|
17655
|
+
target: {
|
|
17656
|
+
kind: "preset",
|
|
17657
|
+
presetId: resolved.id
|
|
17658
|
+
}
|
|
17659
|
+
});
|
|
17660
|
+
if (outcome.kind === "applied" && outcome.notice !== void 0) app.notify(outcome.notice, "error");
|
|
17661
|
+
return {
|
|
17662
|
+
kind: "pending",
|
|
17663
|
+
preset: resolved.id
|
|
17664
|
+
};
|
|
17665
|
+
}
|
|
17666
|
+
const outcome = await runner.recomposeBlank(id);
|
|
16333
17667
|
if (outcome.kind === "locked") return {
|
|
16334
|
-
kind: "
|
|
16335
|
-
|
|
17668
|
+
kind: "locked",
|
|
17669
|
+
sessionId: liveAgent.session.id
|
|
16336
17670
|
};
|
|
16337
|
-
|
|
17671
|
+
await runner.refreshCatalog({
|
|
17672
|
+
source: "preset",
|
|
17673
|
+
target: {
|
|
17674
|
+
kind: "agent",
|
|
17675
|
+
key: runner.sessionGeneration
|
|
17676
|
+
},
|
|
17677
|
+
agent: runner.liveAgent
|
|
17678
|
+
});
|
|
16338
17679
|
runner.updateWelcomeCard();
|
|
17680
|
+
return {
|
|
17681
|
+
kind: "switched",
|
|
17682
|
+
preset: outcome.preset
|
|
17683
|
+
};
|
|
17684
|
+
};
|
|
17685
|
+
const pickPreset = async (id) => {
|
|
17686
|
+
let outcome;
|
|
17687
|
+
try {
|
|
17688
|
+
outcome = await applyPresetSelection(id);
|
|
17689
|
+
} catch (error) {
|
|
17690
|
+
app.notify(safeErrorMessage(error), "error");
|
|
17691
|
+
return;
|
|
17692
|
+
}
|
|
17693
|
+
if (outcome.kind === "pending") app.notify(`new sessions will start on preset ${outcome.preset}`, "info");
|
|
17694
|
+
else if (outcome.kind === "switched") app.notify(`session preset switched to ${outcome.preset}`, "info");
|
|
17695
|
+
else app.notify(`session "${outcome.sessionId}" has already started; its agent preset is fixed — preset switching is only available in a new session`, "error");
|
|
17696
|
+
};
|
|
17697
|
+
if (verb !== "") try {
|
|
17698
|
+
const outcome = await applyPresetSelection(verb);
|
|
17699
|
+
if (outcome.kind === "pending") return {
|
|
17700
|
+
kind: "success",
|
|
17701
|
+
text: `new sessions will start on preset ${outcome.preset}`
|
|
17702
|
+
};
|
|
17703
|
+
if (outcome.kind === "locked") {
|
|
17704
|
+
const message = `session "${outcome.sessionId}" has already started; its agent preset is fixed — preset switching is only available in a new session`;
|
|
17705
|
+
app.notify(message, "error");
|
|
17706
|
+
return {
|
|
17707
|
+
kind: "error",
|
|
17708
|
+
text: message
|
|
17709
|
+
};
|
|
17710
|
+
}
|
|
16339
17711
|
return {
|
|
16340
17712
|
kind: "success",
|
|
16341
17713
|
text: `session preset switched to ${outcome.preset}`
|
|
@@ -16351,17 +17723,37 @@ function registerTuiCommands(runner) {
|
|
|
16351
17723
|
kind: "success",
|
|
16352
17724
|
text: "no agent presets configured"
|
|
16353
17725
|
};
|
|
16354
|
-
|
|
16355
|
-
|
|
16356
|
-
|
|
16357
|
-
|
|
16358
|
-
|
|
16359
|
-
|
|
16360
|
-
|
|
16361
|
-
|
|
16362
|
-
|
|
16363
|
-
|
|
16364
|
-
|
|
17726
|
+
if (liveAgent !== void 0 && liveAgent.session.events.some((event) => event.type === "turn/start")) {
|
|
17727
|
+
const message = `preset switching is only available in a new session — session "${liveAgent.session.id}" has already started; its preset is fixed (use /new for a fresh session, or /preset default <id> for future sessions)`;
|
|
17728
|
+
app.notify(message, "error");
|
|
17729
|
+
return {
|
|
17730
|
+
kind: "error",
|
|
17731
|
+
text: message
|
|
17732
|
+
};
|
|
17733
|
+
}
|
|
17734
|
+
const close = app.openSettings(roster.map((preset) => {
|
|
17735
|
+
const display = presetDisplayText(preset);
|
|
17736
|
+
return {
|
|
17737
|
+
id: preset.id,
|
|
17738
|
+
label: `${display.name} (${preset.id})`,
|
|
17739
|
+
description: [
|
|
17740
|
+
display.description,
|
|
17741
|
+
preset.trust === "system" ? "system" : "user",
|
|
17742
|
+
preset.id === presets.defaultId ? "default" : void 0,
|
|
17743
|
+
preset.id === current ? "← current" : void 0,
|
|
17744
|
+
preset.broken
|
|
17745
|
+
].filter(Boolean).join(" · "),
|
|
17746
|
+
currentValue: "",
|
|
17747
|
+
values: [preset.id]
|
|
17748
|
+
};
|
|
17749
|
+
}), (id) => {
|
|
17750
|
+
close();
|
|
17751
|
+
runOwned("preset pick", () => pickPreset(id), {
|
|
17752
|
+
diag: runner.diag,
|
|
17753
|
+
sessionId: () => runner.liveAgent?.session.id,
|
|
17754
|
+
onError: (error) => app.notify(`preset selection failed: ${safeErrorMessage(error)}`, "error")
|
|
17755
|
+
});
|
|
17756
|
+
}, () => {});
|
|
16365
17757
|
return { kind: "success" };
|
|
16366
17758
|
}
|
|
16367
17759
|
});
|
|
@@ -16469,31 +17861,54 @@ function registerTuiCommands(runner) {
|
|
|
16469
17861
|
return { kind: "success" };
|
|
16470
17862
|
}
|
|
16471
17863
|
});
|
|
16472
|
-
|
|
16473
|
-
|
|
16474
|
-
|
|
16475
|
-
|
|
16476
|
-
|
|
16477
|
-
|
|
16478
|
-
|
|
16479
|
-
|
|
16480
|
-
|
|
16481
|
-
|
|
16482
|
-
|
|
16483
|
-
|
|
16484
|
-
if (name === "") {
|
|
16485
|
-
const current = titles.get(liveAgent.session);
|
|
17864
|
+
const titleHandler = async (invocation) => {
|
|
17865
|
+
const liveAgent = await requireAgent();
|
|
17866
|
+
const titles = ctx.get("sessionTitle");
|
|
17867
|
+
if (titles === void 0) return {
|
|
17868
|
+
kind: "error",
|
|
17869
|
+
text: "session title service unavailable"
|
|
17870
|
+
};
|
|
17871
|
+
const name = invocation.rawInput.trim();
|
|
17872
|
+
if (name !== "") {
|
|
17873
|
+
try {
|
|
17874
|
+
titles.rename(liveAgent.session, name);
|
|
17875
|
+
} catch (error) {
|
|
16486
17876
|
return {
|
|
16487
|
-
kind: "
|
|
16488
|
-
text:
|
|
17877
|
+
kind: "error",
|
|
17878
|
+
text: safeErrorMessage(error)
|
|
16489
17879
|
};
|
|
16490
17880
|
}
|
|
16491
|
-
titles.rename(liveAgent.session, name);
|
|
16492
17881
|
return {
|
|
16493
17882
|
kind: "success",
|
|
16494
17883
|
text: `title set: ${name}`
|
|
16495
17884
|
};
|
|
16496
17885
|
}
|
|
17886
|
+
try {
|
|
17887
|
+
const regenerated = await titles.refresh(liveAgent.session, invocation.signal);
|
|
17888
|
+
if (regenerated === void 0) {
|
|
17889
|
+
app.notify("no conversation yet — title left as-is", "info");
|
|
17890
|
+
return { kind: "success" };
|
|
17891
|
+
}
|
|
17892
|
+
app.notify(`title regenerated: ${regenerated.title}`, "info");
|
|
17893
|
+
return { kind: "success" };
|
|
17894
|
+
} catch (error) {
|
|
17895
|
+
return {
|
|
17896
|
+
kind: "error",
|
|
17897
|
+
text: safeErrorMessage(error)
|
|
17898
|
+
};
|
|
17899
|
+
}
|
|
17900
|
+
};
|
|
17901
|
+
commands.register({
|
|
17902
|
+
name: "title",
|
|
17903
|
+
description: "Set the session title; without an argument, regenerate it from the conversation (overwrites the current title)",
|
|
17904
|
+
input: { hint: "<title>" },
|
|
17905
|
+
handler: titleHandler
|
|
17906
|
+
});
|
|
17907
|
+
commands.register({
|
|
17908
|
+
name: "rename",
|
|
17909
|
+
description: "Alias of /title: set the session title, or regenerate it without an argument (overwrites the current title)",
|
|
17910
|
+
input: { hint: "<title>" },
|
|
17911
|
+
handler: titleHandler
|
|
16497
17912
|
});
|
|
16498
17913
|
commands.register({
|
|
16499
17914
|
name: "copy",
|
|
@@ -16688,7 +18103,13 @@ function registerTuiCommands(runner) {
|
|
|
16688
18103
|
{
|
|
16689
18104
|
id: "k-enter",
|
|
16690
18105
|
label: "Enter",
|
|
16691
|
-
description: "Submit (
|
|
18106
|
+
description: "Submit (steers the running turn while busy when Enter while busy is Steer; skill commands steer too, UI commands run locally)",
|
|
18107
|
+
currentValue: ""
|
|
18108
|
+
},
|
|
18109
|
+
{
|
|
18110
|
+
id: "k-queue",
|
|
18111
|
+
label: "Ctrl+Enter",
|
|
18112
|
+
description: "Queue the draft while the agent is busy (the opposite of Enter while busy)",
|
|
16692
18113
|
currentValue: ""
|
|
16693
18114
|
},
|
|
16694
18115
|
{
|
|
@@ -16754,7 +18175,7 @@ function registerTuiCommands(runner) {
|
|
|
16754
18175
|
{
|
|
16755
18176
|
id: "k-bang",
|
|
16756
18177
|
label: "! cmd",
|
|
16757
|
-
description: "Run a shell command
|
|
18178
|
+
description: "Run a shell command and submit the command and its output to the session; !! runs locally without recording",
|
|
16758
18179
|
currentValue: ""
|
|
16759
18180
|
},
|
|
16760
18181
|
{
|
|
@@ -16773,8 +18194,28 @@ function registerTuiCommands(runner) {
|
|
|
16773
18194
|
return { kind: "success" };
|
|
16774
18195
|
}
|
|
16775
18196
|
});
|
|
16776
|
-
|
|
16777
|
-
|
|
18197
|
+
if (initial?.snapshot !== void 0) installSurfaceSnapshot(initial.snapshot);
|
|
18198
|
+
else if (initial?.skills !== void 0) withCommandCommit(() => {
|
|
18199
|
+
replaceSkillCommands(initial.skills.skills, /* @__PURE__ */ new Set());
|
|
18200
|
+
savedScopedCommands = [];
|
|
18201
|
+
installCompletions(mergeGlobalAndSavedScoped());
|
|
18202
|
+
});
|
|
18203
|
+
else refreshCompletions();
|
|
18204
|
+
ctx.on("commands/change", () => {
|
|
18205
|
+
if (commandCommitDepth > 0) {
|
|
18206
|
+
commandCommitDirty = true;
|
|
18207
|
+
return;
|
|
18208
|
+
}
|
|
18209
|
+
refreshCompletions();
|
|
18210
|
+
});
|
|
18211
|
+
return {
|
|
18212
|
+
/** The claim test for the dispatch: is /name advertised right now? */
|
|
18213
|
+
wasAdvertised,
|
|
18214
|
+
/** One synchronous catalog commit (the coordinator's install hook). */
|
|
18215
|
+
installSnapshot: (snapshot) => installSurfaceSnapshot(snapshot),
|
|
18216
|
+
/** The revalidating transition (the coordinator's target-change hook). */
|
|
18217
|
+
enterTransition: () => enterCatalogTransition()
|
|
18218
|
+
};
|
|
16778
18219
|
}
|
|
16779
18220
|
//#endregion
|
|
16780
18221
|
//#region src/diag.ts
|
|
@@ -17323,13 +18764,15 @@ function sessionUnchanged(locked, agent, generation) {
|
|
|
17323
18764
|
* (a queue splice during the guard survives) and steers them with the
|
|
17324
18765
|
* draft. Any state change — agent switch, generation bump, queue change —
|
|
17325
18766
|
* aborts with `stale` and a retry notice; nothing is written and nothing
|
|
17326
|
-
* is lost.
|
|
18767
|
+
* is lost. With `onlyDraft` the queue is neither read nor removed: the
|
|
18768
|
+
* draft alone is steered (or followed up when the agent is idle).
|
|
17327
18769
|
*/
|
|
17328
|
-
async function steerAll(deps, text) {
|
|
18770
|
+
async function steerAll(deps, text, options = {}) {
|
|
18771
|
+
const onlyDraft = options.onlyDraft === true;
|
|
17329
18772
|
const agent = deps.currentAgent();
|
|
17330
18773
|
if (agent === void 0) return "ok";
|
|
17331
18774
|
const generation = deps.currentGeneration();
|
|
17332
|
-
const snapshot = [...agent.inbox.nextTurn, ...agent.inbox.nextStep];
|
|
18775
|
+
const snapshot = onlyDraft ? [] : [...agent.inbox.nextTurn, ...agent.inbox.nextStep];
|
|
17333
18776
|
const verdict = await deps.guard.run(savePayloadIdentity(snapshot, text));
|
|
17334
18777
|
if (verdict.kind === "blocked") {
|
|
17335
18778
|
const verbatim = deps.restoreDraft(text);
|
|
@@ -17345,6 +18788,13 @@ async function steerAll(deps, text) {
|
|
|
17345
18788
|
deps.notify(verbatim ? deps.staleNotice() : deps.mergedNotice(), "error");
|
|
17346
18789
|
return "stale";
|
|
17347
18790
|
}
|
|
18791
|
+
if (onlyDraft) {
|
|
18792
|
+
if (verdict.kind === "forced") deps.notify(deps.forcedNotice(), "error");
|
|
18793
|
+
const message = deps.createDraft(text);
|
|
18794
|
+
if (now.status === "running") now.steer(message);
|
|
18795
|
+
else now.followup(message);
|
|
18796
|
+
return "ok";
|
|
18797
|
+
}
|
|
17348
18798
|
const current = [...now.inbox.nextTurn, ...now.inbox.nextStep];
|
|
17349
18799
|
if (!(current.length === snapshot.length && current.every((message, index) => message.id === snapshot[index].id))) {
|
|
17350
18800
|
const verbatim = deps.restoreDraft(text);
|
|
@@ -17367,6 +18817,75 @@ async function steerAll(deps, text) {
|
|
|
17367
18817
|
return "ok";
|
|
17368
18818
|
}
|
|
17369
18819
|
//#endregion
|
|
18820
|
+
//#region src/shell-context.ts
|
|
18821
|
+
/**
|
|
18822
|
+
* `!` shell context submission (kimi parity): a completed local shell run
|
|
18823
|
+
* is submitted to the session as an ordinary guarded user message, so the
|
|
18824
|
+
* model sees the command AND its output on the next turn. `!!` runs stay
|
|
18825
|
+
* purely local — no session write, no model visibility (pi's
|
|
18826
|
+
* excluded-from-context semantics). Extracted from the runner so the
|
|
18827
|
+
* guard/TOCTOU races are testable headless, exactly like steer.ts:
|
|
18828
|
+
*
|
|
18829
|
+
* - The agent/generation identity is captured BEFORE the guard runs.
|
|
18830
|
+
* - AFTER the guard returns, the identity is re-validated: same agent
|
|
18831
|
+
* object, same session generation. A switch while the guard read the
|
|
18832
|
+
* file aborts (`stale`) — the output is never written to a session the
|
|
18833
|
+
* guard did not check.
|
|
18834
|
+
* - A blocked write keeps the caller's card visible (the output is not
|
|
18835
|
+
* lost) and the identical `!` re-run can force through its one-time
|
|
18836
|
+
* guard token.
|
|
18837
|
+
* @module @xmoon76/dsh-pi-tui/shell-context
|
|
18838
|
+
*/
|
|
18839
|
+
/**
|
|
18840
|
+
* Submit a completed `!` shell run's command+output to the session:
|
|
18841
|
+
* capture identity → guard → re-validate → followup. No-op without an
|
|
18842
|
+
* agent. `blocked` keeps the caller's card (the output stays visible; the
|
|
18843
|
+
* identical `!` re-run can force through its one-time token); `stale`
|
|
18844
|
+
* aborts for a retry against the new session.
|
|
18845
|
+
*/
|
|
18846
|
+
async function submitShellResult(deps, text) {
|
|
18847
|
+
const agent = deps.currentAgent();
|
|
18848
|
+
if (agent === void 0) return "ok";
|
|
18849
|
+
const generation = deps.currentGeneration();
|
|
18850
|
+
const verdict = await deps.guard.run(text);
|
|
18851
|
+
if (verdict.kind === "blocked") {
|
|
18852
|
+
deps.notify(deps.blockedNotice(verdict.reason), "error");
|
|
18853
|
+
return "blocked";
|
|
18854
|
+
}
|
|
18855
|
+
if (!sessionUnchanged({
|
|
18856
|
+
agent,
|
|
18857
|
+
generation
|
|
18858
|
+
}, deps.currentAgent(), deps.currentGeneration())) {
|
|
18859
|
+
deps.notify(deps.staleNotice(), "error");
|
|
18860
|
+
return "stale";
|
|
18861
|
+
}
|
|
18862
|
+
if (verdict.kind === "forced") deps.notify(deps.forcedNotice(), "error");
|
|
18863
|
+
agent.followup(deps.createMessage(text));
|
|
18864
|
+
deps.onSubmitted();
|
|
18865
|
+
return "ok";
|
|
18866
|
+
}
|
|
18867
|
+
/**
|
|
18868
|
+
* Classify one `!` line: 'context' submits the command+output to the
|
|
18869
|
+
* session (kimi parity), 'local' runs purely off-session (pi's `!!`
|
|
18870
|
+
* escape hatch). Returns undefined for a non-`!` line.
|
|
18871
|
+
*/
|
|
18872
|
+
function shellModeOf(text) {
|
|
18873
|
+
if (!text.startsWith("!")) return void 0;
|
|
18874
|
+
return text.startsWith("!!") ? "local" : "context";
|
|
18875
|
+
}
|
|
18876
|
+
/** Extract the command after the `!` prefix ('' when nothing follows). */
|
|
18877
|
+
function shellCommandOf(text) {
|
|
18878
|
+
return text.replace(/^!+/, "").trim();
|
|
18879
|
+
}
|
|
18880
|
+
/**
|
|
18881
|
+
* The model-facing submission text: the command echoed `$`-style (kimi
|
|
18882
|
+
* ShellExecution parity) followed by the settled card result (output +
|
|
18883
|
+
* `[exit N]` / truncation lines).
|
|
18884
|
+
*/
|
|
18885
|
+
function formatShellSubmitText(command, result) {
|
|
18886
|
+
return `$ ${command}\n${result}`;
|
|
18887
|
+
}
|
|
18888
|
+
//#endregion
|
|
17370
18889
|
//#region src/bounded-output.ts
|
|
17371
18890
|
/**
|
|
17372
18891
|
* Bounded accumulation of local-shell output: the UI card only ever holds
|
|
@@ -17653,6 +19172,191 @@ function parseShellWords(line) {
|
|
|
17653
19172
|
return words;
|
|
17654
19173
|
}
|
|
17655
19174
|
//#endregion
|
|
19175
|
+
//#region src/skill-catalog-refresh.ts
|
|
19176
|
+
/** Whether two targets are the same owner (no transition needed). The
|
|
19177
|
+
* preset identity is the caller-provided presetId compared EXACTLY —
|
|
19178
|
+
* `undefined` (the deployment default) never conflates with a user preset
|
|
19179
|
+
* literally named `'default'`, and a resolved id never conflates with the
|
|
19180
|
+
* raw id it came from (the worst case is a benign extra transition). */
|
|
19181
|
+
function sameTarget(left, right) {
|
|
19182
|
+
if (left === void 0) return false;
|
|
19183
|
+
if (left.kind !== right.kind) return false;
|
|
19184
|
+
return left.kind === "agent" ? left.key === right.key : left.presetId === right.presetId;
|
|
19185
|
+
}
|
|
19186
|
+
/**
|
|
19187
|
+
* The coordinator. One instance per runner; `dispose()` aborts the active
|
|
19188
|
+
* refresh on teardown.
|
|
19189
|
+
*/
|
|
19190
|
+
var CatalogRefreshCoordinator = class {
|
|
19191
|
+
epoch = 0;
|
|
19192
|
+
active;
|
|
19193
|
+
appliedTarget;
|
|
19194
|
+
snapshot;
|
|
19195
|
+
hooks;
|
|
19196
|
+
lifecycleSignal;
|
|
19197
|
+
diag;
|
|
19198
|
+
constructor(hooks, lifecycleSignal, diag) {
|
|
19199
|
+
this.hooks = hooks;
|
|
19200
|
+
this.lifecycleSignal = lifecycleSignal;
|
|
19201
|
+
this.diag = diag;
|
|
19202
|
+
}
|
|
19203
|
+
/** The last applied snapshot (for same-target partial merges and the
|
|
19204
|
+
* `/reload` report). */
|
|
19205
|
+
get currentSnapshot() {
|
|
19206
|
+
return this.snapshot;
|
|
19207
|
+
}
|
|
19208
|
+
/**
|
|
19209
|
+
* Run one refresh request. A new request aborts the active one; only the
|
|
19210
|
+
* LATEST epoch may commit. Returns the settled outcome; never rejects
|
|
19211
|
+
* (failures and supersessions are outcomes, not exceptions).
|
|
19212
|
+
*/
|
|
19213
|
+
async refresh(request) {
|
|
19214
|
+
this.active?.controller.abort();
|
|
19215
|
+
const epoch = ++this.epoch;
|
|
19216
|
+
const controller = new AbortController();
|
|
19217
|
+
const signal = AbortSignal.any([this.lifecycleSignal, controller.signal]);
|
|
19218
|
+
this.active = {
|
|
19219
|
+
epoch,
|
|
19220
|
+
controller
|
|
19221
|
+
};
|
|
19222
|
+
const targetChanged = !sameTarget(this.appliedTarget, request.target);
|
|
19223
|
+
try {
|
|
19224
|
+
if (targetChanged) this.hooks.enterCatalogTransition();
|
|
19225
|
+
const committed = request.target.kind === "agent" ? { snapshot: await this.hooks.readAgent(request.agent, signal) } : await this.readStandingSnapshot(request.target.presetId, signal);
|
|
19226
|
+
if (signal.aborted || epoch !== this.epoch) {
|
|
19227
|
+
this.diag.debug("catalog refresh superseded", {
|
|
19228
|
+
epoch,
|
|
19229
|
+
source: request.source
|
|
19230
|
+
});
|
|
19231
|
+
return { kind: "superseded" };
|
|
19232
|
+
}
|
|
19233
|
+
const merged = targetChanged ? committed.snapshot : this.mergePartial(committed.snapshot);
|
|
19234
|
+
this.hooks.installSnapshot(merged);
|
|
19235
|
+
this.snapshot = merged;
|
|
19236
|
+
this.appliedTarget = request.target;
|
|
19237
|
+
this.diag.info("catalog applied", {
|
|
19238
|
+
epoch,
|
|
19239
|
+
source: request.source,
|
|
19240
|
+
commands: merged.commands.length,
|
|
19241
|
+
scopedCommands: merged.scopedCommands.length,
|
|
19242
|
+
skills: merged.skills.length,
|
|
19243
|
+
issues: merged.issues.length
|
|
19244
|
+
});
|
|
19245
|
+
return {
|
|
19246
|
+
kind: "applied",
|
|
19247
|
+
snapshot: merged,
|
|
19248
|
+
...committed.notice === void 0 ? {} : { notice: committed.notice }
|
|
19249
|
+
};
|
|
19250
|
+
} catch (error) {
|
|
19251
|
+
if (signal.aborted || epoch !== this.epoch) {
|
|
19252
|
+
this.diag.debug("catalog refresh superseded", {
|
|
19253
|
+
epoch,
|
|
19254
|
+
source: request.source
|
|
19255
|
+
});
|
|
19256
|
+
return { kind: "superseded" };
|
|
19257
|
+
}
|
|
19258
|
+
const message = safeErrorMessage(error);
|
|
19259
|
+
this.diag.warn("catalog unavailable", {
|
|
19260
|
+
epoch,
|
|
19261
|
+
source: request.source,
|
|
19262
|
+
error: message
|
|
19263
|
+
});
|
|
19264
|
+
return {
|
|
19265
|
+
kind: "failed",
|
|
19266
|
+
error: message
|
|
19267
|
+
};
|
|
19268
|
+
} finally {
|
|
19269
|
+
if (this.active?.epoch === epoch) this.active = void 0;
|
|
19270
|
+
}
|
|
19271
|
+
}
|
|
19272
|
+
/** Abort the active refresh (app teardown). Late results are dropped. */
|
|
19273
|
+
dispose() {
|
|
19274
|
+
this.active?.controller.abort();
|
|
19275
|
+
}
|
|
19276
|
+
/** Same-target partial failure: a provider issue keeps the OLD field for
|
|
19277
|
+
* that provider (the old snapshot is still more useful than a blank
|
|
19278
|
+
* catalog); the successful providers update as usual. */
|
|
19279
|
+
mergePartial(snapshot) {
|
|
19280
|
+
const old = this.snapshot;
|
|
19281
|
+
if (old === void 0) return snapshot;
|
|
19282
|
+
const commandsFailed = snapshot.issues.some((issue) => issue.provider === "commands");
|
|
19283
|
+
const skillsFailed = snapshot.issues.some((issue) => issue.provider === "skills");
|
|
19284
|
+
if (!commandsFailed && !skillsFailed) return snapshot;
|
|
19285
|
+
return Object.freeze({
|
|
19286
|
+
commands: commandsFailed ? old.commands : snapshot.commands,
|
|
19287
|
+
scopedCommands: commandsFailed ? old.scopedCommands : snapshot.scopedCommands,
|
|
19288
|
+
skills: skillsFailed ? old.skills : snapshot.skills,
|
|
19289
|
+
issues: snapshot.issues
|
|
19290
|
+
});
|
|
19291
|
+
}
|
|
19292
|
+
/** One standing read wrapped into a skills-only surface snapshot: the
|
|
19293
|
+
* sessionless view has no agent-scoped commands, and the global commands
|
|
19294
|
+
* layer is already registered — only the skill wrappers are installed.
|
|
19295
|
+
* An INCOMPLETE observation (`complete !== true`) is never an
|
|
19296
|
+
* authoritative blank catalog: it carries a detached skills issue, so a
|
|
19297
|
+
* same-target reload keeps the last-good skills (mergePartial) and a
|
|
19298
|
+
* target change keeps the revalidating transition wrappers
|
|
19299
|
+
* (installSurfaceSnapshot's skillsFailed guard). */
|
|
19300
|
+
async readStandingSnapshot(presetId, signal) {
|
|
19301
|
+
const read = await this.hooks.readStanding(presetId, signal);
|
|
19302
|
+
const incomplete = read.catalog.complete !== true;
|
|
19303
|
+
return {
|
|
19304
|
+
snapshot: Object.freeze({
|
|
19305
|
+
commands: Object.freeze([]),
|
|
19306
|
+
scopedCommands: Object.freeze([]),
|
|
19307
|
+
skills: read.catalog.skills,
|
|
19308
|
+
issues: Object.freeze(incomplete ? [Object.freeze({
|
|
19309
|
+
provider: "skills",
|
|
19310
|
+
message: "incomplete skill observation"
|
|
19311
|
+
})] : [])
|
|
19312
|
+
}),
|
|
19313
|
+
...read.notice === void 0 ? {} : { notice: read.notice }
|
|
19314
|
+
};
|
|
19315
|
+
}
|
|
19316
|
+
};
|
|
19317
|
+
/**
|
|
19318
|
+
* The deterministic, timer-free coalescer for invalidation notifications
|
|
19319
|
+
* (`skills/change`): while one refresh is in flight, further notifications
|
|
19320
|
+
* mark the gate DIRTY; when the in-flight refresh settles, ONE more refresh
|
|
19321
|
+
* runs if the gate is dirty. A burst therefore costs at most two reads, and
|
|
19322
|
+
* the re-run always observes the CURRENT ownership because the runner's
|
|
19323
|
+
* `start` callback reads live state at run time.
|
|
19324
|
+
*/
|
|
19325
|
+
var CoalescingRefreshGate = class {
|
|
19326
|
+
inFlight = false;
|
|
19327
|
+
dirty = false;
|
|
19328
|
+
start;
|
|
19329
|
+
/** @param start - invoked synchronously to START one owned refresh. Must
|
|
19330
|
+
* never throw and must not itself be re-entrant (the gate owns the
|
|
19331
|
+
* in-flight bookkeeping; `start` only launches the owned task). */
|
|
19332
|
+
constructor(start) {
|
|
19333
|
+
this.start = start;
|
|
19334
|
+
}
|
|
19335
|
+
/** One invalidation notification arrived. */
|
|
19336
|
+
notify() {
|
|
19337
|
+
if (this.inFlight) {
|
|
19338
|
+
this.dirty = true;
|
|
19339
|
+
return;
|
|
19340
|
+
}
|
|
19341
|
+
this.inFlight = true;
|
|
19342
|
+
this.start();
|
|
19343
|
+
}
|
|
19344
|
+
/** Terminal settle of the in-flight refresh (called from EVERY terminal
|
|
19345
|
+
* runOwned callback: onResult / onCancel / onError). Clears the in-flight
|
|
19346
|
+
* flag; a dirty gate starts exactly one follow-up refresh. IDEMPOTENT: a
|
|
19347
|
+
* second settle (e.g. onResult throws and runOwned routes to onError)
|
|
19348
|
+
* must not clear the in-flight flag of a follow-up refresh it did not
|
|
19349
|
+
* start. */
|
|
19350
|
+
settled() {
|
|
19351
|
+
if (!this.inFlight) return;
|
|
19352
|
+
this.inFlight = false;
|
|
19353
|
+
if (this.dirty) {
|
|
19354
|
+
this.dirty = false;
|
|
19355
|
+
this.notify();
|
|
19356
|
+
}
|
|
19357
|
+
}
|
|
19358
|
+
};
|
|
19359
|
+
//#endregion
|
|
17656
19360
|
//#region src/index.ts
|
|
17657
19361
|
/**
|
|
17658
19362
|
* @xmoon76/dsh-pi-tui — the bundle's runner plugin. Waits for the startup
|
|
@@ -17685,6 +19389,9 @@ const REPAINT_FLUSH_MS = 50;
|
|
|
17685
19389
|
* dispatches through `commands.execute`, which creates the session lazily
|
|
17686
19390
|
* (the command line IS the first user input). Commands in this set must
|
|
17687
19391
|
* tolerate `liveAgent === undefined` in their handlers.
|
|
19392
|
+
*
|
|
19393
|
+
* Exported for the headless suite: the gate is exactly where a sessionless
|
|
19394
|
+
* command silently starts creating sessions again.
|
|
17688
19395
|
*/
|
|
17689
19396
|
const SESSIONLESS_COMMANDS = /* @__PURE__ */ new Set([
|
|
17690
19397
|
"exit",
|
|
@@ -17698,14 +19405,123 @@ const SESSIONLESS_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
17698
19405
|
"resume",
|
|
17699
19406
|
"search",
|
|
17700
19407
|
"new",
|
|
17701
|
-
"fork"
|
|
19408
|
+
"fork",
|
|
19409
|
+
"preset"
|
|
17702
19410
|
]);
|
|
17703
19411
|
/**
|
|
17704
|
-
*
|
|
17705
|
-
*
|
|
17706
|
-
*
|
|
17707
|
-
*
|
|
19412
|
+
* The LOCAL-execute command set: TUI-owned UI/control commands AND core
|
|
19413
|
+
* control commands the TUI does not itself register (e.g. /kill) that
|
|
19414
|
+
* must ALWAYS run locally through the commands service, never steered,
|
|
19415
|
+
* regardless of the busyEnter preference. Everything NOT in this set —
|
|
19416
|
+
* plain prompts AND non-local commands (the per-skill slash commands like
|
|
19417
|
+
* /grilling or /matrix-cli) — flows through the busy-Enter submission
|
|
19418
|
+
* policy while the agent is running: web parity, where a skill invocation
|
|
19419
|
+
* is a plain `session.prompt` whose leading `/name` line the host's
|
|
19420
|
+
* pre-step listener (dsh-tool-skill) resolves into the injected skill
|
|
19421
|
+
* body — there is no command-execution wire for skills.
|
|
17708
19422
|
*/
|
|
19423
|
+
const LOCAL_COMMANDS = /* @__PURE__ */ new Set([
|
|
19424
|
+
"copy",
|
|
19425
|
+
"exit",
|
|
19426
|
+
"export",
|
|
19427
|
+
"fork",
|
|
19428
|
+
"help",
|
|
19429
|
+
"kill",
|
|
19430
|
+
"login",
|
|
19431
|
+
"logout",
|
|
19432
|
+
"model",
|
|
19433
|
+
"new",
|
|
19434
|
+
"preset",
|
|
19435
|
+
"queue",
|
|
19436
|
+
"quit",
|
|
19437
|
+
"reload",
|
|
19438
|
+
"rename",
|
|
19439
|
+
"resume",
|
|
19440
|
+
"search",
|
|
19441
|
+
"sessions",
|
|
19442
|
+
"settings",
|
|
19443
|
+
"skill",
|
|
19444
|
+
"status",
|
|
19445
|
+
"subagents",
|
|
19446
|
+
"tasks",
|
|
19447
|
+
"title",
|
|
19448
|
+
"yolo"
|
|
19449
|
+
]);
|
|
19450
|
+
/**
|
|
19451
|
+
* Whether one submission steers under the busy-Enter preference: NOT a
|
|
19452
|
+
* force-queued chord, NOT a TUI-owned local command, and the agent is
|
|
19453
|
+
* running with the preference set to 'steer'. Pure so the dispatch gate
|
|
19454
|
+
* (inside the runner closure) is testable headless.
|
|
19455
|
+
* @param parsed - the parsed slash command, undefined for a plain prompt.
|
|
19456
|
+
* @param running - whether the live agent reports running.
|
|
19457
|
+
* @param busyEnter - the persisted preference value (''/undefined = queue).
|
|
19458
|
+
* @param forceQueue - the Ctrl+Enter chord: always queue, never steer.
|
|
19459
|
+
*/
|
|
19460
|
+
function shouldSteerOnEnter(parsed, running, busyEnter, forceQueue) {
|
|
19461
|
+
if (forceQueue) return false;
|
|
19462
|
+
if (parsed !== void 0 && LOCAL_COMMANDS.has(parsed.name)) return false;
|
|
19463
|
+
return running && busyEnter === "steer";
|
|
19464
|
+
}
|
|
19465
|
+
/**
|
|
19466
|
+
* The advertised-claim miss decision (pure, exported for the headless
|
|
19467
|
+
* suite): a slash input whose name was advertised by the completion list at
|
|
19468
|
+
* submit time but that the REAL session's catalog lacks is CONSUMED with an
|
|
19469
|
+
* explicit error — never sent to the model as a plain user message. An
|
|
19470
|
+
* unadvertised miss keeps the existing plain-input fallback (the user may
|
|
19471
|
+
* deliberately send slash text to the model).
|
|
19472
|
+
* @param execution - the settled `commands.execute` outcome.
|
|
19473
|
+
* @param wasAdvertised - the claim captured BEFORE session creation.
|
|
19474
|
+
* @returns whether the miss must be consumed as an advertised miss.
|
|
19475
|
+
*/
|
|
19476
|
+
function shouldConsumeAdvertisedMiss(execution, wasAdvertised) {
|
|
19477
|
+
return execution === void 0 && wasAdvertised;
|
|
19478
|
+
}
|
|
19479
|
+
async function resolveInitialCatalog(options) {
|
|
19480
|
+
const { liveAgent, presetId, signal, ctx, diag } = options;
|
|
19481
|
+
if (liveAgent !== void 0) try {
|
|
19482
|
+
const snapshot = await readSurfaceCatalog(liveAgent, signal, ctx);
|
|
19483
|
+
diag.info("surface catalog prefetched", {
|
|
19484
|
+
commands: snapshot.commands.length,
|
|
19485
|
+
scopedCommands: snapshot.scopedCommands.length,
|
|
19486
|
+
skills: snapshot.skills.length
|
|
19487
|
+
});
|
|
19488
|
+
return { snapshot };
|
|
19489
|
+
} catch (error) {
|
|
19490
|
+
if (isCancellation(error)) return {};
|
|
19491
|
+
const message = safeErrorMessage(error);
|
|
19492
|
+
diag.warn("surface catalog unavailable", {
|
|
19493
|
+
phase: "resume",
|
|
19494
|
+
error: message
|
|
19495
|
+
});
|
|
19496
|
+
return { notice: `surface catalog unavailable: ${message}` };
|
|
19497
|
+
}
|
|
19498
|
+
const target = await resolveColdSkillTarget(ctx, presetId, process.cwd());
|
|
19499
|
+
if (target.target === void 0) return {};
|
|
19500
|
+
try {
|
|
19501
|
+
const catalog = await readHumanSkillCatalog(target.target.registry, {
|
|
19502
|
+
cwd: target.target.cwd,
|
|
19503
|
+
scope: target.target.scope,
|
|
19504
|
+
signal
|
|
19505
|
+
});
|
|
19506
|
+
diag.info("skill catalog standing ready", {
|
|
19507
|
+
preset: presetId ?? "default",
|
|
19508
|
+
skills: catalog.skills.length,
|
|
19509
|
+
complete: catalog.complete
|
|
19510
|
+
});
|
|
19511
|
+
return {
|
|
19512
|
+
skills: catalog,
|
|
19513
|
+
...target.degraded === void 0 ? {} : { notice: target.degraded }
|
|
19514
|
+
};
|
|
19515
|
+
} catch (error) {
|
|
19516
|
+
if (isCancellation(error)) return {};
|
|
19517
|
+
const message = safeErrorMessage(error);
|
|
19518
|
+
diag.warn("skill catalog unavailable", {
|
|
19519
|
+
phase: "cold",
|
|
19520
|
+
error: message
|
|
19521
|
+
});
|
|
19522
|
+
return { notice: `skill catalog unavailable: ${message}` };
|
|
19523
|
+
}
|
|
19524
|
+
}
|
|
17709
19525
|
function subagentJobTranscriptId(snapshot) {
|
|
17710
19526
|
if (typeof snapshot !== "object" || snapshot === null) return void 0;
|
|
17711
19527
|
const childSessionId = snapshot.childSessionId;
|
|
@@ -17998,6 +19814,7 @@ function apply(ctx, config) {
|
|
|
17998
19814
|
const compose = (presetId) => composeAgent(ctx, selected, presetId);
|
|
17999
19815
|
const withPresetMeta = (composition) => composition.agentPreset === void 0 ? {} : { agentPreset: composition.agentPreset };
|
|
18000
19816
|
const launchPreset = startup.presetId ?? (process.env.DSH_PI_TUI_PRESET?.trim() || void 0);
|
|
19817
|
+
let pendingPreset;
|
|
18001
19818
|
diag.info("boot", {
|
|
18002
19819
|
pid: process.pid,
|
|
18003
19820
|
dsh: dshVersion() ?? "unknown",
|
|
@@ -18022,7 +19839,7 @@ function apply(ctx, config) {
|
|
|
18022
19839
|
/** Resolve the launch composition, falling back to the default on an unknown id. */
|
|
18023
19840
|
const launchComposition = async () => {
|
|
18024
19841
|
try {
|
|
18025
|
-
return { composition: await compose(launchPreset) };
|
|
19842
|
+
return { composition: await compose(pendingPreset ?? launchPreset) };
|
|
18026
19843
|
} catch (error) {
|
|
18027
19844
|
const message = safeErrorMessage(error);
|
|
18028
19845
|
ctx.logger.warn(`tui-runner: launch preset unavailable: ${message}`);
|
|
@@ -18092,6 +19909,29 @@ function apply(ctx, config) {
|
|
|
18092
19909
|
let liveHandle = handle;
|
|
18093
19910
|
let liveAgent = handle?.agent;
|
|
18094
19911
|
if (liveAgent !== void 0) await liveAgent.whenIdle();
|
|
19912
|
+
let initialSnapshot;
|
|
19913
|
+
let initialSkills;
|
|
19914
|
+
let surfaceNotice;
|
|
19915
|
+
{
|
|
19916
|
+
let effectivePresetId;
|
|
19917
|
+
try {
|
|
19918
|
+
const launched = await launchComposition();
|
|
19919
|
+
if (launched.failure !== void 0) resumeFailure = launched.failure;
|
|
19920
|
+
effectivePresetId = launched.composition.agentPreset;
|
|
19921
|
+
} catch (error) {
|
|
19922
|
+
diag.warn("preset resolution failed at startup", { error: safeErrorMessage(error) });
|
|
19923
|
+
}
|
|
19924
|
+
const resolution = await resolveInitialCatalog({
|
|
19925
|
+
liveAgent,
|
|
19926
|
+
presetId: effectivePresetId,
|
|
19927
|
+
signal: lifecycleController.signal,
|
|
19928
|
+
ctx,
|
|
19929
|
+
diag
|
|
19930
|
+
});
|
|
19931
|
+
initialSnapshot = resolution.snapshot;
|
|
19932
|
+
initialSkills = resolution.skills;
|
|
19933
|
+
surfaceNotice = resolution.notice;
|
|
19934
|
+
}
|
|
18095
19935
|
let guardState = freshGuardState();
|
|
18096
19936
|
let guardToken;
|
|
18097
19937
|
/**
|
|
@@ -18228,6 +20068,7 @@ function apply(ctx, config) {
|
|
|
18228
20068
|
guardToken = void 0;
|
|
18229
20069
|
bumpSessionGeneration();
|
|
18230
20070
|
await initLiveSession(next.agent);
|
|
20071
|
+
await refreshLiveCatalog(next.agent);
|
|
18231
20072
|
diag.info("switch ok", {
|
|
18232
20073
|
from: from ?? "(none)",
|
|
18233
20074
|
to: next.agent.session.id,
|
|
@@ -18314,6 +20155,7 @@ function apply(ctx, config) {
|
|
|
18314
20155
|
cleanedUp = true;
|
|
18315
20156
|
guardToken = void 0;
|
|
18316
20157
|
lifecycleController.abort();
|
|
20158
|
+
catalogCoordinator?.dispose();
|
|
18317
20159
|
localShellController?.abort();
|
|
18318
20160
|
for (const file of shellTempFiles) try {
|
|
18319
20161
|
rmSync(file, { force: true });
|
|
@@ -18341,22 +20183,19 @@ function apply(ctx, config) {
|
|
|
18341
20183
|
cleanup();
|
|
18342
20184
|
};
|
|
18343
20185
|
});
|
|
18344
|
-
/**
|
|
20186
|
+
/**
|
|
20187
|
+
* Run a `!` shell command. `!` (context mode) runs the command and then
|
|
20188
|
+
* submits the completed command+output to the session as an ordinary
|
|
20189
|
+
* guarded user message (kimi parity: the model sees both on the next
|
|
20190
|
+
* turn; the result wakes a turn but is never steered into a running
|
|
20191
|
+
* one); `!!` (local mode) runs purely off-session — the card is the
|
|
20192
|
+
* only record (pi's excluded-from-context escape hatch).
|
|
20193
|
+
*/
|
|
18345
20194
|
const runLocalShell = (text) => {
|
|
18346
|
-
const includeInContext = text
|
|
18347
|
-
const command = text
|
|
20195
|
+
const includeInContext = shellModeOf(text) === "context";
|
|
20196
|
+
const command = shellCommandOf(text);
|
|
18348
20197
|
if (command === "") return;
|
|
18349
|
-
|
|
18350
|
-
if (liveAgent === void 0) return;
|
|
18351
|
-
liveAgent.followup(createUserMessage({
|
|
18352
|
-
content: [{
|
|
18353
|
-
type: "text",
|
|
18354
|
-
text: command
|
|
18355
|
-
}],
|
|
18356
|
-
source: { kind: "user" }
|
|
18357
|
-
}));
|
|
18358
|
-
return;
|
|
18359
|
-
}
|
|
20198
|
+
const generationAtRun = sessionGeneration;
|
|
18360
20199
|
localShellController?.abort();
|
|
18361
20200
|
localShellController = new AbortController();
|
|
18362
20201
|
const localSignal = localShellController.signal;
|
|
@@ -18372,6 +20211,51 @@ function apply(ctx, config) {
|
|
|
18372
20211
|
const releaseController = () => {
|
|
18373
20212
|
if (localShellController?.signal === localSignal) localShellController = void 0;
|
|
18374
20213
|
};
|
|
20214
|
+
/**
|
|
20215
|
+
* Submit the completed run to the session (context mode only): guard
|
|
20216
|
+
* → re-validate → followup. Blocked keeps the card (the output stays
|
|
20217
|
+
* visible; the identical `!` re-run forces through its one-time guard
|
|
20218
|
+
* token); accepted clears the settled card — the transcript's user
|
|
20219
|
+
* row becomes the record. An owned workflow: the outcome drives the
|
|
20220
|
+
* notify and the card — runOwned (AGENTS.md), never a bare void.
|
|
20221
|
+
*/
|
|
20222
|
+
const submitResult = (result) => {
|
|
20223
|
+
if (sessionGeneration !== generationAtRun) {
|
|
20224
|
+
app.notify("the session changed while the command ran — the output was not submitted", "error");
|
|
20225
|
+
return;
|
|
20226
|
+
}
|
|
20227
|
+
const submitted = formatShellSubmitText(command, result);
|
|
20228
|
+
runOwned("shell submit", () => submitShellResult({
|
|
20229
|
+
currentAgent: () => liveAgent,
|
|
20230
|
+
currentGeneration: () => sessionGeneration,
|
|
20231
|
+
guard: { run: async (identity) => {
|
|
20232
|
+
const verdict = await guardSend("submit", identity);
|
|
20233
|
+
if (verdict.kind === "blocked") return {
|
|
20234
|
+
kind: "blocked",
|
|
20235
|
+
reason: verdict.reason
|
|
20236
|
+
};
|
|
20237
|
+
return { kind: verdict.kind === "forced" ? "forced" : "ok" };
|
|
20238
|
+
} },
|
|
20239
|
+
notify: (message, kind) => app.notify(message, kind),
|
|
20240
|
+
blockedNotice: (reason) => reason === "removed" ? `This session's log was removed externally — the command output was not submitted (it stays on the card). Run the same ! command again to force` : reason === "tail-mismatch" ? "This session file was rewritten by another process (same event count, different content) — the command output was not submitted (it stays on the card). Run the same ! command again to force (may corrupt the session log)" : reason === "unreadable" ? "This session's log could not be read (locked or corrupt) — the command output was not submitted (it stays on the card). Run the same ! command again to force (may corrupt the session log)" : "This session may be open in another dsh process (TUI/web) — the command output was not submitted (it stays on the card). Run the same ! command again to force (may corrupt the session log)",
|
|
20241
|
+
forcedNotice: () => GUARD_FORCED_NOTIFY,
|
|
20242
|
+
staleNotice: () => "the session changed while the submission was being checked — the output was not submitted",
|
|
20243
|
+
createMessage: (text) => createUserMessage({
|
|
20244
|
+
content: [{
|
|
20245
|
+
type: "text",
|
|
20246
|
+
text
|
|
20247
|
+
}],
|
|
20248
|
+
source: { kind: "user" }
|
|
20249
|
+
}),
|
|
20250
|
+
onSubmitted: () => app.clearSettledLocalMessages()
|
|
20251
|
+
}, submitted), {
|
|
20252
|
+
diag,
|
|
20253
|
+
sessionId: () => liveAgent?.session.id,
|
|
20254
|
+
onError: (error) => {
|
|
20255
|
+
app.notify(`shell submit failed: ${safeErrorMessage(error)}`, "error");
|
|
20256
|
+
}
|
|
20257
|
+
});
|
|
20258
|
+
};
|
|
18375
20259
|
let settled = false;
|
|
18376
20260
|
const settle = (result, status) => {
|
|
18377
20261
|
if (settled) return;
|
|
@@ -18384,6 +20268,7 @@ function apply(ctx, config) {
|
|
|
18384
20268
|
result,
|
|
18385
20269
|
status
|
|
18386
20270
|
});
|
|
20271
|
+
if (includeInContext && !localSignal.aborted) submitResult(result);
|
|
18387
20272
|
};
|
|
18388
20273
|
const shell = ctx.get("shell");
|
|
18389
20274
|
if (shell !== void 0) {
|
|
@@ -18537,7 +20422,10 @@ function apply(ctx, config) {
|
|
|
18537
20422
|
folder: childFolder
|
|
18538
20423
|
};
|
|
18539
20424
|
repaint(app, childFolder);
|
|
18540
|
-
app.
|
|
20425
|
+
app.setViewerMode({
|
|
20426
|
+
id: childId,
|
|
20427
|
+
label: label ?? childId
|
|
20428
|
+
});
|
|
18541
20429
|
};
|
|
18542
20430
|
/** Leave the subagent viewer (single Esc). Returns whether it exited. */
|
|
18543
20431
|
const exitView = () => {
|
|
@@ -18545,6 +20433,7 @@ function apply(ctx, config) {
|
|
|
18545
20433
|
viewing = void 0;
|
|
18546
20434
|
app.clearLocalMessages();
|
|
18547
20435
|
app.clearNotify();
|
|
20436
|
+
app.setViewerMode(void 0);
|
|
18548
20437
|
repaint(app, folder);
|
|
18549
20438
|
refreshStatus();
|
|
18550
20439
|
return true;
|
|
@@ -18566,6 +20455,8 @@ function apply(ctx, config) {
|
|
|
18566
20455
|
* user input is the deferred trigger), guard against cross-process
|
|
18567
20456
|
* divergence, then execute a registered slash command or follow up. */
|
|
18568
20457
|
const dispatchViaSession = (text) => {
|
|
20458
|
+
const parsedAtSubmit = parseCommand(text);
|
|
20459
|
+
const wasAdvertised = parsedAtSubmit !== void 0 && wasAdvertisedClaim?.(parsedAtSubmit.name) === true;
|
|
18569
20460
|
runOwned("submit", () => ensureSession().then(async () => {
|
|
18570
20461
|
const agent = liveAgent;
|
|
18571
20462
|
if (agent === void 0) return;
|
|
@@ -18595,6 +20486,10 @@ function apply(ctx, config) {
|
|
|
18595
20486
|
diag,
|
|
18596
20487
|
sessionId: () => agent.session.id,
|
|
18597
20488
|
onResult: (execution) => {
|
|
20489
|
+
if (shouldConsumeAdvertisedMiss(execution, wasAdvertised)) {
|
|
20490
|
+
app.notify(`/${parsedAtSubmit?.name ?? "?"} is not available in the created session`, "error");
|
|
20491
|
+
return;
|
|
20492
|
+
}
|
|
18598
20493
|
if (execution === void 0) {
|
|
18599
20494
|
if (sessionUnchanged({
|
|
18600
20495
|
agent,
|
|
@@ -18671,44 +20566,119 @@ function apply(ctx, config) {
|
|
|
18671
20566
|
}
|
|
18672
20567
|
});
|
|
18673
20568
|
};
|
|
20569
|
+
/**
|
|
20570
|
+
* Steer into the running turn with guard re-validation. Shared by
|
|
20571
|
+
* Ctrl+S (the whole queue plus a non-empty draft) and the busy-Enter
|
|
20572
|
+
* preference — Enter while the agent is running with busyEnter=steer
|
|
20573
|
+
* steers the DRAFT ONLY (web busyEnter parity): explicitly queued
|
|
20574
|
+
* messages stay queued until Ctrl+S or the /queue actions, because
|
|
20575
|
+
* already-steered input cannot be pulled back.
|
|
20576
|
+
* @param text - the submitted draft ('' allowed for Ctrl+S).
|
|
20577
|
+
* @param onlyDraft - busy-Enter mode: never read or remove the queue.
|
|
20578
|
+
*/
|
|
20579
|
+
const steerNow = (text, onlyDraft = false) => {
|
|
20580
|
+
if (viewing !== void 0) {
|
|
20581
|
+
if (text.trim() !== "") app.setEditorText(mergeDraft(app.getDraft(), text));
|
|
20582
|
+
app.notify("viewing a subagent — Esc returns before steering", "info");
|
|
20583
|
+
return;
|
|
20584
|
+
}
|
|
20585
|
+
app.clearSettledLocalMessages();
|
|
20586
|
+
if (onlyDraft) {
|
|
20587
|
+
if (text.trim() === "") return;
|
|
20588
|
+
} else if (liveAgent !== void 0) {
|
|
20589
|
+
if ([...liveAgent.inbox.nextTurn, ...liveAgent.inbox.nextStep].length === 0 && text.trim() === "") return;
|
|
20590
|
+
} else if (text.trim() === "") return;
|
|
20591
|
+
runOwned("steer", () => ensureSession().then(async () => {
|
|
20592
|
+
if (liveAgent === void 0) return;
|
|
20593
|
+
await steerAll({
|
|
20594
|
+
currentAgent: () => liveAgent,
|
|
20595
|
+
currentGeneration: () => sessionGeneration,
|
|
20596
|
+
guard: { run: async (identity) => {
|
|
20597
|
+
const verdict = await guardSend("save", identity);
|
|
20598
|
+
if (verdict.kind === "blocked") return {
|
|
20599
|
+
kind: "blocked",
|
|
20600
|
+
reason: verdict.reason
|
|
20601
|
+
};
|
|
20602
|
+
return { kind: verdict.kind === "forced" ? "forced" : "ok" };
|
|
20603
|
+
} },
|
|
20604
|
+
notify: (message, kind) => app.notify(message, kind),
|
|
20605
|
+
restoreDraft: (draft) => {
|
|
20606
|
+
const merged = mergeDraft(app.getDraft(), draft);
|
|
20607
|
+
app.setEditorText(merged);
|
|
20608
|
+
return merged === draft;
|
|
20609
|
+
},
|
|
20610
|
+
createDraft: (draft) => createUserMessage({
|
|
20611
|
+
content: [{
|
|
20612
|
+
type: "text",
|
|
20613
|
+
text: draft
|
|
20614
|
+
}],
|
|
20615
|
+
source: { kind: "user" }
|
|
20616
|
+
}),
|
|
20617
|
+
blockedNotice: (reason) => reason === "removed" ? GUARD_REMOVED_NOTIFY("save") : reason === "tail-mismatch" ? GUARD_TAIL_MISMATCH_NOTIFY("save") : GUARD_BLOCKED_NOTIFY("save"),
|
|
20618
|
+
forcedNotice: () => GUARD_FORCED_NOTIFY,
|
|
20619
|
+
staleNotice: () => "the queue or session changed while sending — try again",
|
|
20620
|
+
mergedNotice: () => "the draft changed while sending — review it before submitting again (the earlier text was preserved below)"
|
|
20621
|
+
}, text, onlyDraft ? { onlyDraft: true } : void 0);
|
|
20622
|
+
}), {
|
|
20623
|
+
diag,
|
|
20624
|
+
sessionId: () => liveAgent?.session.id,
|
|
20625
|
+
onError: failSubmission(text)
|
|
20626
|
+
});
|
|
20627
|
+
};
|
|
20628
|
+
/**
|
|
20629
|
+
* Dispatch one user submission end to end: the viewer guard, the input-
|
|
20630
|
+
* history persistence, `!` local shells, sessionless commands, the
|
|
20631
|
+
* busy-Enter preference, and the session dispatch. The Ctrl+Enter
|
|
20632
|
+
* opposite chord forces the QUEUE mode (forceQueue), web busyEnter
|
|
20633
|
+
* parity — the accelerated chord uses the other behavior.
|
|
20634
|
+
* @param text - the submitted draft.
|
|
20635
|
+
* @param forceQueue - the chord: never steer, queue instead.
|
|
20636
|
+
*/
|
|
20637
|
+
const dispatchUserInput = (text, forceQueue = false) => {
|
|
20638
|
+
if (viewing !== void 0) {
|
|
20639
|
+
app.setEditorText(mergeDraft(app.getDraft(), text));
|
|
20640
|
+
app.notify("viewing a subagent — Esc returns before submitting", "info");
|
|
20641
|
+
return;
|
|
20642
|
+
}
|
|
20643
|
+
app.clearSettledLocalMessages();
|
|
20644
|
+
const history = app.getInputHistory();
|
|
20645
|
+
if (history.length > 0) {
|
|
20646
|
+
const settings = tuiSettings;
|
|
20647
|
+
if (settings !== void 0) runDetached("settings history write", () => settings.replace({
|
|
20648
|
+
...settings.get(),
|
|
20649
|
+
history: {
|
|
20650
|
+
...settings.get().history,
|
|
20651
|
+
[sessionCwd()]: history
|
|
20652
|
+
}
|
|
20653
|
+
}), {
|
|
20654
|
+
diag,
|
|
20655
|
+
notify: (message) => app.notify(message, "error"),
|
|
20656
|
+
recoverable: () => true
|
|
20657
|
+
});
|
|
20658
|
+
}
|
|
20659
|
+
if (text.startsWith("!")) {
|
|
20660
|
+
if (text.startsWith("!!")) runLocalShell(text);
|
|
20661
|
+
else if (shellCommandOf(text) !== "") runOwned("contextual shell", () => ensureSession().then(() => runLocalShell(text)), {
|
|
20662
|
+
diag,
|
|
20663
|
+
sessionId: () => liveAgent?.session.id,
|
|
20664
|
+
onError: failSubmission(text)
|
|
20665
|
+
});
|
|
20666
|
+
return;
|
|
20667
|
+
}
|
|
20668
|
+
const parsed = parseCommand(text);
|
|
20669
|
+
if (parsed !== void 0 && liveAgent === void 0 && SESSIONLESS_COMMANDS.has(parsed.name)) {
|
|
20670
|
+
runLocalCommand(parsed, text);
|
|
20671
|
+
return;
|
|
20672
|
+
}
|
|
20673
|
+
if (shouldSteerOnEnter(parsed, liveAgent?.status === "running", tuiSettings?.get().busyEnter, forceQueue)) {
|
|
20674
|
+
steerNow(text, true);
|
|
20675
|
+
return;
|
|
20676
|
+
}
|
|
20677
|
+
dispatchViaSession(text);
|
|
20678
|
+
};
|
|
18674
20679
|
app = startProcessTui({
|
|
18675
|
-
onSubmit: (text) =>
|
|
18676
|
-
|
|
18677
|
-
app.setEditorText(mergeDraft(app.getDraft(), text));
|
|
18678
|
-
app.notify("viewing a subagent — Esc returns before submitting", "info");
|
|
18679
|
-
return;
|
|
18680
|
-
}
|
|
18681
|
-
const history = app.getInputHistory();
|
|
18682
|
-
if (history.length > 0) {
|
|
18683
|
-
const settings = tuiSettings;
|
|
18684
|
-
if (settings !== void 0) runDetached("settings history write", () => settings.replace({
|
|
18685
|
-
...settings.get(),
|
|
18686
|
-
history: {
|
|
18687
|
-
...settings.get().history,
|
|
18688
|
-
[sessionCwd()]: history
|
|
18689
|
-
}
|
|
18690
|
-
}), {
|
|
18691
|
-
diag,
|
|
18692
|
-
notify: (message) => app.notify(message, "error"),
|
|
18693
|
-
recoverable: () => true
|
|
18694
|
-
});
|
|
18695
|
-
}
|
|
18696
|
-
if (text.startsWith("!")) {
|
|
18697
|
-
if (text.startsWith("!!")) runOwned("contextual shell", () => ensureSession().then(() => runLocalShell(text)), {
|
|
18698
|
-
diag,
|
|
18699
|
-
sessionId: () => liveAgent?.session.id,
|
|
18700
|
-
onError: failSubmission(text)
|
|
18701
|
-
});
|
|
18702
|
-
else runLocalShell(text);
|
|
18703
|
-
return;
|
|
18704
|
-
}
|
|
18705
|
-
const parsed = parseCommand(text);
|
|
18706
|
-
if (parsed !== void 0 && liveAgent === void 0 && SESSIONLESS_COMMANDS.has(parsed.name)) {
|
|
18707
|
-
runLocalCommand(parsed, text);
|
|
18708
|
-
return;
|
|
18709
|
-
}
|
|
18710
|
-
dispatchViaSession(text);
|
|
18711
|
-
},
|
|
20680
|
+
onSubmit: (text) => dispatchUserInput(text),
|
|
20681
|
+
onQueueSubmit: (text) => dispatchUserInput(text, true),
|
|
18712
20682
|
runOwned: (label, task, options) => {
|
|
18713
20683
|
runOwned(label, task, {
|
|
18714
20684
|
...options,
|
|
@@ -18724,52 +20694,7 @@ function apply(ctx, config) {
|
|
|
18724
20694
|
localShellController?.abort();
|
|
18725
20695
|
liveAgent?.cancel({ kind: "user" });
|
|
18726
20696
|
},
|
|
18727
|
-
onSteer: (text) =>
|
|
18728
|
-
if (viewing !== void 0) {
|
|
18729
|
-
if (text.trim() !== "") app.setEditorText(mergeDraft(app.getDraft(), text));
|
|
18730
|
-
app.notify("viewing a subagent — Esc returns before steering", "info");
|
|
18731
|
-
return;
|
|
18732
|
-
}
|
|
18733
|
-
if (liveAgent !== void 0) {
|
|
18734
|
-
if ([...liveAgent.inbox.nextTurn, ...liveAgent.inbox.nextStep].length === 0 && text.trim() === "") return;
|
|
18735
|
-
} else if (text.trim() === "") return;
|
|
18736
|
-
runOwned("steer", () => ensureSession().then(async () => {
|
|
18737
|
-
if (liveAgent === void 0) return;
|
|
18738
|
-
await steerAll({
|
|
18739
|
-
currentAgent: () => liveAgent,
|
|
18740
|
-
currentGeneration: () => sessionGeneration,
|
|
18741
|
-
guard: { run: async (identity) => {
|
|
18742
|
-
const verdict = await guardSend("save", identity);
|
|
18743
|
-
if (verdict.kind === "blocked") return {
|
|
18744
|
-
kind: "blocked",
|
|
18745
|
-
reason: verdict.reason
|
|
18746
|
-
};
|
|
18747
|
-
return { kind: verdict.kind === "forced" ? "forced" : "ok" };
|
|
18748
|
-
} },
|
|
18749
|
-
notify: (message, kind) => app.notify(message, kind),
|
|
18750
|
-
restoreDraft: (draft) => {
|
|
18751
|
-
const merged = mergeDraft(app.getDraft(), draft);
|
|
18752
|
-
app.setEditorText(merged);
|
|
18753
|
-
return merged === draft;
|
|
18754
|
-
},
|
|
18755
|
-
createDraft: (draft) => createUserMessage({
|
|
18756
|
-
content: [{
|
|
18757
|
-
type: "text",
|
|
18758
|
-
text: draft
|
|
18759
|
-
}],
|
|
18760
|
-
source: { kind: "user" }
|
|
18761
|
-
}),
|
|
18762
|
-
blockedNotice: (reason) => reason === "removed" ? GUARD_REMOVED_NOTIFY("save") : reason === "tail-mismatch" ? GUARD_TAIL_MISMATCH_NOTIFY("save") : GUARD_BLOCKED_NOTIFY("save"),
|
|
18763
|
-
forcedNotice: () => GUARD_FORCED_NOTIFY,
|
|
18764
|
-
staleNotice: () => "the queue or session changed while sending — try again",
|
|
18765
|
-
mergedNotice: () => "the draft changed while sending — review it before submitting again (the earlier text was preserved below)"
|
|
18766
|
-
}, text);
|
|
18767
|
-
}), {
|
|
18768
|
-
diag,
|
|
18769
|
-
sessionId: () => liveAgent?.session.id,
|
|
18770
|
-
onError: failSubmission(text)
|
|
18771
|
-
});
|
|
18772
|
-
},
|
|
20697
|
+
onSteer: (text) => steerNow(text),
|
|
18773
20698
|
openExternalEditor: async (draft) => {
|
|
18774
20699
|
const [editor, ...editorArgs] = parseShellWords(process.env.VISUAL ?? process.env.EDITOR ?? "vi");
|
|
18775
20700
|
if (editor === void 0) throw new Error("empty editor command");
|
|
@@ -18858,31 +20783,50 @@ function apply(ctx, config) {
|
|
|
18858
20783
|
refreshQueue();
|
|
18859
20784
|
},
|
|
18860
20785
|
onOpenTasks: () => {
|
|
18861
|
-
if (
|
|
18862
|
-
let
|
|
18863
|
-
try {
|
|
18864
|
-
|
|
18865
|
-
} catch {
|
|
18866
|
-
return;
|
|
18867
|
-
}
|
|
18868
|
-
if (snapshots.length === 0) return;
|
|
20786
|
+
if (liveAgent === void 0) return;
|
|
20787
|
+
let jobSnapshots = [];
|
|
20788
|
+
if (jobs !== void 0) try {
|
|
20789
|
+
jobSnapshots = jobs.list(liveAgent);
|
|
20790
|
+
} catch {}
|
|
18869
20791
|
const now = Date.now();
|
|
18870
|
-
|
|
18871
|
-
const
|
|
18872
|
-
const
|
|
18873
|
-
if (
|
|
18874
|
-
if (
|
|
18875
|
-
|
|
18876
|
-
|
|
18877
|
-
|
|
18878
|
-
|
|
18879
|
-
|
|
18880
|
-
|
|
18881
|
-
|
|
18882
|
-
|
|
20792
|
+
let rows = buildTaskRows(jobSnapshots, []);
|
|
20793
|
+
const selectRow = (value) => {
|
|
20794
|
+
const row = rows.find((candidate) => candidate.value === value);
|
|
20795
|
+
if (row === void 0) return;
|
|
20796
|
+
if (row.kind === "subagent") {
|
|
20797
|
+
runOwned("subagent view from tasks", () => enterView(row.childId, row.label), {
|
|
20798
|
+
diag,
|
|
20799
|
+
sessionId: () => liveAgent?.session.id,
|
|
20800
|
+
onError: (error) => app.notify(`could not open the subagent view: ${safeErrorMessage(error)}`, "error")
|
|
20801
|
+
});
|
|
20802
|
+
return;
|
|
20803
|
+
}
|
|
20804
|
+
openJobView(row.jobId);
|
|
20805
|
+
};
|
|
20806
|
+
const pickerItems = (target) => target.map((row) => ({
|
|
20807
|
+
value: row.value,
|
|
20808
|
+
label: taskRowLabel(row),
|
|
20809
|
+
description: describeTaskRow(row, now),
|
|
20810
|
+
group: rowGroup(row)
|
|
20811
|
+
}));
|
|
20812
|
+
const handle = app.openPicker(pickerItems(rows), selectRow, () => {}, {
|
|
20813
|
+
header: "tasks · subagents",
|
|
18883
20814
|
enableSearch: true,
|
|
18884
20815
|
showHint: true
|
|
18885
20816
|
});
|
|
20817
|
+
if (subagents !== void 0) {
|
|
20818
|
+
const sessionId = liveAgent.session.id;
|
|
20819
|
+
const generation = sessionGeneration;
|
|
20820
|
+
runOwned("task browser children", () => subagents.listChildren(sessionId), {
|
|
20821
|
+
diag,
|
|
20822
|
+
sessionId: () => liveAgent?.session.id,
|
|
20823
|
+
onResult: (entries) => {
|
|
20824
|
+
if (sessionGeneration !== generation || liveAgent?.session.id !== sessionId) return;
|
|
20825
|
+
rows = buildTaskRows(jobSnapshots, entries);
|
|
20826
|
+
handle.setItems(pickerItems(rows));
|
|
20827
|
+
}
|
|
20828
|
+
});
|
|
20829
|
+
}
|
|
18886
20830
|
}
|
|
18887
20831
|
}, {
|
|
18888
20832
|
present,
|
|
@@ -18892,28 +20836,35 @@ function apply(ctx, config) {
|
|
|
18892
20836
|
theme: z.string(),
|
|
18893
20837
|
footer: z.string(),
|
|
18894
20838
|
fullscreen: z.string(),
|
|
20839
|
+
busyEnter: z.string(),
|
|
18895
20840
|
history: z.dict(z.array(z.string()))
|
|
18896
20841
|
}), { base: {
|
|
18897
20842
|
theme: "auto",
|
|
18898
20843
|
footer: "full",
|
|
18899
20844
|
fullscreen: "on",
|
|
20845
|
+
busyEnter: "queue",
|
|
18900
20846
|
history: {}
|
|
18901
20847
|
} });
|
|
20848
|
+
if (tuiSettings?.get().fullscreen === "on") app.setFullscreen(true);
|
|
18902
20849
|
const storedTheme = tuiSettings?.get().theme;
|
|
18903
20850
|
if (storedTheme === "auto") {
|
|
18904
|
-
runDetached("theme autodetect", () => app.autoDetectTheme(), { diag });
|
|
20851
|
+
runDetached("theme autodetect", () => app.autoDetectTheme({ shouldApply: () => tuiSettings?.get().theme === "auto" }), { diag });
|
|
18905
20852
|
app.onTerminalThemeChange((theme) => {
|
|
18906
20853
|
if (tuiSettings?.get().theme === "auto") app.applyTheme(theme);
|
|
18907
20854
|
});
|
|
18908
|
-
|
|
18909
|
-
else if (storedTheme
|
|
20855
|
+
app.trackTerminalTheme(true);
|
|
20856
|
+
} else if (storedTheme === "dark" || storedTheme === "light") {
|
|
20857
|
+
app.applyTheme(storedTheme);
|
|
20858
|
+
app.trackTerminalTheme(false);
|
|
20859
|
+
} else if (storedTheme?.startsWith("custom:")) {
|
|
18910
20860
|
const palette = loadCustomTheme(storedTheme.slice(7));
|
|
18911
20861
|
if (palette !== void 0) app.applyPalette(palette);
|
|
20862
|
+
app.trackTerminalTheme(false);
|
|
18912
20863
|
}
|
|
18913
20864
|
if (tuiSettings?.get().footer === "compact") app.setFooterPreset("compact");
|
|
18914
|
-
if (tuiSettings?.get().fullscreen === "on") app.setFullscreen(true);
|
|
18915
20865
|
refreshStatus();
|
|
18916
20866
|
let refreshTasks = () => {};
|
|
20867
|
+
let refreshAgents = () => {};
|
|
18917
20868
|
const jobs = ctx.get("jobs");
|
|
18918
20869
|
if (jobs !== void 0) {
|
|
18919
20870
|
refreshTasks = () => {
|
|
@@ -18928,9 +20879,36 @@ function apply(ctx, config) {
|
|
|
18928
20879
|
} catch {}
|
|
18929
20880
|
app.setTasks(tasks);
|
|
18930
20881
|
};
|
|
18931
|
-
jobs.onJobsChanged(() =>
|
|
20882
|
+
jobs.onJobsChanged(() => {
|
|
20883
|
+
refreshTasks();
|
|
20884
|
+
refreshAgents();
|
|
20885
|
+
});
|
|
18932
20886
|
refreshTasks();
|
|
18933
20887
|
}
|
|
20888
|
+
const subagents = ctx.get("subagents");
|
|
20889
|
+
if (subagents !== void 0) {
|
|
20890
|
+
refreshAgents = () => {
|
|
20891
|
+
if (liveAgent === void 0) {
|
|
20892
|
+
app.setAgents([]);
|
|
20893
|
+
return;
|
|
20894
|
+
}
|
|
20895
|
+
const sessionId = liveAgent.session.id;
|
|
20896
|
+
const generation = sessionGeneration;
|
|
20897
|
+
runOwned("task browser agents refresh", () => subagents.listChildren(sessionId), {
|
|
20898
|
+
diag,
|
|
20899
|
+
sessionId: () => liveAgent?.session.id,
|
|
20900
|
+
onResult: (entries) => {
|
|
20901
|
+
if (sessionGeneration !== generation || liveAgent?.session.id !== sessionId) return;
|
|
20902
|
+
app.setAgents(entries.filter((entry) => entry.kind === "child" && entry.activity === "running").map((entry) => ({
|
|
20903
|
+
id: entry.id,
|
|
20904
|
+
label: entry.label ?? entry.id,
|
|
20905
|
+
activity: entry.activity
|
|
20906
|
+
})));
|
|
20907
|
+
}
|
|
20908
|
+
});
|
|
20909
|
+
};
|
|
20910
|
+
refreshAgents();
|
|
20911
|
+
}
|
|
18934
20912
|
/**
|
|
18935
20913
|
* Open one job from the task browser: a bash job shows a STATUS viewer
|
|
18936
20914
|
* (never the output — the job's single read cursor belongs to the agent's
|
|
@@ -19018,7 +20996,12 @@ function apply(ctx, config) {
|
|
|
19018
20996
|
app.setQueueItems(queue);
|
|
19019
20997
|
};
|
|
19020
20998
|
refreshQueue();
|
|
19021
|
-
/**
|
|
20999
|
+
/**
|
|
21000
|
+
* Rebuild every live-session surface after resume, create, or swap.
|
|
21001
|
+
* The surface catalog is NOT touched here: the initial owner's catalog
|
|
21002
|
+
* came from the pre-mount prefetch/probe, and the first deferred create
|
|
21003
|
+
* plus every switch await the coordinator refresh themselves.
|
|
21004
|
+
*/
|
|
19022
21005
|
const initLiveSession = async (agent) => {
|
|
19023
21006
|
folder = new TranscriptFolder();
|
|
19024
21007
|
folder.apply(agent.session.events);
|
|
@@ -19032,11 +21015,15 @@ function apply(ctx, config) {
|
|
|
19032
21015
|
repaint(app, folder);
|
|
19033
21016
|
refreshStatus();
|
|
19034
21017
|
refreshQueue();
|
|
21018
|
+
refreshTasks();
|
|
21019
|
+
refreshAgents();
|
|
19035
21020
|
app.resetInputHistory(tuiSettings?.get().history[sessionCwd()] ?? []);
|
|
19036
21021
|
setTerminalTitle(`dsh-pi-tui · ${shortCwd(sessionCwd())} · ${agent.session.id}`);
|
|
19037
21022
|
updateWelcomeCard();
|
|
19038
|
-
registerCommands(
|
|
19039
|
-
|
|
21023
|
+
registerCommands({
|
|
21024
|
+
snapshot: initialSnapshot,
|
|
21025
|
+
skills: initialSkills
|
|
21026
|
+
});
|
|
19040
21027
|
};
|
|
19041
21028
|
/**
|
|
19042
21029
|
* Create the first session lazily — the FIRST user message triggers it
|
|
@@ -19065,6 +21052,7 @@ function apply(ctx, config) {
|
|
|
19065
21052
|
await liveAgent.whenIdle();
|
|
19066
21053
|
bumpSessionGeneration();
|
|
19067
21054
|
await initLiveSession(liveAgent);
|
|
21055
|
+
await refreshLiveCatalog(liveAgent);
|
|
19068
21056
|
} catch (error) {
|
|
19069
21057
|
const message = safeErrorMessage(error);
|
|
19070
21058
|
ctx.logger.warn(`tui-runner: failed to start with preset "${launchPreset ?? "default"}": ${message}`);
|
|
@@ -19088,6 +21076,7 @@ function apply(ctx, config) {
|
|
|
19088
21076
|
await liveAgent.whenIdle();
|
|
19089
21077
|
bumpSessionGeneration();
|
|
19090
21078
|
await initLiveSession(liveAgent);
|
|
21079
|
+
await refreshLiveCatalog(liveAgent);
|
|
19091
21080
|
}
|
|
19092
21081
|
if (resumeFailure !== void 0) {
|
|
19093
21082
|
app.notify(resumeFailure, "error");
|
|
@@ -19099,7 +21088,67 @@ function apply(ctx, config) {
|
|
|
19099
21088
|
return creating;
|
|
19100
21089
|
};
|
|
19101
21090
|
let commandsRegistered = false;
|
|
19102
|
-
|
|
21091
|
+
/** The claim test installed by registerTuiCommands: is a slash name
|
|
21092
|
+
* advertised by the CURRENT completion list? The dispatch captures it
|
|
21093
|
+
* BEFORE any session creation (see dispatchViaSession). */
|
|
21094
|
+
let wasAdvertisedClaim;
|
|
21095
|
+
/** The catalog refresh coordinator: the ONE post-mount refresh owner
|
|
21096
|
+
* (first session, switches, /preset, /reload). Built inside
|
|
21097
|
+
* registerCommands once the surface hooks exist. */
|
|
21098
|
+
let catalogRefreshRequest;
|
|
21099
|
+
let catalogCoordinator;
|
|
21100
|
+
/** `skills/change` coalescing: bursts of invalidation notifications cost
|
|
21101
|
+
* at most two reads, and the follow-up re-read observes the CURRENT
|
|
21102
|
+
* ownership (live agent vs standing preset). */
|
|
21103
|
+
let skillsChangeSubscribed = false;
|
|
21104
|
+
const skillsChangeGate = new CoalescingRefreshGate(() => {
|
|
21105
|
+
runOwned("skills/change refresh", async () => {
|
|
21106
|
+
const refresh = catalogRefreshRequest;
|
|
21107
|
+
if (refresh === void 0) return void 0;
|
|
21108
|
+
const target = liveAgent === void 0 ? {
|
|
21109
|
+
kind: "preset",
|
|
21110
|
+
presetId: pendingPreset ?? launchPreset
|
|
21111
|
+
} : {
|
|
21112
|
+
kind: "agent",
|
|
21113
|
+
key: sessionGeneration
|
|
21114
|
+
};
|
|
21115
|
+
return refresh({
|
|
21116
|
+
source: "invalidation",
|
|
21117
|
+
target,
|
|
21118
|
+
...target.kind === "agent" ? { agent: liveAgent } : {}
|
|
21119
|
+
});
|
|
21120
|
+
}, {
|
|
21121
|
+
diag,
|
|
21122
|
+
sessionId: () => liveAgent?.session.id,
|
|
21123
|
+
onResult: (outcome) => {
|
|
21124
|
+
if (outcome !== void 0 && outcome.kind === "applied" && outcome.notice !== void 0) app.notify(outcome.notice, "error");
|
|
21125
|
+
skillsChangeGate.settled();
|
|
21126
|
+
},
|
|
21127
|
+
onCancel: () => {
|
|
21128
|
+
skillsChangeGate.settled();
|
|
21129
|
+
},
|
|
21130
|
+
onError: (error) => {
|
|
21131
|
+
skillsChangeGate.settled();
|
|
21132
|
+
app.notify(`skill catalog refresh failed: ${safeErrorMessage(error)}`, "error");
|
|
21133
|
+
}
|
|
21134
|
+
});
|
|
21135
|
+
});
|
|
21136
|
+
/** Subscribe to the dsh-skill invalidation notification once, through
|
|
21137
|
+
* the single-point adapter (plan appendix B.1). The event carries no
|
|
21138
|
+
* scope or cwd, so the refresh target follows the CURRENT ownership; an
|
|
21139
|
+
* unavailable or throwing subscription degrades to no subscription —
|
|
21140
|
+
* owner switches and /reload still refresh. The flag is set only after
|
|
21141
|
+
* a successful subscribe, so a throwing subscribe can retry on a later
|
|
21142
|
+
* registration attempt. */
|
|
21143
|
+
const subscribeSkillsChangeEvents = () => {
|
|
21144
|
+
if (skillsChangeSubscribed) return;
|
|
21145
|
+
try {
|
|
21146
|
+
subscribeSkillsChange(ctx, () => skillsChangeGate.notify());
|
|
21147
|
+
skillsChangeSubscribed = true;
|
|
21148
|
+
} catch (error) {
|
|
21149
|
+
diag.warn("skills/change subscription unavailable", { error: safeErrorMessage(error) });
|
|
21150
|
+
}
|
|
21151
|
+
};
|
|
19103
21152
|
const runner = {
|
|
19104
21153
|
ctx,
|
|
19105
21154
|
app,
|
|
@@ -19127,6 +21176,26 @@ function apply(ctx, config) {
|
|
|
19127
21176
|
return sessionGeneration;
|
|
19128
21177
|
},
|
|
19129
21178
|
compose,
|
|
21179
|
+
get pendingPreset() {
|
|
21180
|
+
return pendingPreset;
|
|
21181
|
+
},
|
|
21182
|
+
set pendingPreset(id) {
|
|
21183
|
+
pendingPreset = id;
|
|
21184
|
+
},
|
|
21185
|
+
/** The effective preset id for COLD (sessionless) reads: the run-local
|
|
21186
|
+
* pending override ahead of the launch-time --preset (the SAME
|
|
21187
|
+
* precedence ensureSession uses); undefined = the saved/default
|
|
21188
|
+
* preset applies. */
|
|
21189
|
+
get effectivePresetId() {
|
|
21190
|
+
return pendingPreset ?? launchPreset;
|
|
21191
|
+
},
|
|
21192
|
+
refreshCatalog: (request) => {
|
|
21193
|
+
const refresh = catalogRefreshRequest;
|
|
21194
|
+
return refresh === void 0 ? Promise.resolve({
|
|
21195
|
+
kind: "failed",
|
|
21196
|
+
error: "catalog refresh unavailable"
|
|
21197
|
+
}) : refresh(request);
|
|
21198
|
+
},
|
|
19130
21199
|
switchSession,
|
|
19131
21200
|
swapTo: (next) => swapTo(next),
|
|
19132
21201
|
currentPreset,
|
|
@@ -19138,12 +21207,32 @@ function apply(ctx, config) {
|
|
|
19138
21207
|
requestExit,
|
|
19139
21208
|
exit
|
|
19140
21209
|
};
|
|
19141
|
-
const registerCommands = () => {
|
|
21210
|
+
const registerCommands = (initial) => {
|
|
19142
21211
|
if (commandsRegistered) return;
|
|
19143
21212
|
if (ctx.get("commands") === void 0) return;
|
|
19144
21213
|
commandsRegistered = true;
|
|
19145
21214
|
try {
|
|
19146
|
-
|
|
21215
|
+
const installed = registerTuiCommands(runner, initial);
|
|
21216
|
+
wasAdvertisedClaim = installed.wasAdvertised;
|
|
21217
|
+
catalogCoordinator = new CatalogRefreshCoordinator({
|
|
21218
|
+
readAgent: (agent, readSignal) => readSurfaceCatalog(agent, readSignal, ctx),
|
|
21219
|
+
readStanding: async (presetId, readSignal) => {
|
|
21220
|
+
const target = await resolveColdSkillTarget(ctx, presetId, process.cwd());
|
|
21221
|
+
if (target.target === void 0) throw new Error("skill service unavailable");
|
|
21222
|
+
return {
|
|
21223
|
+
catalog: await readHumanSkillCatalog(target.target.registry, {
|
|
21224
|
+
cwd: target.target.cwd,
|
|
21225
|
+
scope: target.target.scope,
|
|
21226
|
+
signal: readSignal
|
|
21227
|
+
}),
|
|
21228
|
+
...target.degraded === void 0 ? {} : { notice: target.degraded }
|
|
21229
|
+
};
|
|
21230
|
+
},
|
|
21231
|
+
installSnapshot: (next) => installed.installSnapshot(next),
|
|
21232
|
+
enterCatalogTransition: () => installed.enterTransition()
|
|
21233
|
+
}, lifecycleController.signal, diag);
|
|
21234
|
+
catalogRefreshRequest = (request) => catalogCoordinator.refresh(request);
|
|
21235
|
+
subscribeSkillsChangeEvents();
|
|
19147
21236
|
} catch (error) {
|
|
19148
21237
|
commandsRegistered = false;
|
|
19149
21238
|
const message = safeErrorMessage(error);
|
|
@@ -19152,13 +21241,21 @@ function apply(ctx, config) {
|
|
|
19152
21241
|
app.notify(`command registration failed: ${message}`, "error");
|
|
19153
21242
|
}
|
|
19154
21243
|
};
|
|
19155
|
-
/**
|
|
19156
|
-
|
|
19157
|
-
|
|
21244
|
+
/** Await one live-owner catalog refresh through the coordinator (the
|
|
21245
|
+
* first deferred create and every session switch): the refresh attempt
|
|
21246
|
+
* settles before the caller continues, and its outcome is an outcome —
|
|
21247
|
+
* provider issues degrade fields, failures warn, the submission or the
|
|
21248
|
+
* switch proceeds either way. */
|
|
21249
|
+
const refreshLiveCatalog = async (agent) => {
|
|
21250
|
+
const refresh = catalogRefreshRequest;
|
|
19158
21251
|
if (refresh === void 0) return;
|
|
19159
|
-
|
|
19160
|
-
|
|
19161
|
-
|
|
21252
|
+
await refresh({
|
|
21253
|
+
source: "live-session",
|
|
21254
|
+
target: {
|
|
21255
|
+
kind: "agent",
|
|
21256
|
+
key: sessionGeneration
|
|
21257
|
+
},
|
|
21258
|
+
agent
|
|
19162
21259
|
});
|
|
19163
21260
|
};
|
|
19164
21261
|
if (liveAgent !== void 0) await initLiveSession(liveAgent);
|
|
@@ -19167,7 +21264,14 @@ function apply(ctx, config) {
|
|
|
19167
21264
|
refreshStatus();
|
|
19168
21265
|
setTerminalTitle(`dsh-pi-tui · ${shortCwd(sessionCwd())}`);
|
|
19169
21266
|
}
|
|
19170
|
-
registerCommands(
|
|
21267
|
+
registerCommands({
|
|
21268
|
+
snapshot: initialSnapshot,
|
|
21269
|
+
skills: initialSkills
|
|
21270
|
+
});
|
|
21271
|
+
if (surfaceNotice !== void 0) {
|
|
21272
|
+
app.notify(surfaceNotice, "error");
|
|
21273
|
+
surfaceNotice = void 0;
|
|
21274
|
+
}
|
|
19171
21275
|
if (resumeFailure !== void 0) {
|
|
19172
21276
|
app.notify(resumeFailure, "error");
|
|
19173
21277
|
resumeFailure = void 0;
|
|
@@ -19182,8 +21286,10 @@ function apply(ctx, config) {
|
|
|
19182
21286
|
return;
|
|
19183
21287
|
}
|
|
19184
21288
|
if (session.id !== liveAgent.session.id) return;
|
|
19185
|
-
if (event.type === "tool/call")
|
|
19186
|
-
|
|
21289
|
+
if (event.type === "tool/call") {
|
|
21290
|
+
callArgs.set(event.data.callId, typeof event.data.arguments === "string" ? event.data.arguments : JSON.stringify(event.data.arguments));
|
|
21291
|
+
if (typeof event.data.name === "string" && event.data.name.startsWith("subagent")) refreshAgents();
|
|
21292
|
+
} else if (event.type === "tool/result") callArgs.delete(event.data.message.content[0]?.toolCallId ?? "");
|
|
19187
21293
|
folder.apply([event]);
|
|
19188
21294
|
statsFolder.apply([event]);
|
|
19189
21295
|
if (event.type === "goal/change") goalText = foldGoal([event]);
|
|
@@ -19214,6 +21320,8 @@ function apply(ctx, config) {
|
|
|
19214
21320
|
});
|
|
19215
21321
|
} else if (event.type === "step/start") refreshStatus();
|
|
19216
21322
|
});
|
|
21323
|
+
ctx.on("subagent/start", () => refreshAgents());
|
|
21324
|
+
ctx.on("subagent/end", () => refreshAgents());
|
|
19217
21325
|
if (liveAgent !== void 0) {
|
|
19218
21326
|
app.setPlanMode(foldPlanMode(liveAgent.session.events));
|
|
19219
21327
|
app.setWorking(workingFromLog(liveAgent.session.events));
|
|
@@ -19273,4 +21381,4 @@ function apply(ctx, config) {
|
|
|
19273
21381
|
});
|
|
19274
21382
|
}
|
|
19275
21383
|
//#endregion
|
|
19276
|
-
export { Config, apply, composeAgent, dangerCommand, forkSeed, inject, name, recomposeBlank, recordedPreset, resumeCommand, runningProfile, subagentJobTranscriptId, subagentJobViewHint };
|
|
21384
|
+
export { Config, LOCAL_COMMANDS, SESSIONLESS_COMMANDS, apply, composeAgent, dangerCommand, forkSeed, inject, name, recomposeBlank, recordedPreset, resolveInitialCatalog, resumeCommand, runningProfile, shouldConsumeAdvertisedMiss, shouldSteerOnEnter, subagentJobTranscriptId, subagentJobViewHint };
|