@xmoon76/dsh-pi-tui 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.d.mts +38 -1
- package/dist/index.mjs +1339 -282
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -12607,25 +12607,36 @@ var TranscriptSearchComponent = class {
|
|
|
12607
12607
|
//#endregion
|
|
12608
12608
|
//#region src/question.ts
|
|
12609
12609
|
/**
|
|
12610
|
-
* The user-questions dialog flow (ask_user_question v2):
|
|
12611
|
-
*
|
|
12612
|
-
*
|
|
12613
|
-
*
|
|
12614
|
-
*
|
|
12610
|
+
* The user-questions dialog flow (ask_user_question v2): the component that
|
|
12611
|
+
* sits in the EDITOR SEAT (kimi's mountEditorReplacement pattern) while the
|
|
12612
|
+
* agent asks — one question at a time, drafts per question, whole batch
|
|
12613
|
+
* submitted at the end — the Web QuestionComposer semantics (per-question
|
|
12614
|
+
* paging, batch submit, skip) with pi/kimi keyboard ergonomics (↑↓
|
|
12615
|
+
* highlight, digits, real Input for free text, review page).
|
|
12615
12616
|
*
|
|
12616
|
-
* Pure component: the app layer owns the promise/abort plumbing
|
|
12617
|
-
* input here while the flow is active.
|
|
12617
|
+
* Pure component: the app layer owns the promise/abort plumbing, the seat
|
|
12618
|
+
* swap, and routes input here while the flow is active.
|
|
12618
12619
|
* @module @xmoon76/dsh-pi-tui/question
|
|
12619
12620
|
*/
|
|
12620
12621
|
/** The "type your own answer" row shown below options (pi isOther parity). */
|
|
12621
12622
|
const OTHER_ROW = "\0other";
|
|
12622
12623
|
/**
|
|
12623
|
-
*
|
|
12624
|
-
*
|
|
12625
|
-
*
|
|
12626
|
-
*
|
|
12624
|
+
* Default total physical-row budget of the question flow itself. The
|
|
12625
|
+
* QuestionFrame in tui-app.ts re-derives the budget from the terminal height
|
|
12626
|
+
* on every render (60% cap, 8..24 content rows) and pushes it through
|
|
12627
|
+
* {@link QuestionFlow.setMaxRows}; 24 is the fallback for direct renders.
|
|
12628
|
+
* The Frame wrapper adds its two border rows, and NOTHING clips the flow's
|
|
12629
|
+
* output in the editor-seat layout — so EVERYTHING (tabs, question, detail,
|
|
12630
|
+
* options with their descriptions, scroll marker, skipped note, hint) must
|
|
12631
|
+
* fit in the budget.
|
|
12627
12632
|
*/
|
|
12628
|
-
const
|
|
12633
|
+
const DEFAULT_BUDGET = 24;
|
|
12634
|
+
/**
|
|
12635
|
+
* Smallest supported content-row budget (a 16-row terminal caps the frame at
|
|
12636
|
+
* 10 rows). Below this the required rows (tabs, the question's first row,
|
|
12637
|
+
* the current option, the hint) cannot all coexist.
|
|
12638
|
+
*/
|
|
12639
|
+
const MIN_SUPPORTED_BUDGET = 8;
|
|
12629
12640
|
/**
|
|
12630
12641
|
* Total physical-row budget for the question body (question text + detail),
|
|
12631
12642
|
* shared so a single unbroken long line cannot push the options and hints
|
|
@@ -12658,14 +12669,15 @@ function appendWrappedBudgeted(lines, firstPrefix, continuationPrefix, content,
|
|
|
12658
12669
|
return Math.max(0, budget - used);
|
|
12659
12670
|
}
|
|
12660
12671
|
/**
|
|
12661
|
-
*
|
|
12662
|
-
*
|
|
12663
|
-
*
|
|
12664
|
-
*
|
|
12665
|
-
*
|
|
12672
|
+
* Required-first push: the FIRST physical row always carries `content` —
|
|
12673
|
+
* never just a cut marker — while extra rows fill `budget` and a `... N
|
|
12674
|
+
* more` marker reports content that was itself cut, only when a row remains
|
|
12675
|
+
* after the content. Used for the question body and the highlighted option
|
|
12676
|
+
* label, whose first rows may never vanish (a 1-row budget shows the
|
|
12677
|
+
* content, not the marker).
|
|
12666
12678
|
* @returns the remaining budget (≥ 0).
|
|
12667
12679
|
*/
|
|
12668
|
-
function
|
|
12680
|
+
function appendContentFirst(lines, firstPrefix, continuationPrefix, content, width, budget) {
|
|
12669
12681
|
if (budget <= 0) return 0;
|
|
12670
12682
|
const prefixWidth = Math.max(visibleWidth(firstPrefix), visibleWidth(continuationPrefix));
|
|
12671
12683
|
const wrapped = wrapTextWithAnsi(content, Math.max(1, width - prefixWidth));
|
|
@@ -12723,7 +12735,14 @@ var QuestionFlow = class {
|
|
|
12723
12735
|
otherInput = new Input();
|
|
12724
12736
|
/** Review-page action highlight (0 = Submit, 1 = Cancel). */
|
|
12725
12737
|
submitIdx = 0;
|
|
12726
|
-
|
|
12738
|
+
/**
|
|
12739
|
+
* Current content-row budget (8..24). The editor-seat QuestionFrame in
|
|
12740
|
+
* tui-app.ts re-derives it from the terminal height on every render and
|
|
12741
|
+
* pushes it through {@link setMaxRows}; nothing clips the flow's output
|
|
12742
|
+
* in that layout, so every render must fit the budget exactly.
|
|
12743
|
+
*/
|
|
12744
|
+
budget = DEFAULT_BUDGET;
|
|
12745
|
+
_focused = false;
|
|
12727
12746
|
constructor(questions, onDone, onCancel) {
|
|
12728
12747
|
this.questions = questions;
|
|
12729
12748
|
this.onDone = onDone;
|
|
@@ -12737,6 +12756,28 @@ var QuestionFlow = class {
|
|
|
12737
12756
|
this.otherInput.onEscape = () => this.exitOther();
|
|
12738
12757
|
this.syncEditMode();
|
|
12739
12758
|
}
|
|
12759
|
+
/**
|
|
12760
|
+
* Focus mirror for the editor-seat layout: the app focuses the wrapping
|
|
12761
|
+
* QuestionFrame, which forwards here. The free-text Input renders its
|
|
12762
|
+
* hardware cursor only while focused AND editing, so the mirror must sync
|
|
12763
|
+
* it — a plain field would leave the cursor permanently hidden.
|
|
12764
|
+
*/
|
|
12765
|
+
get focused() {
|
|
12766
|
+
return this._focused;
|
|
12767
|
+
}
|
|
12768
|
+
set focused(value) {
|
|
12769
|
+
this._focused = value;
|
|
12770
|
+
if (this.editingOther) this.otherInput.focused = value;
|
|
12771
|
+
}
|
|
12772
|
+
/**
|
|
12773
|
+
* Replace the content-row budget (the Frame wrapper adds its two border
|
|
12774
|
+
* rows on top). Called by QuestionFrame on every render immediately
|
|
12775
|
+
* before the flow renders, so no invalidation is needed; values outside
|
|
12776
|
+
* [MIN_SUPPORTED_BUDGET, DEFAULT_BUDGET] are clamped.
|
|
12777
|
+
*/
|
|
12778
|
+
setMaxRows(rows) {
|
|
12779
|
+
this.budget = Math.max(MIN_SUPPORTED_BUDGET, Math.min(DEFAULT_BUDGET, Math.floor(rows)));
|
|
12780
|
+
}
|
|
12740
12781
|
/** The rows of the current question (options plus the free-text row). */
|
|
12741
12782
|
rows() {
|
|
12742
12783
|
const question = this.questions[this.tab];
|
|
@@ -12983,7 +13024,7 @@ var QuestionFlow = class {
|
|
|
12983
13024
|
lines.push(tabs.join(" "));
|
|
12984
13025
|
lines.push("");
|
|
12985
13026
|
if (this.tab >= this.questions.length) {
|
|
12986
|
-
let reviewBudget =
|
|
13027
|
+
let reviewBudget = this.budget - lines.length - 2;
|
|
12987
13028
|
reviewBudget = appendWrappedBudgeted(lines, "", "", color.textStrong("Review your answer before submit"), safeWidth, reviewBudget);
|
|
12988
13029
|
if (reviewBudget > 0) {
|
|
12989
13030
|
lines.push("");
|
|
@@ -13010,31 +13051,37 @@ var QuestionFlow = class {
|
|
|
13010
13051
|
const question = this.questions[this.tab];
|
|
13011
13052
|
const draft = this.draft();
|
|
13012
13053
|
if (question === void 0 || draft === void 0) return lines;
|
|
13013
|
-
|
|
13014
|
-
|
|
13015
|
-
|
|
13054
|
+
const rows = this.rows();
|
|
13055
|
+
const multi = question.multiSelect === true;
|
|
13056
|
+
this.editingOther && rows.some((row) => row.key === OTHER_ROW);
|
|
13057
|
+
const skippedRow = draft.skipped ? 1 : 0;
|
|
13058
|
+
const optionless = rows.length === 0 || rows.length === 1 && rows[0]?.key === OTHER_ROW;
|
|
13059
|
+
const tail = optionless ? 3 + skippedRow : 4 + skippedRow;
|
|
13060
|
+
let bodyAllowance = this.budget - lines.length - tail;
|
|
13061
|
+
const header = question.header;
|
|
13062
|
+
if (header !== void 0 && header !== "" && bodyAllowance >= 2) {
|
|
13063
|
+
lines.push(color.textDim(header));
|
|
13064
|
+
bodyAllowance -= 1;
|
|
13065
|
+
}
|
|
13066
|
+
const bodyBudget = Math.min(MAX_BODY_LINES, Math.max(1, bodyAllowance));
|
|
13067
|
+
let bodyLeft = appendContentFirst(lines, `${color.primary("?")} `, " ", color.textStrong(question.question), safeWidth, bodyBudget);
|
|
13016
13068
|
if (question.detail !== void 0 && question.detail !== "") for (const line of question.detail.split("\n")) {
|
|
13017
|
-
if (
|
|
13018
|
-
appendWrappedBudgeted(lines, " ", " ", color.textDim("... more content hidden"), safeWidth,
|
|
13069
|
+
if (bodyLeft <= 1) {
|
|
13070
|
+
appendWrappedBudgeted(lines, " ", " ", color.textDim("... more content hidden"), safeWidth, bodyLeft);
|
|
13019
13071
|
break;
|
|
13020
13072
|
}
|
|
13021
|
-
|
|
13073
|
+
bodyLeft = appendWrappedBudgeted(lines, " ", " ", color.textDim(line), safeWidth, bodyLeft);
|
|
13022
13074
|
}
|
|
13023
|
-
|
|
13024
|
-
const multi = question.multiSelect === true;
|
|
13025
|
-
this.editingOther && rows.some((row) => row.key === OTHER_ROW);
|
|
13026
|
-
if (rows.length > 0 && !(rows.length === 1 && rows[0]?.key === OTHER_ROW)) {
|
|
13075
|
+
if (!optionless) {
|
|
13027
13076
|
lines.push("");
|
|
13028
|
-
const
|
|
13029
|
-
const skippedRow = draft.skipped ? 1 : 0;
|
|
13030
|
-
let optionBudget = DIALOG_BUDGET - lines.length - 2 - showingRow - skippedRow;
|
|
13077
|
+
const optionBudget = this.budget - lines.length - skippedRow - 2;
|
|
13031
13078
|
const maxWindow = Math.max(1, Math.min(MAX_VISIBLE_OPTIONS, optionBudget));
|
|
13032
13079
|
const visibleCount = Math.min(rows.length, maxWindow);
|
|
13033
13080
|
const half = Math.floor(maxWindow / 2);
|
|
13034
13081
|
const maxStart = Math.max(0, rows.length - visibleCount);
|
|
13035
13082
|
const start = Math.max(0, Math.min(this.cursor - half, maxStart));
|
|
13036
13083
|
const end = Math.min(rows.length, start + visibleCount);
|
|
13037
|
-
|
|
13084
|
+
{
|
|
13038
13085
|
const cursorRow = rows[this.cursor];
|
|
13039
13086
|
let reserved = 1;
|
|
13040
13087
|
if (cursorRow !== void 0 && cursorRow.key !== OTHER_ROW) {
|
|
@@ -13066,7 +13113,7 @@ var QuestionFlow = class {
|
|
|
13066
13113
|
const indent = " ".repeat(visibleWidth(prefix));
|
|
13067
13114
|
const available = isCursor || cursorRendered ? left : left - reserved;
|
|
13068
13115
|
const beforeLabel = lines.length;
|
|
13069
|
-
if (isCursor)
|
|
13116
|
+
if (isCursor) appendContentFirst(lines, prefix, indent, `${label}${badge}`, safeWidth, available);
|
|
13070
13117
|
else appendWrappedBudgeted(lines, prefix, indent, `${label}${badge}`, safeWidth, available);
|
|
13071
13118
|
left -= lines.length - beforeLabel;
|
|
13072
13119
|
if (row.description !== void 0 && row.description !== "" && left > 0) {
|
|
@@ -13077,9 +13124,9 @@ var QuestionFlow = class {
|
|
|
13077
13124
|
}
|
|
13078
13125
|
if (isCursor) cursorRendered = true;
|
|
13079
13126
|
}
|
|
13080
|
-
if (
|
|
13127
|
+
if (rows.length > MAX_VISIBLE_OPTIONS && left > 0) left = appendWrappedBudgeted(lines, " ", " ", `showing ${start + 1}-${end} of ${rows.length}`, safeWidth, left);
|
|
13081
13128
|
}
|
|
13082
|
-
} else
|
|
13129
|
+
} else {
|
|
13083
13130
|
const inputLine = this.otherInput.render(Math.max(1, safeWidth - 2))[0] ?? "";
|
|
13084
13131
|
const stripped = inputLine.startsWith("> ") ? inputLine.slice(2) : inputLine;
|
|
13085
13132
|
lines.push(` ${stripped}`);
|
|
@@ -13610,13 +13657,16 @@ function preview(text, lines) {
|
|
|
13610
13657
|
const CONTEXT_BAR_WIDTH = 12;
|
|
13611
13658
|
/**
|
|
13612
13659
|
* Rounded-frame wrapper for overlay content: `╭─╮` border in the border
|
|
13613
|
-
* token, one cell of padding, width sized to the content.
|
|
13614
|
-
*
|
|
13660
|
+
* token, one cell of padding, width sized to the content. With `fillWidth`
|
|
13661
|
+
* the frame keeps the overlay's full width instead of hugging the widest
|
|
13662
|
+
* content row. Keyboard input forwards to the wrapped component.
|
|
13615
13663
|
*/
|
|
13616
13664
|
var Frame = class {
|
|
13617
13665
|
child;
|
|
13618
|
-
|
|
13666
|
+
fillWidth;
|
|
13667
|
+
constructor(child, fillWidth = false) {
|
|
13619
13668
|
this.child = child;
|
|
13669
|
+
this.fillWidth = fillWidth;
|
|
13620
13670
|
}
|
|
13621
13671
|
invalidate() {
|
|
13622
13672
|
this.child.invalidate?.();
|
|
@@ -13630,7 +13680,7 @@ var Frame = class {
|
|
|
13630
13680
|
render(width) {
|
|
13631
13681
|
const inner = Math.max(1, Math.floor(width) - 4);
|
|
13632
13682
|
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))));
|
|
13683
|
+
const contentWidth = this.fillWidth ? inner : Math.min(inner, Math.max(1, ...lines.map((line) => visibleWidth(line))));
|
|
13634
13684
|
const frameWidth = contentWidth + 4;
|
|
13635
13685
|
const b = color.border;
|
|
13636
13686
|
const out = [b(`╭${"─".repeat(frameWidth - 2)}╮`)];
|
|
@@ -13643,6 +13693,36 @@ var Frame = class {
|
|
|
13643
13693
|
return out;
|
|
13644
13694
|
}
|
|
13645
13695
|
};
|
|
13696
|
+
/**
|
|
13697
|
+
* Frame for the question flow in the EDITOR SEAT: it re-derives the flow's
|
|
13698
|
+
* row budget from the terminal height on EVERY render (60% cap, 8..24
|
|
13699
|
+
* content rows — the flow's render output IS its height in the seat layout,
|
|
13700
|
+
* nothing clips it), so an active resize or a queued flow presented later
|
|
13701
|
+
* always budgets against the current terminal. It also forwards focus to the
|
|
13702
|
+
* flow so its free-text Input keeps the hardware cursor (a plain Frame would
|
|
13703
|
+
* swallow the focus flag).
|
|
13704
|
+
*/
|
|
13705
|
+
var QuestionFrame = class extends Frame {
|
|
13706
|
+
flow;
|
|
13707
|
+
heightOf;
|
|
13708
|
+
constructor(flow, heightOf) {
|
|
13709
|
+
super(flow, true);
|
|
13710
|
+
this.flow = flow;
|
|
13711
|
+
this.heightOf = heightOf;
|
|
13712
|
+
}
|
|
13713
|
+
render(width) {
|
|
13714
|
+
const rows = Math.max(1, this.heightOf());
|
|
13715
|
+
const frameRows = Math.max(10, Math.min(26, Math.floor(rows * .6)));
|
|
13716
|
+
this.flow.setMaxRows(frameRows - 2);
|
|
13717
|
+
return super.render(width);
|
|
13718
|
+
}
|
|
13719
|
+
get focused() {
|
|
13720
|
+
return this.flow.focused;
|
|
13721
|
+
}
|
|
13722
|
+
set focused(value) {
|
|
13723
|
+
this.flow.focused = value;
|
|
13724
|
+
}
|
|
13725
|
+
};
|
|
13646
13726
|
/** The session head card: identity facts, wrapped to the available width so
|
|
13647
13727
|
* nothing is truncated, framed with a box whose width matches the editor's
|
|
13648
13728
|
* border below it (a fixed-width rule looked misaligned next to the frame). */
|
|
@@ -13712,6 +13792,134 @@ var Spacer = class {
|
|
|
13712
13792
|
return [""];
|
|
13713
13793
|
}
|
|
13714
13794
|
};
|
|
13795
|
+
/**
|
|
13796
|
+
* Bullet + continuation-indent wrapper that keeps its child LIVE, so a
|
|
13797
|
+
* terminal resize re-renders the child at the new width instead of
|
|
13798
|
+
* re-wrapping a frozen render (the 5a76526 regression: assistant/user
|
|
13799
|
+
* messages were flattened to a static Text at build time, so markdown
|
|
13800
|
+
* tables could never reflow and border lines wrapped as plain text on
|
|
13801
|
+
* narrow windows). The bullet leads the FIRST line; wrapped continuation
|
|
13802
|
+
* lines indent under it (kimi prefix+indent parity).
|
|
13803
|
+
*
|
|
13804
|
+
* The prefixed output keeps a REFERENCE-STABLE cache: when the child
|
|
13805
|
+
* returns the same array instance (its own text+width cache hit) at the
|
|
13806
|
+
* same width, the wrapper returns the same prefixed array — so the fork's
|
|
13807
|
+
* per-frame processed-line reuse (fork AGENTS.md divergence 5) keeps
|
|
13808
|
+
* hitting on steady frames instead of re-normalizing every line.
|
|
13809
|
+
*/
|
|
13810
|
+
var BulletedComponent = class {
|
|
13811
|
+
child;
|
|
13812
|
+
prefix;
|
|
13813
|
+
prefixWidth;
|
|
13814
|
+
indent;
|
|
13815
|
+
lastChild;
|
|
13816
|
+
lastWidth = -1;
|
|
13817
|
+
cached;
|
|
13818
|
+
constructor(child, prefix) {
|
|
13819
|
+
this.child = child;
|
|
13820
|
+
this.prefix = prefix;
|
|
13821
|
+
this.prefixWidth = visibleWidth(prefix);
|
|
13822
|
+
this.indent = " ".repeat(this.prefixWidth);
|
|
13823
|
+
}
|
|
13824
|
+
invalidate() {
|
|
13825
|
+
this.child.invalidate?.();
|
|
13826
|
+
}
|
|
13827
|
+
dispose() {
|
|
13828
|
+
this.child.dispose?.();
|
|
13829
|
+
}
|
|
13830
|
+
render(width) {
|
|
13831
|
+
const inner = Math.max(1, width - this.prefixWidth);
|
|
13832
|
+
const child = this.child.render(inner);
|
|
13833
|
+
if (child === this.lastChild && width === this.lastWidth && this.cached !== void 0) return this.cached;
|
|
13834
|
+
this.lastChild = child;
|
|
13835
|
+
this.lastWidth = width;
|
|
13836
|
+
this.cached = child.map((line, index) => (index === 0 ? this.prefix : this.indent) + line);
|
|
13837
|
+
return this.cached;
|
|
13838
|
+
}
|
|
13839
|
+
};
|
|
13840
|
+
/**
|
|
13841
|
+
* Longest prefix of `text` whose WRAPPED height fits `budget` rows at
|
|
13842
|
+
* `width`, with an ellipsis marking a cut — the approval dialog's height
|
|
13843
|
+
* budget must count wrapped rows, not raw lines, because a single long
|
|
13844
|
+
* line can wrap across many display rows. Wrapped height is monotonic in
|
|
13845
|
+
* the prefix length, so a binary search bounds the wrap calls. The
|
|
13846
|
+
* ellipsis reserves its own row when truncating (a full last row would
|
|
13847
|
+
* otherwise push it onto a new row and overflow the budget).
|
|
13848
|
+
* @param text - the candidate text ('' yields '').
|
|
13849
|
+
* @param width - the wrap width.
|
|
13850
|
+
* @param budget - the row budget; 0 or negative yields '…' for non-empty.
|
|
13851
|
+
* @returns the fitted text and whether it was truncated.
|
|
13852
|
+
*/
|
|
13853
|
+
function capWrappedToHeight(text, width, budget) {
|
|
13854
|
+
if (text === "") return {
|
|
13855
|
+
text: "",
|
|
13856
|
+
truncated: false
|
|
13857
|
+
};
|
|
13858
|
+
if (budget <= 0) return {
|
|
13859
|
+
text: "",
|
|
13860
|
+
truncated: true
|
|
13861
|
+
};
|
|
13862
|
+
const fits = (candidate, rows) => wrapTextWithAnsi(candidate, width).length <= rows;
|
|
13863
|
+
if (fits(text, budget)) return {
|
|
13864
|
+
text,
|
|
13865
|
+
truncated: false
|
|
13866
|
+
};
|
|
13867
|
+
if (budget === 1) return {
|
|
13868
|
+
text: truncateToWidth(text, width, "…"),
|
|
13869
|
+
truncated: true
|
|
13870
|
+
};
|
|
13871
|
+
const target = budget - 1;
|
|
13872
|
+
let low = 0;
|
|
13873
|
+
let high = text.length;
|
|
13874
|
+
while (low < high) {
|
|
13875
|
+
const mid = Math.ceil((low + high) / 2);
|
|
13876
|
+
if (fits(text.slice(0, mid), target)) low = mid;
|
|
13877
|
+
else high = mid - 1;
|
|
13878
|
+
}
|
|
13879
|
+
return {
|
|
13880
|
+
text: `${text.slice(0, low)}…`,
|
|
13881
|
+
truncated: true
|
|
13882
|
+
};
|
|
13883
|
+
}
|
|
13884
|
+
/**
|
|
13885
|
+
* Fit `text` into `budget` wrapped rows for the approval dialog, ending with
|
|
13886
|
+
* a dimmed `... N more` marker row when the content is cut. The marker rides
|
|
13887
|
+
* INSIDE the budget (content rows cap at budget−1, the marker itself is
|
|
13888
|
+
* width-cropped so it can never wrap), so a section can never silently
|
|
13889
|
+
* overflow the dialog's maxHeight — same marker semantics as the
|
|
13890
|
+
* question flow's `appendWrappedBudgeted`. A single-row budget keeps
|
|
13891
|
+
* the old width-cropped ellipsis (a bare marker row would waste the row).
|
|
13892
|
+
* @returns the display text (rows joined with '\n') and the hidden row count.
|
|
13893
|
+
*/
|
|
13894
|
+
function capWrappedToMarker(text, width, budget) {
|
|
13895
|
+
if (text === "" || budget <= 0) return {
|
|
13896
|
+
text: "",
|
|
13897
|
+
hidden: 0
|
|
13898
|
+
};
|
|
13899
|
+
const total = wrapTextWithAnsi(text, width).length;
|
|
13900
|
+
if (total <= budget) return {
|
|
13901
|
+
text,
|
|
13902
|
+
hidden: 0
|
|
13903
|
+
};
|
|
13904
|
+
if (budget === 1) return {
|
|
13905
|
+
text: truncateToWidth(text, width, "…"),
|
|
13906
|
+
hidden: total - 1
|
|
13907
|
+
};
|
|
13908
|
+
const target = budget - 1;
|
|
13909
|
+
let low = 0;
|
|
13910
|
+
let high = text.length;
|
|
13911
|
+
while (low < high) {
|
|
13912
|
+
const mid = Math.ceil((low + high) / 2);
|
|
13913
|
+
if (wrapTextWithAnsi(text.slice(0, mid), width).length <= target) low = mid;
|
|
13914
|
+
else high = mid - 1;
|
|
13915
|
+
}
|
|
13916
|
+
const hidden = total - target;
|
|
13917
|
+
const marker = truncateToWidth(color.textDim(`... ${hidden} more line${hidden > 1 ? "s" : ""}`), width, "…");
|
|
13918
|
+
return {
|
|
13919
|
+
text: `${text.slice(0, low)}\n${marker}`,
|
|
13920
|
+
hidden
|
|
13921
|
+
};
|
|
13922
|
+
}
|
|
13715
13923
|
/** The live job-output viewer body: a title line + refreshable text panel. */
|
|
13716
13924
|
var OutputViewerPanel = class {
|
|
13717
13925
|
title;
|
|
@@ -13751,6 +13959,13 @@ var TuiApp = class TuiApp {
|
|
|
13751
13959
|
terminal;
|
|
13752
13960
|
tui;
|
|
13753
13961
|
editor;
|
|
13962
|
+
/**
|
|
13963
|
+
* The editor's SEAT (kimi's editorContainer): holds the editor normally,
|
|
13964
|
+
* the QuestionFrame while a question flow is active — so the dialog
|
|
13965
|
+
* renders in the editor's row position (full width, above the footer)
|
|
13966
|
+
* instead of a centered modal that covered the transcript.
|
|
13967
|
+
*/
|
|
13968
|
+
editorSeat;
|
|
13754
13969
|
header;
|
|
13755
13970
|
messagesView;
|
|
13756
13971
|
footer;
|
|
@@ -13789,6 +14004,8 @@ var TuiApp = class TuiApp {
|
|
|
13789
14004
|
dock;
|
|
13790
14005
|
/** Active background tasks for the dock/footer lines (label + status). */
|
|
13791
14006
|
dockTasks = [];
|
|
14007
|
+
/** Live child subagents (continuable or running one-shot) for the dock/footer lines (never jobs records). */
|
|
14008
|
+
dockAgents = [];
|
|
13792
14009
|
/**
|
|
13793
14010
|
* The queued-input pane below the todo panel (kimi QueuePane parity):
|
|
13794
14011
|
* a border rule plus one `❯ text` row per pending message and a dim hint.
|
|
@@ -13810,6 +14027,19 @@ var TuiApp = class TuiApp {
|
|
|
13810
14027
|
}
|
|
13811
14028
|
/** Fullscreen (alt-screen) instance; absent in regular mode. */
|
|
13812
14029
|
fullscreen;
|
|
14030
|
+
/** One shared in-flight autodetect; concurrent callers coalesce onto it
|
|
14031
|
+
* (overlapping OSC 11 queries would mis-pair replies by FIFO order). */
|
|
14032
|
+
autoDetectInFlight;
|
|
14033
|
+
/** The LATEST shouldApply guard of the in-flight autodetect; consulted at
|
|
14034
|
+
* settle time so a late result can never override a newer explicit
|
|
14035
|
+
* choice; reset when the flight settles. */
|
|
14036
|
+
autoDetectGuard;
|
|
14037
|
+
/** Whether the terminal's live scheme reports are being tracked. */
|
|
14038
|
+
followingTerminalTheme = false;
|
|
14039
|
+
/** Terminal-scheme listeners, fanned out from every screen's reports. */
|
|
14040
|
+
terminalSchemeListeners = /* @__PURE__ */ new Set();
|
|
14041
|
+
/** Per-screen scheme-report registrations, rebuilt on screen switches. */
|
|
14042
|
+
schemeDisposers = [];
|
|
13813
14043
|
/** The mounted transcript-search overlay, while one is open. */
|
|
13814
14044
|
searchOverlay;
|
|
13815
14045
|
/** The search input component, while one is open (for match counts). */
|
|
@@ -13874,6 +14104,13 @@ var TuiApp = class TuiApp {
|
|
|
13874
14104
|
externalEditorInFlight = false;
|
|
13875
14105
|
/** The live session's auto-generated title, shown in the header when set. */
|
|
13876
14106
|
sessionTitleText = "";
|
|
14107
|
+
/** The read-only subagent viewer: while set, the editor bar shows a
|
|
14108
|
+
* placeholder, the editor border switches to the accent color, and the
|
|
14109
|
+
* header carries a persistent badge — the transient notify line is not
|
|
14110
|
+
* the only signal. */
|
|
14111
|
+
viewerMode;
|
|
14112
|
+
/** The real draft preserved while the viewer covers the editor bar. */
|
|
14113
|
+
draftBeforeViewer;
|
|
13877
14114
|
constructor(terminal, events, options = {}) {
|
|
13878
14115
|
if (events.openExternalEditor !== void 0 && events.runOwned === void 0) throw new Error("openExternalEditor requires runOwned (the owned-task entry — AGENTS.md)");
|
|
13879
14116
|
this.terminal = terminal;
|
|
@@ -13899,13 +14136,15 @@ var TuiApp = class TuiApp {
|
|
|
13899
14136
|
this.queuePane = new Text("", 0, 0);
|
|
13900
14137
|
this.working = new WorkingIndicator(() => this.requestRender(), options.workingIntervalMs === void 0 ? {} : { intervalMs: options.workingIntervalMs });
|
|
13901
14138
|
this.footer = new Text("", 0, 0);
|
|
14139
|
+
this.editorSeat = new Container();
|
|
14140
|
+
this.editorSeat.addChild(this.editor);
|
|
13902
14141
|
this.tui.addChild(this.header);
|
|
13903
14142
|
this.tui.addChild(this.messagesView);
|
|
13904
14143
|
this.tui.addChild(this.dock);
|
|
13905
14144
|
this.tui.addChild(this.todoPanel);
|
|
13906
14145
|
this.tui.addChild(this.queuePane);
|
|
13907
14146
|
this.tui.addChild(this.working);
|
|
13908
|
-
this.tui.addChild(this.
|
|
14147
|
+
this.tui.addChild(this.editorSeat);
|
|
13909
14148
|
this.tui.addChild(this.footer);
|
|
13910
14149
|
this.tui.setFocus(this.editor);
|
|
13911
14150
|
this.tui.addInputListener((data) => this.routeInput(data));
|
|
@@ -13941,6 +14180,8 @@ var TuiApp = class TuiApp {
|
|
|
13941
14180
|
this.clearNotify();
|
|
13942
14181
|
this.working.dispose();
|
|
13943
14182
|
this.cancelQuestionFlows();
|
|
14183
|
+
for (const dispose of this.schemeDisposers) dispose();
|
|
14184
|
+
this.schemeDisposers = [];
|
|
13944
14185
|
this.tui.stop();
|
|
13945
14186
|
this.fullscreen?.stop();
|
|
13946
14187
|
this.fullscreen = void 0;
|
|
@@ -13950,6 +14191,7 @@ var TuiApp = class TuiApp {
|
|
|
13950
14191
|
if (isKeyRelease(data) || isKeyRepeat(data)) return void 0;
|
|
13951
14192
|
if (this.activeQuestions !== void 0) return this.handleQuestionKey(data);
|
|
13952
14193
|
if (this.activeApproval !== void 0) return this.handleApprovalKey(data);
|
|
14194
|
+
if (this.viewerMode !== void 0 && !this.overlayHost.hasOverlayEntries && !matchesKey(data, "escape") && !matchesKey(data, "ctrl+o")) return { consume: true };
|
|
13953
14195
|
if (this.searchOverlay !== void 0) {
|
|
13954
14196
|
if (matchesKey(data, "escape")) {
|
|
13955
14197
|
this.closeTranscriptSearch();
|
|
@@ -13983,6 +14225,17 @@ var TuiApp = class TuiApp {
|
|
|
13983
14225
|
this.events.onDequeue?.();
|
|
13984
14226
|
return { consume: true };
|
|
13985
14227
|
}
|
|
14228
|
+
if (matchesKey(data, "ctrl+enter")) {
|
|
14229
|
+
if (this.overlayHost.hasOverlayEntries) return void 0;
|
|
14230
|
+
if (this.events.onQueueSubmit === void 0) return void 0;
|
|
14231
|
+
const text = this.editor.getText();
|
|
14232
|
+
if (text.trim() === "") return void 0;
|
|
14233
|
+
this.rememberInput(text);
|
|
14234
|
+
this.clearNotify();
|
|
14235
|
+
this.editor.setText("");
|
|
14236
|
+
this.events.onQueueSubmit(text);
|
|
14237
|
+
return { consume: true };
|
|
14238
|
+
}
|
|
13986
14239
|
if (matchesKey(data, "escape")) {
|
|
13987
14240
|
if (this.overlayHost.hasOverlayEntries) return void 0;
|
|
13988
14241
|
if (this.events.onSingleEscape?.() === true) return { consume: true };
|
|
@@ -14047,6 +14300,14 @@ var TuiApp = class TuiApp {
|
|
|
14047
14300
|
* compositor interleaves stacked boxes line by line (two different-width
|
|
14048
14301
|
* dialogs would garble into one frame), and modal hiding is done here so
|
|
14049
14302
|
* the vendored fork stays pristine.
|
|
14303
|
+
*
|
|
14304
|
+
* While a question flow owns the seat (a logical capturing modal), a NEW
|
|
14305
|
+
* overlay joins the question's suspension set instead of appearing on top:
|
|
14306
|
+
* it is hidden, the question's directly suspended handles become ITS
|
|
14307
|
+
* dependents (kept hidden, restored only when it closes), and the overlay
|
|
14308
|
+
* itself becomes the question's frontmost suspended handle — so reverse
|
|
14309
|
+
* modal order survives the question. Question input keeps first priority
|
|
14310
|
+
* either way (routeInput).
|
|
14050
14311
|
* @param component - the overlay content.
|
|
14051
14312
|
* @param options - overlay sizing/positioning.
|
|
14052
14313
|
* @returns the handle; hide() also forgets the handle.
|
|
@@ -14054,6 +14315,27 @@ var TuiApp = class TuiApp {
|
|
|
14054
14315
|
showOverlayOnHost(component, options) {
|
|
14055
14316
|
const handle = this.overlayHost.showOverlay(component, options);
|
|
14056
14317
|
this.overlayHandles.add(handle);
|
|
14318
|
+
const question = this.activeQuestions;
|
|
14319
|
+
if (question !== void 0) {
|
|
14320
|
+
for (const other of this.overlayHandles) if (other !== handle && !other.isHidden()) {
|
|
14321
|
+
other.setHidden(true);
|
|
14322
|
+
question.suspendedOverlays.add(other);
|
|
14323
|
+
}
|
|
14324
|
+
if (options?.nonCapturing !== true) {
|
|
14325
|
+
const dependents = /* @__PURE__ */ new Set();
|
|
14326
|
+
for (const other of question.suspendedOverlays) dependents.add(other);
|
|
14327
|
+
if (dependents.size > 0) {
|
|
14328
|
+
for (const other of dependents) question.suspendedOverlays.delete(other);
|
|
14329
|
+
this.overlayDependents.set(handle, dependents);
|
|
14330
|
+
}
|
|
14331
|
+
}
|
|
14332
|
+
handle.setHidden(true);
|
|
14333
|
+
question.suspendedOverlays.add(handle);
|
|
14334
|
+
return {
|
|
14335
|
+
...handle,
|
|
14336
|
+
hide: () => this.closeOverlayHandle(handle)
|
|
14337
|
+
};
|
|
14338
|
+
}
|
|
14057
14339
|
if (options?.nonCapturing !== true) {
|
|
14058
14340
|
const hidden = /* @__PURE__ */ new Set();
|
|
14059
14341
|
for (const other of this.overlayHandles) if (other !== handle && !other.isHidden()) {
|
|
@@ -14064,15 +14346,33 @@ var TuiApp = class TuiApp {
|
|
|
14064
14346
|
}
|
|
14065
14347
|
return {
|
|
14066
14348
|
...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
|
-
}
|
|
14349
|
+
hide: () => this.closeOverlayHandle(handle)
|
|
14073
14350
|
};
|
|
14074
14351
|
}
|
|
14075
14352
|
/**
|
|
14353
|
+
* Question-aware close for one tracked overlay handle (the wrapper's
|
|
14354
|
+
* hide). Without an active question this matches the historical behavior:
|
|
14355
|
+
* the handle's dependents are unhidden, the graph is cleaned, and the
|
|
14356
|
+
* overlay is removed. While a question owns the seat, the handle leaves
|
|
14357
|
+
* the question's suspension set, every dependency set drops it (no parent
|
|
14358
|
+
* retains a dead child), and its still-mounted dependents remain hidden
|
|
14359
|
+
* and become DIRECTLY owned by the question — they must not flash back
|
|
14360
|
+
* while the question is still up.
|
|
14361
|
+
*/
|
|
14362
|
+
closeOverlayHandle(handle) {
|
|
14363
|
+
const question = this.activeQuestions;
|
|
14364
|
+
if (question !== void 0) question.suspendedOverlays.delete(handle);
|
|
14365
|
+
for (const dependents of this.overlayDependents.values()) dependents.delete(handle);
|
|
14366
|
+
const owned = this.overlayDependents.get(handle);
|
|
14367
|
+
if (owned !== void 0) {
|
|
14368
|
+
this.overlayDependents.delete(handle);
|
|
14369
|
+
if (question !== void 0) for (const dependent of owned) question.suspendedOverlays.add(dependent);
|
|
14370
|
+
else for (const dependent of owned) dependent.setHidden(false);
|
|
14371
|
+
}
|
|
14372
|
+
this.overlayHandles.delete(handle);
|
|
14373
|
+
handle.hide();
|
|
14374
|
+
}
|
|
14375
|
+
/**
|
|
14076
14376
|
* Launch the external editor with the current draft. The TUI stops first
|
|
14077
14377
|
* (raw mode released) and restarts after the editor returns; a fullscreen
|
|
14078
14378
|
* mode is not restored (the editor session ends in regular mode).
|
|
@@ -14189,6 +14489,20 @@ var TuiApp = class TuiApp {
|
|
|
14189
14489
|
this.rebuildMessages();
|
|
14190
14490
|
}
|
|
14191
14491
|
/**
|
|
14492
|
+
* Drop SETTLED local cards (completed `!`/`!!` shell runs); running cards
|
|
14493
|
+
* survive so a live stream is never dismissed by a concurrent submit.
|
|
14494
|
+
* Local cards are a live view, not a record: context runs clear on submit
|
|
14495
|
+
* success (the transcript's user row takes over), local-only runs on the
|
|
14496
|
+
* next user submission (dispatchUserInput / steerNow).
|
|
14497
|
+
*/
|
|
14498
|
+
clearSettledLocalMessages() {
|
|
14499
|
+
const running = this.localMessages.filter((message) => message.kind === "tool" && message.status === "running");
|
|
14500
|
+
if (running.length === this.localMessages.length) return;
|
|
14501
|
+
this.localMessages.length = 0;
|
|
14502
|
+
this.localMessages.push(...running);
|
|
14503
|
+
this.rebuildMessages();
|
|
14504
|
+
}
|
|
14505
|
+
/**
|
|
14192
14506
|
* Toggle between regular (terminal scrollback) and fullscreen (alt screen).
|
|
14193
14507
|
* Overlays live on the active screen, so the switch hides every mounted
|
|
14194
14508
|
* overlay; a pending approval prompt is re-rendered on the new screen.
|
|
@@ -14211,6 +14525,8 @@ var TuiApp = class TuiApp {
|
|
|
14211
14525
|
pending?.handle?.hide();
|
|
14212
14526
|
for (const handle of this.overlayHandles) handle.hide();
|
|
14213
14527
|
this.overlayHandles.clear();
|
|
14528
|
+
this.overlayDependents.clear();
|
|
14529
|
+
if (this.activeQuestions !== void 0) this.activeQuestions.suspendedOverlays.clear();
|
|
14214
14530
|
if (enabled) {
|
|
14215
14531
|
const alt = new TuiAltScreen(this.terminal, void 0, void 0, { onCellClick: (x, y) => this.handleFullscreenClick(x, y) });
|
|
14216
14532
|
this.fullscreenScroll = new ScrollView(this.messagesView, {
|
|
@@ -14244,7 +14560,7 @@ var TuiApp = class TuiApp {
|
|
|
14244
14560
|
shrink: 0
|
|
14245
14561
|
},
|
|
14246
14562
|
{
|
|
14247
|
-
component: this.
|
|
14563
|
+
component: this.editorSeat,
|
|
14248
14564
|
shrink: 0
|
|
14249
14565
|
},
|
|
14250
14566
|
{
|
|
@@ -14258,6 +14574,7 @@ var TuiApp = class TuiApp {
|
|
|
14258
14574
|
alt.start();
|
|
14259
14575
|
alt.setFocus(this.editor);
|
|
14260
14576
|
this.fullscreen = alt;
|
|
14577
|
+
this.refreshSchemeRegistrations();
|
|
14261
14578
|
} else {
|
|
14262
14579
|
this.fullscreen?.stop();
|
|
14263
14580
|
this.fullscreen = void 0;
|
|
@@ -14265,9 +14582,12 @@ var TuiApp = class TuiApp {
|
|
|
14265
14582
|
this.tui.start();
|
|
14266
14583
|
this.tui.requestRender(true);
|
|
14267
14584
|
this.tui.setFocus(this.editor);
|
|
14585
|
+
this.refreshSchemeRegistrations();
|
|
14268
14586
|
}
|
|
14269
14587
|
this.events.onFullscreenChange?.(enabled);
|
|
14270
14588
|
if (pending !== void 0) this.renderApprovalDialog(pending);
|
|
14589
|
+
const question = this.activeQuestions;
|
|
14590
|
+
if (question?.frame !== void 0) (this.fullscreen ?? this.tui).setFocus(question.frame);
|
|
14271
14591
|
}
|
|
14272
14592
|
/**
|
|
14273
14593
|
* Open the transcript-search overlay (Ctrl+Shift+F) and focus its input.
|
|
@@ -14467,11 +14787,46 @@ var TuiApp = class TuiApp {
|
|
|
14467
14787
|
/**
|
|
14468
14788
|
* Replace the editor draft. The runner restores a submission that the
|
|
14469
14789
|
* divergence guard blocked, so the user's text survives for a retry.
|
|
14790
|
+
* While the subagent viewer covers the editor, the write goes to the
|
|
14791
|
+
* preserved draft (the placeholder bar stays up; the draft is restored
|
|
14792
|
+
* on exit).
|
|
14470
14793
|
*/
|
|
14471
14794
|
setEditorText(text) {
|
|
14795
|
+
if (this.viewerMode !== void 0) {
|
|
14796
|
+
this.draftBeforeViewer = text;
|
|
14797
|
+
return;
|
|
14798
|
+
}
|
|
14472
14799
|
this.editor.setText(text);
|
|
14473
14800
|
}
|
|
14474
14801
|
/**
|
|
14802
|
+
* Enter or leave the read-only subagent viewer. While viewing, the
|
|
14803
|
+
* editor bar is covered by a placeholder (the real draft is preserved
|
|
14804
|
+
* and restored on exit), the editor border switches to the accent color,
|
|
14805
|
+
* and the header shows a persistent `[viewing subagent]` badge — so the
|
|
14806
|
+
* mode reads at a glance instead of relying on the transient notify.
|
|
14807
|
+
* @param mode - the child identity + label; `undefined` leaves the viewer.
|
|
14808
|
+
*/
|
|
14809
|
+
setViewerMode(mode) {
|
|
14810
|
+
if (mode === void 0) {
|
|
14811
|
+
if (this.viewerMode === void 0) return;
|
|
14812
|
+
this.viewerMode = void 0;
|
|
14813
|
+
this.editor.borderColor = this.editorBorder;
|
|
14814
|
+
this.editor.setText(this.draftBeforeViewer ?? "");
|
|
14815
|
+
this.draftBeforeViewer = void 0;
|
|
14816
|
+
this.editor.invalidate();
|
|
14817
|
+
this.renderHeader();
|
|
14818
|
+
this.requestRender();
|
|
14819
|
+
return;
|
|
14820
|
+
}
|
|
14821
|
+
if (this.viewerMode === void 0) this.draftBeforeViewer = this.editor.getText();
|
|
14822
|
+
this.viewerMode = mode;
|
|
14823
|
+
this.editor.borderColor = color.accent;
|
|
14824
|
+
this.editor.setText(`viewing subagent: ${mode.label} — read-only · Esc returns`);
|
|
14825
|
+
this.editor.invalidate();
|
|
14826
|
+
this.renderHeader();
|
|
14827
|
+
this.requestRender();
|
|
14828
|
+
}
|
|
14829
|
+
/**
|
|
14475
14830
|
* Set the session head rendered above the transcript: the session identity,
|
|
14476
14831
|
* model, version, preset, and cwd, wrapped to the terminal width with a
|
|
14477
14832
|
* full-width rule beneath. Replaces any previous head.
|
|
@@ -14568,16 +14923,8 @@ var TuiApp = class TuiApp {
|
|
|
14568
14923
|
}
|
|
14569
14924
|
/** Render one transcript message as a pi-tui component. */
|
|
14570
14925
|
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
|
-
}
|
|
14926
|
+
if (message.kind === "user") return new BulletedComponent(new Text(message.text, 0, 0), `${color.roleUser("❯")} `);
|
|
14927
|
+
if (message.kind === "assistant") return new BulletedComponent(new Markdown(message.text, 0, 0, markdownTheme), `${color.primary("🐋")} `);
|
|
14581
14928
|
if (message.kind === "thinking") {
|
|
14582
14929
|
if (message.turn >= boundary || this.expandedOverride.get(message) === true) return new Text(color.textDimItalic(`🐳 ${message.text}`), 0, 0);
|
|
14583
14930
|
const lines = message.text.split("\n").filter((line) => line !== "");
|
|
@@ -14649,6 +14996,34 @@ var TuiApp = class TuiApp {
|
|
|
14649
14996
|
return card;
|
|
14650
14997
|
}
|
|
14651
14998
|
/**
|
|
14999
|
+
* The expanded card's terminal-command row: `$ cmd` (bash) or `PS> cmd`
|
|
15000
|
+
* (pwsh) in the shell prompt token, dimmed (kimi ShellExecution parity —
|
|
15001
|
+
* the folded card shows the command; the expanded body must not drop
|
|
15002
|
+
* it). Multi-line commands render every line under the prompt. No-op for
|
|
15003
|
+
* an empty command.
|
|
15004
|
+
* @param card - the card container to fill.
|
|
15005
|
+
* @param command - the command text.
|
|
15006
|
+
* @param prompt - the shell prompt token (`$ ` or `PS> `).
|
|
15007
|
+
*/
|
|
15008
|
+
addTerminalCommandRow(card, command, prompt) {
|
|
15009
|
+
if (command === "") return;
|
|
15010
|
+
const styledPrompt = color.shellMode(prompt);
|
|
15011
|
+
const lines = command.split("\n");
|
|
15012
|
+
for (const [index, line] of lines.entries()) {
|
|
15013
|
+
const prefix = index === 0 ? styledPrompt : " ".repeat(visibleWidth(styledPrompt));
|
|
15014
|
+
card.addChild(new Text(`${prefix}${color.textDim(line)}`, 0, 0));
|
|
15015
|
+
}
|
|
15016
|
+
}
|
|
15017
|
+
/** The bash/pwsh command from the raw args ('' when not a terminal call). */
|
|
15018
|
+
terminalCommand(name, argsRaw) {
|
|
15019
|
+
const call = parseCallPreview(name, argsRaw);
|
|
15020
|
+
return call?.kind === "bash" ? call.command : "";
|
|
15021
|
+
}
|
|
15022
|
+
/** The shell prompt token for a terminal tool (pwsh renders `PS> `). */
|
|
15023
|
+
shellPrompt(name) {
|
|
15024
|
+
return name === "pwsh" ? "PS> " : "$ ";
|
|
15025
|
+
}
|
|
15026
|
+
/**
|
|
14652
15027
|
* Render one expanded tool card's body. When the runner wired a presenter,
|
|
14653
15028
|
* the body follows the tool's own render intent (presentResult): a read
|
|
14654
15029
|
* card shows numbered lines plus the relativized path and total line
|
|
@@ -14689,12 +15064,18 @@ var TuiApp = class TuiApp {
|
|
|
14689
15064
|
this.renderDiffBody(card, callView.diffs, explicitlyExpanded);
|
|
14690
15065
|
return;
|
|
14691
15066
|
}
|
|
15067
|
+
if (callView.card === "terminal") {
|
|
15068
|
+
this.addTerminalCommandRow(card, callView.title, this.shellPrompt(message.name));
|
|
15069
|
+
return;
|
|
15070
|
+
}
|
|
14692
15071
|
if (callView.card === "generic" && callView.rawInput !== void 0) {
|
|
15072
|
+
this.addTerminalCommandRow(card, this.terminalCommand(message.name, message.args), this.shellPrompt(message.name));
|
|
14693
15073
|
const raw = typeof callView.rawInput === "string" ? callView.rawInput : JSON.stringify(callView.rawInput, null, 2);
|
|
14694
15074
|
card.addChild(new Text(color.textDim(raw), 0, 0));
|
|
14695
15075
|
return;
|
|
14696
15076
|
}
|
|
14697
15077
|
}
|
|
15078
|
+
this.addTerminalCommandRow(card, this.terminalCommand(message.name, message.args), this.shellPrompt(message.name));
|
|
14698
15079
|
return;
|
|
14699
15080
|
}
|
|
14700
15081
|
if (message.result === "" && (message.resultBlocks?.length ?? 0) === 0) return;
|
|
@@ -14722,6 +15103,7 @@ var TuiApp = class TuiApp {
|
|
|
14722
15103
|
}
|
|
14723
15104
|
return;
|
|
14724
15105
|
case "terminal": {
|
|
15106
|
+
this.addTerminalCommandRow(card, this.terminalCommand(message.name, message.args), this.shellPrompt(message.name));
|
|
14725
15107
|
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
15108
|
const exit = resultView.exitCode !== void 0 ? `[exit ${resultView.exitCode}]` : resultView.signal !== void 0 ? `[signal ${resultView.signal}]` : "";
|
|
14727
15109
|
if (exit !== "") card.addChild(new Text(color.textDim(exit), 0, 0));
|
|
@@ -14755,7 +15137,10 @@ var TuiApp = class TuiApp {
|
|
|
14755
15137
|
}
|
|
14756
15138
|
}
|
|
14757
15139
|
if (isDiffResult(message.name, message.result)) for (const line of renderDiffLines(message.result)) card.addChild(new Text(line, 0, 0));
|
|
14758
|
-
else
|
|
15140
|
+
else {
|
|
15141
|
+
this.addTerminalCommandRow(card, this.terminalCommand(message.name, message.args), this.shellPrompt(message.name));
|
|
15142
|
+
card.addChild(new Text(color.textDim(message.result), 0, 0));
|
|
15143
|
+
}
|
|
14759
15144
|
}
|
|
14760
15145
|
/**
|
|
14761
15146
|
* Fill a tool card with a diff body: LCS-aligned clustered lines, capped
|
|
@@ -14846,8 +15231,9 @@ var TuiApp = class TuiApp {
|
|
|
14846
15231
|
* switch only has to re-run this. */
|
|
14847
15232
|
renderHeader() {
|
|
14848
15233
|
const badge = this.planMode ? ` ${color.warning("[plan]")}` : "";
|
|
14849
|
-
const
|
|
14850
|
-
this.
|
|
15234
|
+
const viewerBadge = this.viewerMode === void 0 ? "" : ` ${color.accent("[viewing subagent]")}`;
|
|
15235
|
+
const title = this.viewerMode !== void 0 ? ` · ${color.textMuted(this.viewerMode.label)}` : this.sessionTitleText === "" ? "" : ` · ${color.textMuted(this.sessionTitleText)}`;
|
|
15236
|
+
this.header.setText(`🐋 dsh-pi-tui${title}${this.todoText}${badge}${viewerBadge}`);
|
|
14851
15237
|
this.requestRender();
|
|
14852
15238
|
}
|
|
14853
15239
|
/**
|
|
@@ -14871,7 +15257,20 @@ var TuiApp = class TuiApp {
|
|
|
14871
15257
|
*/
|
|
14872
15258
|
setTasks(tasks) {
|
|
14873
15259
|
this.dockTasks = tasks;
|
|
14874
|
-
this.tasksActive = tasks.length > 0;
|
|
15260
|
+
this.tasksActive = tasks.length > 0 || this.dockAgents.length > 0;
|
|
15261
|
+
this.renderDock();
|
|
15262
|
+
this.renderFooter();
|
|
15263
|
+
}
|
|
15264
|
+
/**
|
|
15265
|
+
* Replace the live child-subagent list for the dock lines and the
|
|
15266
|
+
* footer badge. Continuable children and foreground one-shot children
|
|
15267
|
+
* never register jobs records (AGENTS.md), so they arm the ↓/Ctrl+J
|
|
15268
|
+
* trigger through this channel.
|
|
15269
|
+
* @param agents - live children (id + label + activity), empty to hide.
|
|
15270
|
+
*/
|
|
15271
|
+
setAgents(agents) {
|
|
15272
|
+
this.dockAgents = agents;
|
|
15273
|
+
this.tasksActive = this.dockTasks.length > 0 || agents.length > 0;
|
|
14875
15274
|
this.renderDock();
|
|
14876
15275
|
this.renderFooter();
|
|
14877
15276
|
}
|
|
@@ -14911,15 +15310,35 @@ var TuiApp = class TuiApp {
|
|
|
14911
15310
|
}
|
|
14912
15311
|
/**
|
|
14913
15312
|
* Replace the editor draft wholesale (the Alt+↑ dequeue path pulls every
|
|
14914
|
-
* queued message back into the editor for editing).
|
|
15313
|
+
* queued message back into the editor for editing). While the subagent
|
|
15314
|
+
* viewer covers the editor, the write goes to the preserved draft.
|
|
14915
15315
|
*/
|
|
14916
15316
|
setDraft(text) {
|
|
15317
|
+
if (this.viewerMode !== void 0) {
|
|
15318
|
+
this.draftBeforeViewer = text;
|
|
15319
|
+
return;
|
|
15320
|
+
}
|
|
14917
15321
|
this.editor.setText(text);
|
|
14918
15322
|
this.requestRender();
|
|
14919
15323
|
}
|
|
14920
|
-
/** The editor's current draft text (the Alt+↑ dequeue merge reads it)
|
|
15324
|
+
/** The editor's current draft text (the Alt+↑ dequeue merge reads it);
|
|
15325
|
+
* while the viewer covers the editor, the preserved draft is returned. */
|
|
14921
15326
|
getDraft() {
|
|
14922
|
-
return this.editor.getText();
|
|
15327
|
+
return this.viewerMode !== void 0 && this.draftBeforeViewer !== void 0 ? this.draftBeforeViewer : this.editor.getText();
|
|
15328
|
+
}
|
|
15329
|
+
/**
|
|
15330
|
+
* Headless-test hook: current overlay tracking-graph sizes. The graph
|
|
15331
|
+
* (overlayHandles / overlayDependents / the active question's suspension)
|
|
15332
|
+
* is behaviorally invisible — stale entries only leak memory — so the
|
|
15333
|
+
* headless suite asserts its sizes directly (e.g. the fullscreen teardown
|
|
15334
|
+
* must leave it empty instead of retaining dead handles).
|
|
15335
|
+
*/
|
|
15336
|
+
overlayGraphState() {
|
|
15337
|
+
return {
|
|
15338
|
+
handles: this.overlayHandles.size,
|
|
15339
|
+
dependents: this.overlayDependents.size,
|
|
15340
|
+
suspended: this.activeQuestions?.suspendedOverlays.size ?? 0
|
|
15341
|
+
};
|
|
14923
15342
|
}
|
|
14924
15343
|
/**
|
|
14925
15344
|
* Rebuild the persistent dock strip above the todo panel: todo summary and
|
|
@@ -14952,6 +15371,14 @@ var TuiApp = class TuiApp {
|
|
|
14952
15371
|
}
|
|
14953
15372
|
if (this.dockTasks.length > 3) lines.push(color.textDim(` … ${this.dockTasks.length - 3} more`));
|
|
14954
15373
|
}
|
|
15374
|
+
if (this.dockAgents.length > 0) {
|
|
15375
|
+
const shown = this.dockAgents.slice(0, 3);
|
|
15376
|
+
for (const agent of shown) {
|
|
15377
|
+
const label = agent.label.length > 40 ? `${agent.label.slice(0, 40)}…` : agent.label;
|
|
15378
|
+
lines.push(color.textDim(`🤖 ${agent.id} · ${label}`));
|
|
15379
|
+
}
|
|
15380
|
+
if (this.dockAgents.length > 3) lines.push(color.textDim(` … ${this.dockAgents.length - 3} more`));
|
|
15381
|
+
}
|
|
14955
15382
|
this.dock.setText(lines.join("\n"));
|
|
14956
15383
|
this.requestRender();
|
|
14957
15384
|
}
|
|
@@ -14969,12 +15396,17 @@ var TuiApp = class TuiApp {
|
|
|
14969
15396
|
/** Rebuild the two footer lines from the current status and plan badge. */
|
|
14970
15397
|
renderFooter() {
|
|
14971
15398
|
const context = this.status.contextTokens !== void 0 && this.status.contextWindow !== void 0 && this.status.contextWindow > 0 ? contextBar(this.status.contextTokens, this.status.contextWindow) : "";
|
|
15399
|
+
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]") : "";
|
|
15400
|
+
const badgeParts = [];
|
|
15401
|
+
if (this.dockTasks.length > 0) badgeParts.push(`${this.dockTasks.length} task${this.dockTasks.length === 1 ? "" : "s"} running`);
|
|
15402
|
+
if (this.dockAgents.length > 0) badgeParts.push(`${this.dockAgents.length} agent${this.dockAgents.length === 1 ? "" : "s"}`);
|
|
15403
|
+
const taskBadge = badgeParts.length === 0 ? "" : color.primary(`[${badgeParts.join(" · ")}${this.editor.getText().trim() === "" ? " · ↓ view" : ""}]`);
|
|
14972
15404
|
const line1 = [
|
|
14973
|
-
|
|
15405
|
+
permissionBadge,
|
|
14974
15406
|
this.planMode ? color.warning("[plan]") : "",
|
|
14975
15407
|
this.status.goal === void 0 || this.status.goal === "" ? "" : color.primary(this.status.goal),
|
|
14976
15408
|
this.status.model === "" ? "" : `[${this.status.model}]`,
|
|
14977
|
-
|
|
15409
|
+
taskBadge,
|
|
14978
15410
|
this.status.cwd,
|
|
14979
15411
|
this.status.branch === "" ? "" : this.status.branch,
|
|
14980
15412
|
context,
|
|
@@ -15121,25 +15553,85 @@ var TuiApp = class TuiApp {
|
|
|
15121
15553
|
* FORCE_COLOR=0 / CI) stay dark without querying; a terminal that never
|
|
15122
15554
|
* answers falls back to COLORFGBG (VT100/xterm convention); a terminal
|
|
15123
15555
|
* with neither leaves the current theme untouched.
|
|
15556
|
+
*
|
|
15557
|
+
* The query targets the screen that OWNS the terminal input handler (the
|
|
15558
|
+
* alt screen in fullscreen mode): a query on the stopped main screen
|
|
15559
|
+
* would have its reply swallowed by the alt screen's OSC 11 consumer and
|
|
15560
|
+
* time out, silently turning `auto` into a no-op.
|
|
15561
|
+
*
|
|
15562
|
+
* Concurrent calls COALESCE onto one shared in-flight query — overlapping
|
|
15563
|
+
* OSC 11 queries have no sequence ids, so replies would mis-pair by FIFO
|
|
15564
|
+
* order. The LATEST {@link AutoDetectOptions.shouldApply} guard is
|
|
15565
|
+
* consulted at settle time, so a late result can never override a newer
|
|
15566
|
+
* explicit theme choice.
|
|
15567
|
+
* @param options.shouldApply - returns false to drop the settled result
|
|
15568
|
+
* instead of applying it (default: always apply).
|
|
15124
15569
|
*/
|
|
15125
|
-
async autoDetectTheme() {
|
|
15570
|
+
async autoDetectTheme(options) {
|
|
15126
15571
|
if (themeOptOut()) return;
|
|
15127
|
-
|
|
15572
|
+
if (options?.shouldApply !== void 0) this.autoDetectGuard = options.shouldApply;
|
|
15573
|
+
if (this.autoDetectInFlight === void 0) this.autoDetectInFlight = this.runAutoDetect().finally(() => {
|
|
15574
|
+
this.autoDetectInFlight = void 0;
|
|
15575
|
+
this.autoDetectGuard = void 0;
|
|
15576
|
+
});
|
|
15577
|
+
return this.autoDetectInFlight;
|
|
15578
|
+
}
|
|
15579
|
+
async runAutoDetect() {
|
|
15580
|
+
const rgb = await this.overlayHost.queryTerminalBackgroundColor({ timeoutMs: 800 });
|
|
15581
|
+
const guard = this.autoDetectGuard;
|
|
15582
|
+
const apply = () => guard === void 0 || guard();
|
|
15128
15583
|
if (rgb !== void 0) {
|
|
15129
|
-
this.applyTheme(detectThemeFromBackground(rgb));
|
|
15584
|
+
if (apply()) this.applyTheme(detectThemeFromBackground(rgb));
|
|
15130
15585
|
return;
|
|
15131
15586
|
}
|
|
15132
15587
|
const fromEnv = detectThemeFromColorFgBg();
|
|
15133
|
-
if (fromEnv !== void 0) this.applyTheme(fromEnv);
|
|
15588
|
+
if (fromEnv !== void 0 && apply()) this.applyTheme(fromEnv);
|
|
15589
|
+
}
|
|
15590
|
+
/**
|
|
15591
|
+
* Follow the terminal's live colour-scheme reports while `following` is
|
|
15592
|
+
* true. Enabling sends one DSR 996 query — xterm-class terminals only
|
|
15593
|
+
* START reporting after being asked — and reported schemes fan out to the
|
|
15594
|
+
* {@link onTerminalThemeChange} listeners (which guard whether to apply,
|
|
15595
|
+
* typically against the persisted `auto` preference). Disabling stops
|
|
15596
|
+
* tracking. Idempotent.
|
|
15597
|
+
* @param following - whether to track and apply live scheme reports.
|
|
15598
|
+
*/
|
|
15599
|
+
trackTerminalTheme(following) {
|
|
15600
|
+
if (following === this.followingTerminalTheme) return;
|
|
15601
|
+
this.followingTerminalTheme = following;
|
|
15602
|
+
if (!following) return;
|
|
15603
|
+
this.overlayHost.queryTerminalColorScheme({ timeoutMs: 800 }).then((scheme) => {
|
|
15604
|
+
if (scheme === void 0 || !this.followingTerminalTheme) return;
|
|
15605
|
+
for (const listener of [...this.terminalSchemeListeners]) listener(scheme);
|
|
15606
|
+
}).catch(() => {});
|
|
15134
15607
|
}
|
|
15135
15608
|
/**
|
|
15136
15609
|
* Register a live terminal-theme listener (colour-scheme reports). The
|
|
15137
|
-
*
|
|
15610
|
+
* listener is registered on EVERY screen: reports arrive only at the
|
|
15611
|
+
* screen that owns the terminal input handler (the alt screen in
|
|
15612
|
+
* fullscreen mode), so the registrations never double-fire. The caller
|
|
15613
|
+
* guards whether to apply (typically: only while the persisted preference
|
|
15614
|
+
* is `auto`).
|
|
15138
15615
|
* @param listener - receives the detected palette family.
|
|
15139
15616
|
* @returns a disposer.
|
|
15140
15617
|
*/
|
|
15141
15618
|
onTerminalThemeChange(listener) {
|
|
15142
|
-
|
|
15619
|
+
this.terminalSchemeListeners.add(listener);
|
|
15620
|
+
this.refreshSchemeRegistrations();
|
|
15621
|
+
return () => {
|
|
15622
|
+
this.terminalSchemeListeners.delete(listener);
|
|
15623
|
+
this.refreshSchemeRegistrations();
|
|
15624
|
+
};
|
|
15625
|
+
}
|
|
15626
|
+
/** (Re)register the scheme-report fan-out on every screen. */
|
|
15627
|
+
refreshSchemeRegistrations() {
|
|
15628
|
+
for (const dispose of this.schemeDisposers) dispose();
|
|
15629
|
+
this.schemeDisposers = [];
|
|
15630
|
+
const screens = [this.tui];
|
|
15631
|
+
if (this.fullscreen !== void 0) screens.push(this.fullscreen);
|
|
15632
|
+
for (const screen of screens) this.schemeDisposers.push(screen.onTerminalColorSchemeChange((scheme) => {
|
|
15633
|
+
for (const listener of [...this.terminalSchemeListeners]) listener(scheme);
|
|
15634
|
+
}));
|
|
15143
15635
|
}
|
|
15144
15636
|
/**
|
|
15145
15637
|
* Queue an approval prompt and resolve when the user decides. Requests
|
|
@@ -15181,19 +15673,31 @@ var TuiApp = class TuiApp {
|
|
|
15181
15673
|
}
|
|
15182
15674
|
/** Build and mount the approval dialog for one prompt on the active screen. */
|
|
15183
15675
|
renderApprovalDialog(pending) {
|
|
15676
|
+
const width = this.terminal.columns;
|
|
15677
|
+
const maxHeight = Math.max(8, Math.min(16, this.terminal.rows - 2));
|
|
15678
|
+
const contentWidth = Math.max(1, width - 8);
|
|
15679
|
+
const titleShown = truncateToWidth(`Approve ${pending.request.toolName}?`, contentWidth, "…");
|
|
15680
|
+
const dangerShown = pending.request.danger === true ? truncateToWidth("⚠ DANGEROUS COMMAND — confirm carefully", contentWidth, "…") : "";
|
|
15681
|
+
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;
|
|
15682
|
+
const hintWrapped = hintShown === "" ? 0 : wrapTextWithAnsi(hintShown, contentWidth).length;
|
|
15683
|
+
const chrome = 1 + (dangerShown === "" ? 0 : 1) + 1 + hintWrapped + 2 + 2;
|
|
15684
|
+
const reasonBudget = Math.max(0, maxHeight - chrome);
|
|
15685
|
+
const reasonShown = capWrappedToMarker(pending.request.reason ?? "", contentWidth, reasonBudget).text;
|
|
15686
|
+
const reasonWrapped = reasonShown === "" ? 0 : wrapTextWithAnsi(reasonShown, contentWidth).length;
|
|
15687
|
+
const previewBudget = Math.max(0, maxHeight - chrome - reasonWrapped);
|
|
15184
15688
|
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(
|
|
15689
|
+
dialog.addChild(new Text(titleShown, 1, 0));
|
|
15690
|
+
if (dangerShown !== "") dialog.addChild(new Text(color.error(dangerShown), 1, 0));
|
|
15691
|
+
if (pending.request.arguments !== void 0 && pending.request.arguments !== "" && previewBudget > 0) {
|
|
15692
|
+
const previewShown = capWrappedToMarker(pending.request.arguments.split("\n").slice(0, 6).join("\n"), contentWidth, previewBudget).text;
|
|
15693
|
+
if (previewShown !== "") dialog.addChild(new Text(color.textDim(previewShown), 1, 0));
|
|
15694
|
+
}
|
|
15695
|
+
if (reasonShown !== "") dialog.addChild(new Text(reasonShown, 1, 0));
|
|
15696
|
+
dialog.addChild(new Text(" ", 1, 0));
|
|
15697
|
+
dialog.addChild(new Text(hintShown, 1, 0));
|
|
15194
15698
|
pending.handle = this.showOverlayOnHost(new Frame(dialog), {
|
|
15195
|
-
width
|
|
15196
|
-
maxHeight
|
|
15699
|
+
width,
|
|
15700
|
+
maxHeight
|
|
15197
15701
|
});
|
|
15198
15702
|
}
|
|
15199
15703
|
/** Route a key while a prompt is showing; every key is consumed. */
|
|
@@ -15252,6 +15756,7 @@ var TuiApp = class TuiApp {
|
|
|
15252
15756
|
...question.multiSelect !== void 0 ? { multiSelect: question.multiSelect } : {},
|
|
15253
15757
|
...question.intent !== void 0 ? { intent: question.intent } : {}
|
|
15254
15758
|
})), (answers) => this.settleQuestions(state, answers), () => this.settleQuestions(state, void 0)),
|
|
15759
|
+
suspendedOverlays: /* @__PURE__ */ new Set(),
|
|
15255
15760
|
resolve,
|
|
15256
15761
|
reject,
|
|
15257
15762
|
signal
|
|
@@ -15269,13 +15774,20 @@ var TuiApp = class TuiApp {
|
|
|
15269
15774
|
else this.questionQueue.push(state);
|
|
15270
15775
|
});
|
|
15271
15776
|
}
|
|
15272
|
-
/** Mount one flow
|
|
15777
|
+
/** Mount one flow into the editor seat and make it the active one. */
|
|
15273
15778
|
presentQuestion(state) {
|
|
15274
15779
|
this.activeQuestions = state;
|
|
15275
|
-
|
|
15276
|
-
|
|
15277
|
-
|
|
15278
|
-
}
|
|
15780
|
+
for (const handle of this.overlayHandles) if (!handle.isHidden()) {
|
|
15781
|
+
handle.setHidden(true);
|
|
15782
|
+
state.suspendedOverlays.add(handle);
|
|
15783
|
+
}
|
|
15784
|
+
const frame = new QuestionFrame(state.flow, () => this.terminal.rows);
|
|
15785
|
+
state.frame = frame;
|
|
15786
|
+
this.editorSeat.clear();
|
|
15787
|
+
this.editorSeat.addChild(frame);
|
|
15788
|
+
const screen = this.fullscreen ?? this.tui;
|
|
15789
|
+
screen.setFocus(frame);
|
|
15790
|
+
screen.requestRender();
|
|
15279
15791
|
}
|
|
15280
15792
|
/** Abort one flow (its signal fired). The ACTIVE flow settles rejected
|
|
15281
15793
|
* and the queue advances; a QUEUED flow is removed from the queue and
|
|
@@ -15293,12 +15805,6 @@ var TuiApp = class TuiApp {
|
|
|
15293
15805
|
if (index !== -1) this.questionQueue.splice(index, 1);
|
|
15294
15806
|
state.reject(cancellationError("question flow cancelled"));
|
|
15295
15807
|
}
|
|
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
15808
|
/** Cancel every pending flow: each promise rejects exactly once. */
|
|
15303
15809
|
cancelQuestionFlows() {
|
|
15304
15810
|
for (const state of [...this.questionQueue]) this.abortQuestion(state);
|
|
@@ -15316,13 +15822,36 @@ var TuiApp = class TuiApp {
|
|
|
15316
15822
|
settleQuestions(state, answers) {
|
|
15317
15823
|
if (this.activeQuestions !== state || state.settled === true) return;
|
|
15318
15824
|
state.settled = true;
|
|
15319
|
-
this.activeQuestions = void 0;
|
|
15320
|
-
state.handle?.hide();
|
|
15321
15825
|
if (state.onAbort !== void 0 && state.signal !== void 0) state.signal.removeEventListener("abort", state.onAbort);
|
|
15322
|
-
this.
|
|
15826
|
+
const next = this.questionQueue.shift();
|
|
15827
|
+
if (next !== void 0) {
|
|
15828
|
+
next.suspendedOverlays = state.suspendedOverlays;
|
|
15829
|
+
state.suspendedOverlays = /* @__PURE__ */ new Set();
|
|
15830
|
+
const frame = new QuestionFrame(next.flow, () => this.terminal.rows);
|
|
15831
|
+
next.frame = frame;
|
|
15832
|
+
this.editorSeat.clear();
|
|
15833
|
+
this.editorSeat.addChild(frame);
|
|
15834
|
+
this.activeQuestions = next;
|
|
15835
|
+
const screen = this.fullscreen ?? this.tui;
|
|
15836
|
+
screen.setFocus(frame);
|
|
15837
|
+
screen.requestRender();
|
|
15838
|
+
this.settle(state, answers);
|
|
15839
|
+
return;
|
|
15840
|
+
}
|
|
15841
|
+
this.editorSeat.clear();
|
|
15842
|
+
this.editorSeat.addChild(this.editor);
|
|
15843
|
+
this.activeQuestions = void 0;
|
|
15844
|
+
const screen = this.fullscreen ?? this.tui;
|
|
15845
|
+
screen.setFocus(this.editor);
|
|
15846
|
+
for (const handle of state.suspendedOverlays) if (this.overlayHandles.has(handle)) handle.setHidden(false);
|
|
15847
|
+
state.suspendedOverlays.clear();
|
|
15848
|
+
screen.requestRender();
|
|
15849
|
+
this.settle(state, answers);
|
|
15850
|
+
}
|
|
15851
|
+
/** Resolve or reject the settled promise (exactly once, by construction). */
|
|
15852
|
+
settle(state, answers) {
|
|
15323
15853
|
if (answers === void 0) state.reject(cancellationError("question flow cancelled"));
|
|
15324
15854
|
else state.resolve(answers);
|
|
15325
|
-
this.nextQuestion();
|
|
15326
15855
|
}
|
|
15327
15856
|
};
|
|
15328
15857
|
const dim = color.textDim;
|
|
@@ -15336,6 +15865,101 @@ function startProcessTui(events, options = {}) {
|
|
|
15336
15865
|
return app;
|
|
15337
15866
|
}
|
|
15338
15867
|
//#endregion
|
|
15868
|
+
//#region src/tasks-browser.ts
|
|
15869
|
+
/**
|
|
15870
|
+
* Task-browser row model — the merged view over the two background surfaces.
|
|
15871
|
+
*
|
|
15872
|
+
* Source A — the jobs registry (`ctx.jobs`): bash jobs and one-shot
|
|
15873
|
+
* subagent jobs. A job row is STATUS-ONLY:
|
|
15874
|
+
* - a bash job's output has a single read cursor owned by the model's
|
|
15875
|
+
* `job_output`; consuming it from the UI would swallow the main
|
|
15876
|
+
* session's result;
|
|
15877
|
+
* - a one-shot subagent job record carries no child session id, so it can
|
|
15878
|
+
* never be matched to its child transcript — label/order/time are never
|
|
15879
|
+
* identity (see subagentJobTranscriptId).
|
|
15880
|
+
*
|
|
15881
|
+
* Source B — the subagent registry (`ctx.subagents.listChildren`):
|
|
15882
|
+
* live child subagents. Continuable children are separate durable
|
|
15883
|
+
* conversations that deliver no result to the parent, so Enter may open
|
|
15884
|
+
* the read-only transcript viewer directly. A RUNNING one-shot child
|
|
15885
|
+
* (the parent's pending foreground tool call, which registers no job
|
|
15886
|
+
* record) is merged the same way, so the ↓ trigger stays armed while a
|
|
15887
|
+
* foreground delegation is in flight. A finished one-shot child is not
|
|
15888
|
+
* merged — its work is over and `/subagents` remains its surface.
|
|
15889
|
+
*
|
|
15890
|
+
* Deliberate overlap: a running BACKGROUND one-shot has BOTH a job record
|
|
15891
|
+
* and a child record with no cross-reference, so it may appear twice — the
|
|
15892
|
+
* job row (status-only) and the child row (transcript viewable). Dedup is
|
|
15893
|
+
* impossible without the missing childSessionId on the job record; the
|
|
15894
|
+
* viewable child row is strictly more useful, so the overlap is accepted.
|
|
15895
|
+
*
|
|
15896
|
+
* Pure and injectable so row typing, ordering, and descriptions are
|
|
15897
|
+
* unit-testable without any dsh service.
|
|
15898
|
+
* @module @xmoon76/dsh-pi-tui/tasks-browser
|
|
15899
|
+
*/
|
|
15900
|
+
/** Picker-value prefix for a subagent row. */
|
|
15901
|
+
const AGENT_ROW_PREFIX = "agent:";
|
|
15902
|
+
/** Picker-value prefix for a job row. */
|
|
15903
|
+
const JOB_ROW_PREFIX = "job:";
|
|
15904
|
+
/** Picker group label for continuable subagents. */
|
|
15905
|
+
const SUBAGENT_GROUP = "subagents";
|
|
15906
|
+
/** Picker group label for jobs. */
|
|
15907
|
+
const JOB_GROUP = "jobs";
|
|
15908
|
+
const isActiveJob = (status) => status === "running" || status === "stopping";
|
|
15909
|
+
/**
|
|
15910
|
+
* Ordering shared with kimi's tasks browser: active rows first, terminal
|
|
15911
|
+
* rows last. Jobs keep their registry order within each class (active by
|
|
15912
|
+
* startedAt, terminal newest-finish first); subagents keep the registry's
|
|
15913
|
+
* createdAt order (running before inactive).
|
|
15914
|
+
*/
|
|
15915
|
+
function buildTaskRows(jobs, agents) {
|
|
15916
|
+
const sortedAgents = [...agents.filter((agent) => agent.kind === "child" && (agent.mode === "continuable" || agent.mode === "one-shot" && agent.activity === "running")).map((agent) => ({
|
|
15917
|
+
kind: "subagent",
|
|
15918
|
+
value: `${AGENT_ROW_PREFIX}${agent.id}`,
|
|
15919
|
+
childId: agent.id,
|
|
15920
|
+
label: agent.label ?? agent.id,
|
|
15921
|
+
activity: agent.activity ?? "inactive",
|
|
15922
|
+
hasChildren: agent.hasChildren ?? false
|
|
15923
|
+
}))].sort((a, b) => {
|
|
15924
|
+
if (a.activity !== b.activity) return a.activity === "running" ? -1 : 1;
|
|
15925
|
+
return 0;
|
|
15926
|
+
});
|
|
15927
|
+
const sortedJobs = [...jobs].sort((a, b) => {
|
|
15928
|
+
const aTerminal = !isActiveJob(a.status);
|
|
15929
|
+
if (aTerminal !== !isActiveJob(b.status)) return aTerminal ? 1 : -1;
|
|
15930
|
+
if (!aTerminal) return a.startedAt - b.startedAt;
|
|
15931
|
+
return (b.finishedAt ?? b.startedAt) - (a.finishedAt ?? a.startedAt);
|
|
15932
|
+
}).map((job) => ({
|
|
15933
|
+
kind: "job",
|
|
15934
|
+
value: `${JOB_ROW_PREFIX}${job.id}`,
|
|
15935
|
+
jobId: job.id,
|
|
15936
|
+
jobKind: job.kind,
|
|
15937
|
+
label: job.label,
|
|
15938
|
+
status: job.status,
|
|
15939
|
+
detail: job.detail,
|
|
15940
|
+
startedAt: job.startedAt,
|
|
15941
|
+
finishedAt: job.finishedAt
|
|
15942
|
+
}));
|
|
15943
|
+
return [...sortedAgents, ...sortedJobs];
|
|
15944
|
+
}
|
|
15945
|
+
/** The one-line picker label for a row. */
|
|
15946
|
+
function taskRowLabel(row) {
|
|
15947
|
+
if (row.kind === "job") return `${row.jobKind} · ${row.label}`;
|
|
15948
|
+
return `subagent · ${row.label}`;
|
|
15949
|
+
}
|
|
15950
|
+
/** The picker group a row belongs to. */
|
|
15951
|
+
function rowGroup(row) {
|
|
15952
|
+
return row.kind === "job" ? JOB_GROUP : SUBAGENT_GROUP;
|
|
15953
|
+
}
|
|
15954
|
+
/** The picker description line for a row. */
|
|
15955
|
+
function describeTaskRow(row, now) {
|
|
15956
|
+
if (row.kind === "job") {
|
|
15957
|
+
const elapsed = Math.max(0, Math.floor((now - row.startedAt) / 1e3));
|
|
15958
|
+
return `${row.status}${row.detail === void 0 ? "" : ` — ${row.detail}`} · ${elapsed}s`;
|
|
15959
|
+
}
|
|
15960
|
+
return `${row.activity}${row.hasChildren ? " · has children" : ""}`;
|
|
15961
|
+
}
|
|
15962
|
+
//#endregion
|
|
15339
15963
|
//#region src/model-menu.ts
|
|
15340
15964
|
/**
|
|
15341
15965
|
* In-place submenu components for `/model`: the model list and the
|
|
@@ -15619,10 +16243,10 @@ async function loadSessionTitles(query, persistence, ids, signal) {
|
|
|
15619
16243
|
//#region src/commands.ts
|
|
15620
16244
|
/**
|
|
15621
16245
|
* The TUI-owned slash commands (/exit /settings /sessions /skill /model
|
|
15622
|
-
* /new /tasks /preset /subagents /search /title /copy /export
|
|
15623
|
-
* /status /login /logout /help), extracted from the runner's
|
|
15624
|
-
* apply() so the registration surface is testable and the runner
|
|
15625
|
-
* shrinks. Every command reads the live runner state through the
|
|
16246
|
+
* /new /tasks /preset /subagents /search /title /rename /copy /export
|
|
16247
|
+
* /fork /status /login /logout /help), extracted from the runner's
|
|
16248
|
+
* monolithic apply() so the registration surface is testable and the runner
|
|
16249
|
+
* closure shrinks. Every command reads the live runner state through the
|
|
15626
16250
|
* {@link TuiCommandRunner} interface, whose accessors re-read the current
|
|
15627
16251
|
* agent/settings on every access (sessions can swap the live agent).
|
|
15628
16252
|
* @module @xmoon76/dsh-pi-tui/commands
|
|
@@ -15647,6 +16271,47 @@ function metaOf(cwd, presetId) {
|
|
|
15647
16271
|
};
|
|
15648
16272
|
}
|
|
15649
16273
|
/**
|
|
16274
|
+
* Display copy for the four shipped agent presets, fixed in English — the
|
|
16275
|
+
* web surface's `BUILT_IN_PRESET_KEYS` mapping (`dsh-client-ui-agent-preset`),
|
|
16276
|
+
* TUI-side. The EFFECTIVE roster root is the dsh install's own
|
|
16277
|
+
* `config/agent-presets`: the dsh CLI's profile composition replaces this
|
|
16278
|
+
* bundle's shipped root with that one at boot (the `composeProfile`
|
|
16279
|
+
* agent-presets overlay), and its preset.yml language is not ours to
|
|
16280
|
+
* control. Mapping the known ids keeps the picker English regardless of
|
|
16281
|
+
* what the files say; everything else renders file metadata.
|
|
16282
|
+
*/
|
|
16283
|
+
const BUILT_IN_PRESET_COPY = {
|
|
16284
|
+
standard: {
|
|
16285
|
+
name: "Standard mode",
|
|
16286
|
+
description: "Full coding agent with file editing, shell, file and web search, skills, planning, goals, subagents, and workflows."
|
|
16287
|
+
},
|
|
16288
|
+
code: {
|
|
16289
|
+
name: "Code mode",
|
|
16290
|
+
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."
|
|
16291
|
+
},
|
|
16292
|
+
minimal: {
|
|
16293
|
+
name: "Minimal mode",
|
|
16294
|
+
description: "Two-tool coding agent with persistent bash and str_replace_editor."
|
|
16295
|
+
},
|
|
16296
|
+
cordis: {
|
|
16297
|
+
name: "Creator mode",
|
|
16298
|
+
description: "Built for creating custom agent presets, with all Standard mode capabilities plus runtime inspection, plugin experiments, and preset-authoring guidance."
|
|
16299
|
+
}
|
|
16300
|
+
};
|
|
16301
|
+
/** Resolve one roster row's display copy: fixed English for a shipped
|
|
16302
|
+
* (system-trust) preset id, otherwise the preset's file metadata. */
|
|
16303
|
+
function presetDisplayText(preset) {
|
|
16304
|
+
const builtIn = preset.trust === "system" ? BUILT_IN_PRESET_COPY[preset.id] : void 0;
|
|
16305
|
+
if (builtIn !== void 0) return {
|
|
16306
|
+
name: builtIn.name,
|
|
16307
|
+
description: builtIn.description
|
|
16308
|
+
};
|
|
16309
|
+
return {
|
|
16310
|
+
name: preset.name ?? preset.id,
|
|
16311
|
+
...preset.description === void 0 ? {} : { description: preset.description }
|
|
16312
|
+
};
|
|
16313
|
+
}
|
|
16314
|
+
/**
|
|
15650
16315
|
* Register the TUI-owned slash commands on the commands service. The
|
|
15651
16316
|
* completion list is refreshed after every registration so TUI-owned
|
|
15652
16317
|
* commands appear in the editor's tab list. Registration is sessionless:
|
|
@@ -15730,6 +16395,7 @@ function registerTuiCommands(runner) {
|
|
|
15730
16395
|
const tuiSettings = runner.tuiSettings;
|
|
15731
16396
|
const theme = tuiSettings?.get().theme ?? "auto";
|
|
15732
16397
|
const themeValue = theme.startsWith("custom:") ? theme.slice(7) : theme;
|
|
16398
|
+
let lastThemeChoice = themeValue;
|
|
15733
16399
|
const settings = ctx.get("settings");
|
|
15734
16400
|
const permissionNames = [...ctx.get("permissionPresets")?.names ?? []];
|
|
15735
16401
|
let defaultPermission;
|
|
@@ -15784,6 +16450,13 @@ function registerTuiCommands(runner) {
|
|
|
15784
16450
|
currentValue: app.getFooterPreset(),
|
|
15785
16451
|
values: ["full", "compact"]
|
|
15786
16452
|
},
|
|
16453
|
+
{
|
|
16454
|
+
id: "busy-enter",
|
|
16455
|
+
label: "Enter while busy",
|
|
16456
|
+
description: "Steer injects the draft into the running turn; Ctrl+Enter uses the other behavior",
|
|
16457
|
+
currentValue: tuiSettings?.get().busyEnter ?? "queue",
|
|
16458
|
+
values: ["queue", "steer"]
|
|
16459
|
+
},
|
|
15787
16460
|
{
|
|
15788
16461
|
id: "fullscreen",
|
|
15789
16462
|
label: "Fullscreen",
|
|
@@ -15837,12 +16510,19 @@ function registerTuiCommands(runner) {
|
|
|
15837
16510
|
}]), { notify: true });
|
|
15838
16511
|
} else if (id === "theme") {
|
|
15839
16512
|
if (value === "auto" || value === "dark" || value === "light" || customThemeNames().includes(value)) {
|
|
15840
|
-
|
|
15841
|
-
|
|
15842
|
-
|
|
16513
|
+
lastThemeChoice = value;
|
|
16514
|
+
if (value === "auto") {
|
|
16515
|
+
detach("theme autodetect", () => app.autoDetectTheme({ shouldApply: () => lastThemeChoice === "auto" }));
|
|
16516
|
+
app.trackTerminalTheme(true);
|
|
16517
|
+
} else if (value === "dark" || value === "light") {
|
|
16518
|
+
app.applyTheme(value);
|
|
16519
|
+
app.trackTerminalTheme(false);
|
|
16520
|
+
} else {
|
|
15843
16521
|
const palette = loadCustomTheme(value);
|
|
15844
|
-
if (palette !== void 0)
|
|
15845
|
-
|
|
16522
|
+
if (palette !== void 0) {
|
|
16523
|
+
app.applyPalette(palette);
|
|
16524
|
+
app.trackTerminalTheme(false);
|
|
16525
|
+
} else {
|
|
15846
16526
|
app.notify(`theme ${value} not found`, "error");
|
|
15847
16527
|
return;
|
|
15848
16528
|
}
|
|
@@ -15865,6 +16545,14 @@ function registerTuiCommands(runner) {
|
|
|
15865
16545
|
footer: value
|
|
15866
16546
|
}), { notify: true });
|
|
15867
16547
|
}
|
|
16548
|
+
} else if (id === "busy-enter") {
|
|
16549
|
+
if (value === "queue" || value === "steer") {
|
|
16550
|
+
const settings = tuiSettings;
|
|
16551
|
+
if (settings !== void 0) detach("settings busy enter write", () => settings.replace({
|
|
16552
|
+
...settings.get(),
|
|
16553
|
+
busyEnter: value
|
|
16554
|
+
}), { notify: true });
|
|
16555
|
+
}
|
|
15868
16556
|
} else if (id === "fullscreen") {
|
|
15869
16557
|
if (value === "off" || value === "on") app.setFullscreen(value === "on");
|
|
15870
16558
|
}
|
|
@@ -16058,11 +16746,17 @@ function registerTuiCommands(runner) {
|
|
|
16058
16746
|
const settings = runner.tuiSettings;
|
|
16059
16747
|
if (settings !== void 0) {
|
|
16060
16748
|
const doc = settings.get();
|
|
16061
|
-
|
|
16062
|
-
|
|
16063
|
-
|
|
16064
|
-
|
|
16749
|
+
const reloadTheme = doc.theme;
|
|
16750
|
+
if (reloadTheme === "auto") {
|
|
16751
|
+
detach("theme autodetect", () => app.autoDetectTheme({ shouldApply: () => reloadTheme === "auto" }));
|
|
16752
|
+
app.trackTerminalTheme(true);
|
|
16753
|
+
} else if (reloadTheme === "dark" || reloadTheme === "light") {
|
|
16754
|
+
app.applyTheme(reloadTheme);
|
|
16755
|
+
app.trackTerminalTheme(false);
|
|
16756
|
+
} else if (reloadTheme.startsWith("custom:")) {
|
|
16757
|
+
const palette = loadCustomTheme(reloadTheme.slice(7));
|
|
16065
16758
|
if (palette !== void 0) app.applyPalette(palette);
|
|
16759
|
+
app.trackTerminalTheme(false);
|
|
16066
16760
|
}
|
|
16067
16761
|
app.setFooterPreset(doc.footer === "compact" ? "compact" : "full");
|
|
16068
16762
|
app.setFullscreen(doc.fullscreen === "on");
|
|
@@ -16128,7 +16822,7 @@ function registerTuiCommands(runner) {
|
|
|
16128
16822
|
description: "Start a fresh session in this workspace",
|
|
16129
16823
|
handler: async () => {
|
|
16130
16824
|
const liveAgent = runner.liveAgent;
|
|
16131
|
-
const composition = await runner.compose();
|
|
16825
|
+
const composition = await runner.compose(runner.pendingPreset);
|
|
16132
16826
|
const presetId = composition.agentPreset;
|
|
16133
16827
|
const next = await runner.agents.create({
|
|
16134
16828
|
sessionId: SessionId(`session-${randomUUID()}`),
|
|
@@ -16293,13 +16987,13 @@ function registerTuiCommands(runner) {
|
|
|
16293
16987
|
description: "Show or switch the session agent preset",
|
|
16294
16988
|
input: { hint: "[status|<id>|default [<id>]]" },
|
|
16295
16989
|
handler: async (invocation) => {
|
|
16296
|
-
const liveAgent = await requireAgent();
|
|
16297
16990
|
const presets = ctx.get("agentPresets");
|
|
16298
16991
|
if (presets === void 0) return {
|
|
16299
16992
|
kind: "error",
|
|
16300
16993
|
text: "agent presets unavailable in this deployment"
|
|
16301
16994
|
};
|
|
16302
|
-
const
|
|
16995
|
+
const liveAgent = runner.liveAgent;
|
|
16996
|
+
const current = liveAgent === void 0 ? void 0 : presets.composedPreset(liveAgent.ctx) ?? resolveSessionPreset(liveAgent.session);
|
|
16303
16997
|
const matched = invocation.rawInput.trim().match(/^(\S+)(?:\s+(.*))?$/);
|
|
16304
16998
|
const verb = matched?.[1] ?? "";
|
|
16305
16999
|
const rest = matched?.[2]?.trim() ?? "";
|
|
@@ -16328,14 +17022,53 @@ function registerTuiCommands(runner) {
|
|
|
16328
17022
|
text: `default preset set: ${rest}`
|
|
16329
17023
|
};
|
|
16330
17024
|
}
|
|
16331
|
-
|
|
16332
|
-
|
|
17025
|
+
const applyPresetSelection = async (id) => {
|
|
17026
|
+
if (liveAgent === void 0) {
|
|
17027
|
+
const resolved = await presets.resolve(id);
|
|
17028
|
+
runner.pendingPreset = resolved.id;
|
|
17029
|
+
return {
|
|
17030
|
+
kind: "pending",
|
|
17031
|
+
preset: resolved.id
|
|
17032
|
+
};
|
|
17033
|
+
}
|
|
17034
|
+
const outcome = await runner.recomposeBlank(id);
|
|
16333
17035
|
if (outcome.kind === "locked") return {
|
|
16334
|
-
kind: "
|
|
16335
|
-
|
|
17036
|
+
kind: "locked",
|
|
17037
|
+
sessionId: liveAgent.session.id
|
|
16336
17038
|
};
|
|
16337
17039
|
refreshCompletions();
|
|
16338
17040
|
runner.updateWelcomeCard();
|
|
17041
|
+
return {
|
|
17042
|
+
kind: "switched",
|
|
17043
|
+
preset: outcome.preset
|
|
17044
|
+
};
|
|
17045
|
+
};
|
|
17046
|
+
const pickPreset = async (id) => {
|
|
17047
|
+
let outcome;
|
|
17048
|
+
try {
|
|
17049
|
+
outcome = await applyPresetSelection(id);
|
|
17050
|
+
} catch (error) {
|
|
17051
|
+
app.notify(safeErrorMessage(error), "error");
|
|
17052
|
+
return;
|
|
17053
|
+
}
|
|
17054
|
+
if (outcome.kind === "pending") app.notify(`new sessions will start on preset ${outcome.preset}`, "info");
|
|
17055
|
+
else if (outcome.kind === "switched") app.notify(`session preset switched to ${outcome.preset}`, "info");
|
|
17056
|
+
else app.notify(`session "${outcome.sessionId}" has already started; its agent preset is fixed — preset switching is only available in a new session`, "error");
|
|
17057
|
+
};
|
|
17058
|
+
if (verb !== "") try {
|
|
17059
|
+
const outcome = await applyPresetSelection(verb);
|
|
17060
|
+
if (outcome.kind === "pending") return {
|
|
17061
|
+
kind: "success",
|
|
17062
|
+
text: `new sessions will start on preset ${outcome.preset}`
|
|
17063
|
+
};
|
|
17064
|
+
if (outcome.kind === "locked") {
|
|
17065
|
+
const message = `session "${outcome.sessionId}" has already started; its agent preset is fixed — preset switching is only available in a new session`;
|
|
17066
|
+
app.notify(message, "error");
|
|
17067
|
+
return {
|
|
17068
|
+
kind: "error",
|
|
17069
|
+
text: message
|
|
17070
|
+
};
|
|
17071
|
+
}
|
|
16339
17072
|
return {
|
|
16340
17073
|
kind: "success",
|
|
16341
17074
|
text: `session preset switched to ${outcome.preset}`
|
|
@@ -16351,17 +17084,33 @@ function registerTuiCommands(runner) {
|
|
|
16351
17084
|
kind: "success",
|
|
16352
17085
|
text: "no agent presets configured"
|
|
16353
17086
|
};
|
|
16354
|
-
|
|
16355
|
-
|
|
16356
|
-
|
|
16357
|
-
|
|
16358
|
-
|
|
16359
|
-
|
|
16360
|
-
|
|
16361
|
-
|
|
16362
|
-
|
|
16363
|
-
|
|
16364
|
-
|
|
17087
|
+
if (liveAgent !== void 0 && liveAgent.session.events.some((event) => event.type === "turn/start")) {
|
|
17088
|
+
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)`;
|
|
17089
|
+
app.notify(message, "error");
|
|
17090
|
+
return {
|
|
17091
|
+
kind: "error",
|
|
17092
|
+
text: message
|
|
17093
|
+
};
|
|
17094
|
+
}
|
|
17095
|
+
const close = app.openSettings(roster.map((preset) => {
|
|
17096
|
+
const display = presetDisplayText(preset);
|
|
17097
|
+
return {
|
|
17098
|
+
id: preset.id,
|
|
17099
|
+
label: `${display.name} (${preset.id})`,
|
|
17100
|
+
description: [
|
|
17101
|
+
display.description,
|
|
17102
|
+
preset.trust === "system" ? "system" : "user",
|
|
17103
|
+
preset.id === presets.defaultId ? "default" : void 0,
|
|
17104
|
+
preset.id === current ? "← current" : void 0,
|
|
17105
|
+
preset.broken
|
|
17106
|
+
].filter(Boolean).join(" · "),
|
|
17107
|
+
currentValue: "",
|
|
17108
|
+
values: [preset.id]
|
|
17109
|
+
};
|
|
17110
|
+
}), (id) => {
|
|
17111
|
+
close();
|
|
17112
|
+
detach("preset pick", () => pickPreset(id));
|
|
17113
|
+
}, () => {});
|
|
16365
17114
|
return { kind: "success" };
|
|
16366
17115
|
}
|
|
16367
17116
|
});
|
|
@@ -16469,31 +17218,54 @@ function registerTuiCommands(runner) {
|
|
|
16469
17218
|
return { kind: "success" };
|
|
16470
17219
|
}
|
|
16471
17220
|
});
|
|
16472
|
-
|
|
16473
|
-
|
|
16474
|
-
|
|
16475
|
-
|
|
16476
|
-
|
|
16477
|
-
|
|
16478
|
-
|
|
16479
|
-
|
|
16480
|
-
|
|
16481
|
-
|
|
16482
|
-
|
|
16483
|
-
|
|
16484
|
-
if (name === "") {
|
|
16485
|
-
const current = titles.get(liveAgent.session);
|
|
17221
|
+
const titleHandler = async (invocation) => {
|
|
17222
|
+
const liveAgent = await requireAgent();
|
|
17223
|
+
const titles = ctx.get("sessionTitle");
|
|
17224
|
+
if (titles === void 0) return {
|
|
17225
|
+
kind: "error",
|
|
17226
|
+
text: "session title service unavailable"
|
|
17227
|
+
};
|
|
17228
|
+
const name = invocation.rawInput.trim();
|
|
17229
|
+
if (name !== "") {
|
|
17230
|
+
try {
|
|
17231
|
+
titles.rename(liveAgent.session, name);
|
|
17232
|
+
} catch (error) {
|
|
16486
17233
|
return {
|
|
16487
|
-
kind: "
|
|
16488
|
-
text:
|
|
17234
|
+
kind: "error",
|
|
17235
|
+
text: safeErrorMessage(error)
|
|
16489
17236
|
};
|
|
16490
17237
|
}
|
|
16491
|
-
titles.rename(liveAgent.session, name);
|
|
16492
17238
|
return {
|
|
16493
17239
|
kind: "success",
|
|
16494
17240
|
text: `title set: ${name}`
|
|
16495
17241
|
};
|
|
16496
17242
|
}
|
|
17243
|
+
try {
|
|
17244
|
+
const regenerated = await titles.refresh(liveAgent.session, invocation.signal);
|
|
17245
|
+
if (regenerated === void 0) {
|
|
17246
|
+
app.notify("no conversation yet — title left as-is", "info");
|
|
17247
|
+
return { kind: "success" };
|
|
17248
|
+
}
|
|
17249
|
+
app.notify(`title regenerated: ${regenerated.title}`, "info");
|
|
17250
|
+
return { kind: "success" };
|
|
17251
|
+
} catch (error) {
|
|
17252
|
+
return {
|
|
17253
|
+
kind: "error",
|
|
17254
|
+
text: safeErrorMessage(error)
|
|
17255
|
+
};
|
|
17256
|
+
}
|
|
17257
|
+
};
|
|
17258
|
+
commands.register({
|
|
17259
|
+
name: "title",
|
|
17260
|
+
description: "Set the session title; without an argument, regenerate it from the conversation (overwrites the current title)",
|
|
17261
|
+
input: { hint: "<title>" },
|
|
17262
|
+
handler: titleHandler
|
|
17263
|
+
});
|
|
17264
|
+
commands.register({
|
|
17265
|
+
name: "rename",
|
|
17266
|
+
description: "Alias of /title: set the session title, or regenerate it without an argument (overwrites the current title)",
|
|
17267
|
+
input: { hint: "<title>" },
|
|
17268
|
+
handler: titleHandler
|
|
16497
17269
|
});
|
|
16498
17270
|
commands.register({
|
|
16499
17271
|
name: "copy",
|
|
@@ -16688,7 +17460,13 @@ function registerTuiCommands(runner) {
|
|
|
16688
17460
|
{
|
|
16689
17461
|
id: "k-enter",
|
|
16690
17462
|
label: "Enter",
|
|
16691
|
-
description: "Submit (
|
|
17463
|
+
description: "Submit (steers the running turn while busy when Enter while busy is Steer; skill commands steer too, UI commands run locally)",
|
|
17464
|
+
currentValue: ""
|
|
17465
|
+
},
|
|
17466
|
+
{
|
|
17467
|
+
id: "k-queue",
|
|
17468
|
+
label: "Ctrl+Enter",
|
|
17469
|
+
description: "Queue the draft while the agent is busy (the opposite of Enter while busy)",
|
|
16692
17470
|
currentValue: ""
|
|
16693
17471
|
},
|
|
16694
17472
|
{
|
|
@@ -16754,7 +17532,7 @@ function registerTuiCommands(runner) {
|
|
|
16754
17532
|
{
|
|
16755
17533
|
id: "k-bang",
|
|
16756
17534
|
label: "! cmd",
|
|
16757
|
-
description: "Run a shell command
|
|
17535
|
+
description: "Run a shell command and submit the command and its output to the session; !! runs locally without recording",
|
|
16758
17536
|
currentValue: ""
|
|
16759
17537
|
},
|
|
16760
17538
|
{
|
|
@@ -17323,13 +18101,15 @@ function sessionUnchanged(locked, agent, generation) {
|
|
|
17323
18101
|
* (a queue splice during the guard survives) and steers them with the
|
|
17324
18102
|
* draft. Any state change — agent switch, generation bump, queue change —
|
|
17325
18103
|
* aborts with `stale` and a retry notice; nothing is written and nothing
|
|
17326
|
-
* is lost.
|
|
18104
|
+
* is lost. With `onlyDraft` the queue is neither read nor removed: the
|
|
18105
|
+
* draft alone is steered (or followed up when the agent is idle).
|
|
17327
18106
|
*/
|
|
17328
|
-
async function steerAll(deps, text) {
|
|
18107
|
+
async function steerAll(deps, text, options = {}) {
|
|
18108
|
+
const onlyDraft = options.onlyDraft === true;
|
|
17329
18109
|
const agent = deps.currentAgent();
|
|
17330
18110
|
if (agent === void 0) return "ok";
|
|
17331
18111
|
const generation = deps.currentGeneration();
|
|
17332
|
-
const snapshot = [...agent.inbox.nextTurn, ...agent.inbox.nextStep];
|
|
18112
|
+
const snapshot = onlyDraft ? [] : [...agent.inbox.nextTurn, ...agent.inbox.nextStep];
|
|
17333
18113
|
const verdict = await deps.guard.run(savePayloadIdentity(snapshot, text));
|
|
17334
18114
|
if (verdict.kind === "blocked") {
|
|
17335
18115
|
const verbatim = deps.restoreDraft(text);
|
|
@@ -17345,6 +18125,13 @@ async function steerAll(deps, text) {
|
|
|
17345
18125
|
deps.notify(verbatim ? deps.staleNotice() : deps.mergedNotice(), "error");
|
|
17346
18126
|
return "stale";
|
|
17347
18127
|
}
|
|
18128
|
+
if (onlyDraft) {
|
|
18129
|
+
if (verdict.kind === "forced") deps.notify(deps.forcedNotice(), "error");
|
|
18130
|
+
const message = deps.createDraft(text);
|
|
18131
|
+
if (now.status === "running") now.steer(message);
|
|
18132
|
+
else now.followup(message);
|
|
18133
|
+
return "ok";
|
|
18134
|
+
}
|
|
17348
18135
|
const current = [...now.inbox.nextTurn, ...now.inbox.nextStep];
|
|
17349
18136
|
if (!(current.length === snapshot.length && current.every((message, index) => message.id === snapshot[index].id))) {
|
|
17350
18137
|
const verbatim = deps.restoreDraft(text);
|
|
@@ -17367,6 +18154,75 @@ async function steerAll(deps, text) {
|
|
|
17367
18154
|
return "ok";
|
|
17368
18155
|
}
|
|
17369
18156
|
//#endregion
|
|
18157
|
+
//#region src/shell-context.ts
|
|
18158
|
+
/**
|
|
18159
|
+
* `!` shell context submission (kimi parity): a completed local shell run
|
|
18160
|
+
* is submitted to the session as an ordinary guarded user message, so the
|
|
18161
|
+
* model sees the command AND its output on the next turn. `!!` runs stay
|
|
18162
|
+
* purely local — no session write, no model visibility (pi's
|
|
18163
|
+
* excluded-from-context semantics). Extracted from the runner so the
|
|
18164
|
+
* guard/TOCTOU races are testable headless, exactly like steer.ts:
|
|
18165
|
+
*
|
|
18166
|
+
* - The agent/generation identity is captured BEFORE the guard runs.
|
|
18167
|
+
* - AFTER the guard returns, the identity is re-validated: same agent
|
|
18168
|
+
* object, same session generation. A switch while the guard read the
|
|
18169
|
+
* file aborts (`stale`) — the output is never written to a session the
|
|
18170
|
+
* guard did not check.
|
|
18171
|
+
* - A blocked write keeps the caller's card visible (the output is not
|
|
18172
|
+
* lost) and the identical `!` re-run can force through its one-time
|
|
18173
|
+
* guard token.
|
|
18174
|
+
* @module @xmoon76/dsh-pi-tui/shell-context
|
|
18175
|
+
*/
|
|
18176
|
+
/**
|
|
18177
|
+
* Submit a completed `!` shell run's command+output to the session:
|
|
18178
|
+
* capture identity → guard → re-validate → followup. No-op without an
|
|
18179
|
+
* agent. `blocked` keeps the caller's card (the output stays visible; the
|
|
18180
|
+
* identical `!` re-run can force through its one-time token); `stale`
|
|
18181
|
+
* aborts for a retry against the new session.
|
|
18182
|
+
*/
|
|
18183
|
+
async function submitShellResult(deps, text) {
|
|
18184
|
+
const agent = deps.currentAgent();
|
|
18185
|
+
if (agent === void 0) return "ok";
|
|
18186
|
+
const generation = deps.currentGeneration();
|
|
18187
|
+
const verdict = await deps.guard.run(text);
|
|
18188
|
+
if (verdict.kind === "blocked") {
|
|
18189
|
+
deps.notify(deps.blockedNotice(verdict.reason), "error");
|
|
18190
|
+
return "blocked";
|
|
18191
|
+
}
|
|
18192
|
+
if (!sessionUnchanged({
|
|
18193
|
+
agent,
|
|
18194
|
+
generation
|
|
18195
|
+
}, deps.currentAgent(), deps.currentGeneration())) {
|
|
18196
|
+
deps.notify(deps.staleNotice(), "error");
|
|
18197
|
+
return "stale";
|
|
18198
|
+
}
|
|
18199
|
+
if (verdict.kind === "forced") deps.notify(deps.forcedNotice(), "error");
|
|
18200
|
+
agent.followup(deps.createMessage(text));
|
|
18201
|
+
deps.onSubmitted();
|
|
18202
|
+
return "ok";
|
|
18203
|
+
}
|
|
18204
|
+
/**
|
|
18205
|
+
* Classify one `!` line: 'context' submits the command+output to the
|
|
18206
|
+
* session (kimi parity), 'local' runs purely off-session (pi's `!!`
|
|
18207
|
+
* escape hatch). Returns undefined for a non-`!` line.
|
|
18208
|
+
*/
|
|
18209
|
+
function shellModeOf(text) {
|
|
18210
|
+
if (!text.startsWith("!")) return void 0;
|
|
18211
|
+
return text.startsWith("!!") ? "local" : "context";
|
|
18212
|
+
}
|
|
18213
|
+
/** Extract the command after the `!` prefix ('' when nothing follows). */
|
|
18214
|
+
function shellCommandOf(text) {
|
|
18215
|
+
return text.replace(/^!+/, "").trim();
|
|
18216
|
+
}
|
|
18217
|
+
/**
|
|
18218
|
+
* The model-facing submission text: the command echoed `$`-style (kimi
|
|
18219
|
+
* ShellExecution parity) followed by the settled card result (output +
|
|
18220
|
+
* `[exit N]` / truncation lines).
|
|
18221
|
+
*/
|
|
18222
|
+
function formatShellSubmitText(command, result) {
|
|
18223
|
+
return `$ ${command}\n${result}`;
|
|
18224
|
+
}
|
|
18225
|
+
//#endregion
|
|
17370
18226
|
//#region src/bounded-output.ts
|
|
17371
18227
|
/**
|
|
17372
18228
|
* Bounded accumulation of local-shell output: the UI card only ever holds
|
|
@@ -17685,6 +18541,9 @@ const REPAINT_FLUSH_MS = 50;
|
|
|
17685
18541
|
* dispatches through `commands.execute`, which creates the session lazily
|
|
17686
18542
|
* (the command line IS the first user input). Commands in this set must
|
|
17687
18543
|
* tolerate `liveAgent === undefined` in their handlers.
|
|
18544
|
+
*
|
|
18545
|
+
* Exported for the headless suite: the gate is exactly where a sessionless
|
|
18546
|
+
* command silently starts creating sessions again.
|
|
17688
18547
|
*/
|
|
17689
18548
|
const SESSIONLESS_COMMANDS = /* @__PURE__ */ new Set([
|
|
17690
18549
|
"exit",
|
|
@@ -17698,9 +18557,64 @@ const SESSIONLESS_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
17698
18557
|
"resume",
|
|
17699
18558
|
"search",
|
|
17700
18559
|
"new",
|
|
17701
|
-
"fork"
|
|
18560
|
+
"fork",
|
|
18561
|
+
"preset"
|
|
17702
18562
|
]);
|
|
17703
18563
|
/**
|
|
18564
|
+
* The LOCAL-execute command set: TUI-owned UI/control commands AND core
|
|
18565
|
+
* control commands the TUI does not itself register (e.g. /kill) that
|
|
18566
|
+
* must ALWAYS run locally through the commands service, never steered,
|
|
18567
|
+
* regardless of the busyEnter preference. Everything NOT in this set —
|
|
18568
|
+
* plain prompts AND non-local commands (the per-skill slash commands like
|
|
18569
|
+
* /grilling or /matrix-cli) — flows through the busy-Enter submission
|
|
18570
|
+
* policy while the agent is running: web parity, where a skill invocation
|
|
18571
|
+
* is a plain `session.prompt` whose leading `/name` line the host's
|
|
18572
|
+
* pre-step listener (dsh-tool-skill) resolves into the injected skill
|
|
18573
|
+
* body — there is no command-execution wire for skills.
|
|
18574
|
+
*/
|
|
18575
|
+
const LOCAL_COMMANDS = /* @__PURE__ */ new Set([
|
|
18576
|
+
"copy",
|
|
18577
|
+
"exit",
|
|
18578
|
+
"export",
|
|
18579
|
+
"fork",
|
|
18580
|
+
"help",
|
|
18581
|
+
"kill",
|
|
18582
|
+
"login",
|
|
18583
|
+
"logout",
|
|
18584
|
+
"model",
|
|
18585
|
+
"new",
|
|
18586
|
+
"preset",
|
|
18587
|
+
"queue",
|
|
18588
|
+
"quit",
|
|
18589
|
+
"reload",
|
|
18590
|
+
"rename",
|
|
18591
|
+
"resume",
|
|
18592
|
+
"search",
|
|
18593
|
+
"sessions",
|
|
18594
|
+
"settings",
|
|
18595
|
+
"skill",
|
|
18596
|
+
"status",
|
|
18597
|
+
"subagents",
|
|
18598
|
+
"tasks",
|
|
18599
|
+
"title",
|
|
18600
|
+
"yolo"
|
|
18601
|
+
]);
|
|
18602
|
+
/**
|
|
18603
|
+
* Whether one submission steers under the busy-Enter preference: NOT a
|
|
18604
|
+
* force-queued chord, NOT a TUI-owned local command, and the agent is
|
|
18605
|
+
* running with the preference set to 'steer'. Pure so the dispatch gate
|
|
18606
|
+
* (inside the runner closure) is testable headless.
|
|
18607
|
+
* @param parsed - the parsed slash command, undefined for a plain prompt.
|
|
18608
|
+
* @param running - whether the live agent reports running.
|
|
18609
|
+
* @param busyEnter - the persisted preference value (''/undefined = queue).
|
|
18610
|
+
* @param forceQueue - the Ctrl+Enter chord: always queue, never steer.
|
|
18611
|
+
*/
|
|
18612
|
+
function shouldSteerOnEnter(parsed, running, busyEnter, forceQueue) {
|
|
18613
|
+
if (forceQueue) return false;
|
|
18614
|
+
if (parsed !== void 0 && LOCAL_COMMANDS.has(parsed.name)) return false;
|
|
18615
|
+
return running && busyEnter === "steer";
|
|
18616
|
+
}
|
|
18617
|
+
/**
|
|
17704
18618
|
* Read an explicit child-session reference from a future/extended job record.
|
|
17705
18619
|
* Current dsh JobSnapshot records do not expose one, so this normally returns
|
|
17706
18620
|
* undefined. Crucially, label, registration order and timestamps are never
|
|
@@ -17998,6 +18912,7 @@ function apply(ctx, config) {
|
|
|
17998
18912
|
const compose = (presetId) => composeAgent(ctx, selected, presetId);
|
|
17999
18913
|
const withPresetMeta = (composition) => composition.agentPreset === void 0 ? {} : { agentPreset: composition.agentPreset };
|
|
18000
18914
|
const launchPreset = startup.presetId ?? (process.env.DSH_PI_TUI_PRESET?.trim() || void 0);
|
|
18915
|
+
let pendingPreset;
|
|
18001
18916
|
diag.info("boot", {
|
|
18002
18917
|
pid: process.pid,
|
|
18003
18918
|
dsh: dshVersion() ?? "unknown",
|
|
@@ -18022,7 +18937,7 @@ function apply(ctx, config) {
|
|
|
18022
18937
|
/** Resolve the launch composition, falling back to the default on an unknown id. */
|
|
18023
18938
|
const launchComposition = async () => {
|
|
18024
18939
|
try {
|
|
18025
|
-
return { composition: await compose(launchPreset) };
|
|
18940
|
+
return { composition: await compose(pendingPreset ?? launchPreset) };
|
|
18026
18941
|
} catch (error) {
|
|
18027
18942
|
const message = safeErrorMessage(error);
|
|
18028
18943
|
ctx.logger.warn(`tui-runner: launch preset unavailable: ${message}`);
|
|
@@ -18341,22 +19256,19 @@ function apply(ctx, config) {
|
|
|
18341
19256
|
cleanup();
|
|
18342
19257
|
};
|
|
18343
19258
|
});
|
|
18344
|
-
/**
|
|
19259
|
+
/**
|
|
19260
|
+
* Run a `!` shell command. `!` (context mode) runs the command and then
|
|
19261
|
+
* submits the completed command+output to the session as an ordinary
|
|
19262
|
+
* guarded user message (kimi parity: the model sees both on the next
|
|
19263
|
+
* turn; the result wakes a turn but is never steered into a running
|
|
19264
|
+
* one); `!!` (local mode) runs purely off-session — the card is the
|
|
19265
|
+
* only record (pi's excluded-from-context escape hatch).
|
|
19266
|
+
*/
|
|
18345
19267
|
const runLocalShell = (text) => {
|
|
18346
|
-
const includeInContext = text
|
|
18347
|
-
const command = text
|
|
19268
|
+
const includeInContext = shellModeOf(text) === "context";
|
|
19269
|
+
const command = shellCommandOf(text);
|
|
18348
19270
|
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
|
-
}
|
|
19271
|
+
const generationAtRun = sessionGeneration;
|
|
18360
19272
|
localShellController?.abort();
|
|
18361
19273
|
localShellController = new AbortController();
|
|
18362
19274
|
const localSignal = localShellController.signal;
|
|
@@ -18372,6 +19284,51 @@ function apply(ctx, config) {
|
|
|
18372
19284
|
const releaseController = () => {
|
|
18373
19285
|
if (localShellController?.signal === localSignal) localShellController = void 0;
|
|
18374
19286
|
};
|
|
19287
|
+
/**
|
|
19288
|
+
* Submit the completed run to the session (context mode only): guard
|
|
19289
|
+
* → re-validate → followup. Blocked keeps the card (the output stays
|
|
19290
|
+
* visible; the identical `!` re-run forces through its one-time guard
|
|
19291
|
+
* token); accepted clears the settled card — the transcript's user
|
|
19292
|
+
* row becomes the record. An owned workflow: the outcome drives the
|
|
19293
|
+
* notify and the card — runOwned (AGENTS.md), never a bare void.
|
|
19294
|
+
*/
|
|
19295
|
+
const submitResult = (result) => {
|
|
19296
|
+
if (sessionGeneration !== generationAtRun) {
|
|
19297
|
+
app.notify("the session changed while the command ran — the output was not submitted", "error");
|
|
19298
|
+
return;
|
|
19299
|
+
}
|
|
19300
|
+
const submitted = formatShellSubmitText(command, result);
|
|
19301
|
+
runOwned("shell submit", () => submitShellResult({
|
|
19302
|
+
currentAgent: () => liveAgent,
|
|
19303
|
+
currentGeneration: () => sessionGeneration,
|
|
19304
|
+
guard: { run: async (identity) => {
|
|
19305
|
+
const verdict = await guardSend("submit", identity);
|
|
19306
|
+
if (verdict.kind === "blocked") return {
|
|
19307
|
+
kind: "blocked",
|
|
19308
|
+
reason: verdict.reason
|
|
19309
|
+
};
|
|
19310
|
+
return { kind: verdict.kind === "forced" ? "forced" : "ok" };
|
|
19311
|
+
} },
|
|
19312
|
+
notify: (message, kind) => app.notify(message, kind),
|
|
19313
|
+
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)",
|
|
19314
|
+
forcedNotice: () => GUARD_FORCED_NOTIFY,
|
|
19315
|
+
staleNotice: () => "the session changed while the submission was being checked — the output was not submitted",
|
|
19316
|
+
createMessage: (text) => createUserMessage({
|
|
19317
|
+
content: [{
|
|
19318
|
+
type: "text",
|
|
19319
|
+
text
|
|
19320
|
+
}],
|
|
19321
|
+
source: { kind: "user" }
|
|
19322
|
+
}),
|
|
19323
|
+
onSubmitted: () => app.clearSettledLocalMessages()
|
|
19324
|
+
}, submitted), {
|
|
19325
|
+
diag,
|
|
19326
|
+
sessionId: () => liveAgent?.session.id,
|
|
19327
|
+
onError: (error) => {
|
|
19328
|
+
app.notify(`shell submit failed: ${safeErrorMessage(error)}`, "error");
|
|
19329
|
+
}
|
|
19330
|
+
});
|
|
19331
|
+
};
|
|
18375
19332
|
let settled = false;
|
|
18376
19333
|
const settle = (result, status) => {
|
|
18377
19334
|
if (settled) return;
|
|
@@ -18384,6 +19341,7 @@ function apply(ctx, config) {
|
|
|
18384
19341
|
result,
|
|
18385
19342
|
status
|
|
18386
19343
|
});
|
|
19344
|
+
if (includeInContext && !localSignal.aborted) submitResult(result);
|
|
18387
19345
|
};
|
|
18388
19346
|
const shell = ctx.get("shell");
|
|
18389
19347
|
if (shell !== void 0) {
|
|
@@ -18537,7 +19495,10 @@ function apply(ctx, config) {
|
|
|
18537
19495
|
folder: childFolder
|
|
18538
19496
|
};
|
|
18539
19497
|
repaint(app, childFolder);
|
|
18540
|
-
app.
|
|
19498
|
+
app.setViewerMode({
|
|
19499
|
+
id: childId,
|
|
19500
|
+
label: label ?? childId
|
|
19501
|
+
});
|
|
18541
19502
|
};
|
|
18542
19503
|
/** Leave the subagent viewer (single Esc). Returns whether it exited. */
|
|
18543
19504
|
const exitView = () => {
|
|
@@ -18545,6 +19506,7 @@ function apply(ctx, config) {
|
|
|
18545
19506
|
viewing = void 0;
|
|
18546
19507
|
app.clearLocalMessages();
|
|
18547
19508
|
app.clearNotify();
|
|
19509
|
+
app.setViewerMode(void 0);
|
|
18548
19510
|
repaint(app, folder);
|
|
18549
19511
|
refreshStatus();
|
|
18550
19512
|
return true;
|
|
@@ -18671,44 +19633,119 @@ function apply(ctx, config) {
|
|
|
18671
19633
|
}
|
|
18672
19634
|
});
|
|
18673
19635
|
};
|
|
19636
|
+
/**
|
|
19637
|
+
* Steer into the running turn with guard re-validation. Shared by
|
|
19638
|
+
* Ctrl+S (the whole queue plus a non-empty draft) and the busy-Enter
|
|
19639
|
+
* preference — Enter while the agent is running with busyEnter=steer
|
|
19640
|
+
* steers the DRAFT ONLY (web busyEnter parity): explicitly queued
|
|
19641
|
+
* messages stay queued until Ctrl+S or the /queue actions, because
|
|
19642
|
+
* already-steered input cannot be pulled back.
|
|
19643
|
+
* @param text - the submitted draft ('' allowed for Ctrl+S).
|
|
19644
|
+
* @param onlyDraft - busy-Enter mode: never read or remove the queue.
|
|
19645
|
+
*/
|
|
19646
|
+
const steerNow = (text, onlyDraft = false) => {
|
|
19647
|
+
if (viewing !== void 0) {
|
|
19648
|
+
if (text.trim() !== "") app.setEditorText(mergeDraft(app.getDraft(), text));
|
|
19649
|
+
app.notify("viewing a subagent — Esc returns before steering", "info");
|
|
19650
|
+
return;
|
|
19651
|
+
}
|
|
19652
|
+
app.clearSettledLocalMessages();
|
|
19653
|
+
if (onlyDraft) {
|
|
19654
|
+
if (text.trim() === "") return;
|
|
19655
|
+
} else if (liveAgent !== void 0) {
|
|
19656
|
+
if ([...liveAgent.inbox.nextTurn, ...liveAgent.inbox.nextStep].length === 0 && text.trim() === "") return;
|
|
19657
|
+
} else if (text.trim() === "") return;
|
|
19658
|
+
runOwned("steer", () => ensureSession().then(async () => {
|
|
19659
|
+
if (liveAgent === void 0) return;
|
|
19660
|
+
await steerAll({
|
|
19661
|
+
currentAgent: () => liveAgent,
|
|
19662
|
+
currentGeneration: () => sessionGeneration,
|
|
19663
|
+
guard: { run: async (identity) => {
|
|
19664
|
+
const verdict = await guardSend("save", identity);
|
|
19665
|
+
if (verdict.kind === "blocked") return {
|
|
19666
|
+
kind: "blocked",
|
|
19667
|
+
reason: verdict.reason
|
|
19668
|
+
};
|
|
19669
|
+
return { kind: verdict.kind === "forced" ? "forced" : "ok" };
|
|
19670
|
+
} },
|
|
19671
|
+
notify: (message, kind) => app.notify(message, kind),
|
|
19672
|
+
restoreDraft: (draft) => {
|
|
19673
|
+
const merged = mergeDraft(app.getDraft(), draft);
|
|
19674
|
+
app.setEditorText(merged);
|
|
19675
|
+
return merged === draft;
|
|
19676
|
+
},
|
|
19677
|
+
createDraft: (draft) => createUserMessage({
|
|
19678
|
+
content: [{
|
|
19679
|
+
type: "text",
|
|
19680
|
+
text: draft
|
|
19681
|
+
}],
|
|
19682
|
+
source: { kind: "user" }
|
|
19683
|
+
}),
|
|
19684
|
+
blockedNotice: (reason) => reason === "removed" ? GUARD_REMOVED_NOTIFY("save") : reason === "tail-mismatch" ? GUARD_TAIL_MISMATCH_NOTIFY("save") : GUARD_BLOCKED_NOTIFY("save"),
|
|
19685
|
+
forcedNotice: () => GUARD_FORCED_NOTIFY,
|
|
19686
|
+
staleNotice: () => "the queue or session changed while sending — try again",
|
|
19687
|
+
mergedNotice: () => "the draft changed while sending — review it before submitting again (the earlier text was preserved below)"
|
|
19688
|
+
}, text, onlyDraft ? { onlyDraft: true } : void 0);
|
|
19689
|
+
}), {
|
|
19690
|
+
diag,
|
|
19691
|
+
sessionId: () => liveAgent?.session.id,
|
|
19692
|
+
onError: failSubmission(text)
|
|
19693
|
+
});
|
|
19694
|
+
};
|
|
19695
|
+
/**
|
|
19696
|
+
* Dispatch one user submission end to end: the viewer guard, the input-
|
|
19697
|
+
* history persistence, `!` local shells, sessionless commands, the
|
|
19698
|
+
* busy-Enter preference, and the session dispatch. The Ctrl+Enter
|
|
19699
|
+
* opposite chord forces the QUEUE mode (forceQueue), web busyEnter
|
|
19700
|
+
* parity — the accelerated chord uses the other behavior.
|
|
19701
|
+
* @param text - the submitted draft.
|
|
19702
|
+
* @param forceQueue - the chord: never steer, queue instead.
|
|
19703
|
+
*/
|
|
19704
|
+
const dispatchUserInput = (text, forceQueue = false) => {
|
|
19705
|
+
if (viewing !== void 0) {
|
|
19706
|
+
app.setEditorText(mergeDraft(app.getDraft(), text));
|
|
19707
|
+
app.notify("viewing a subagent — Esc returns before submitting", "info");
|
|
19708
|
+
return;
|
|
19709
|
+
}
|
|
19710
|
+
app.clearSettledLocalMessages();
|
|
19711
|
+
const history = app.getInputHistory();
|
|
19712
|
+
if (history.length > 0) {
|
|
19713
|
+
const settings = tuiSettings;
|
|
19714
|
+
if (settings !== void 0) runDetached("settings history write", () => settings.replace({
|
|
19715
|
+
...settings.get(),
|
|
19716
|
+
history: {
|
|
19717
|
+
...settings.get().history,
|
|
19718
|
+
[sessionCwd()]: history
|
|
19719
|
+
}
|
|
19720
|
+
}), {
|
|
19721
|
+
diag,
|
|
19722
|
+
notify: (message) => app.notify(message, "error"),
|
|
19723
|
+
recoverable: () => true
|
|
19724
|
+
});
|
|
19725
|
+
}
|
|
19726
|
+
if (text.startsWith("!")) {
|
|
19727
|
+
if (text.startsWith("!!")) runLocalShell(text);
|
|
19728
|
+
else if (shellCommandOf(text) !== "") runOwned("contextual shell", () => ensureSession().then(() => runLocalShell(text)), {
|
|
19729
|
+
diag,
|
|
19730
|
+
sessionId: () => liveAgent?.session.id,
|
|
19731
|
+
onError: failSubmission(text)
|
|
19732
|
+
});
|
|
19733
|
+
return;
|
|
19734
|
+
}
|
|
19735
|
+
const parsed = parseCommand(text);
|
|
19736
|
+
if (parsed !== void 0 && liveAgent === void 0 && SESSIONLESS_COMMANDS.has(parsed.name)) {
|
|
19737
|
+
runLocalCommand(parsed, text);
|
|
19738
|
+
return;
|
|
19739
|
+
}
|
|
19740
|
+
if (shouldSteerOnEnter(parsed, liveAgent?.status === "running", tuiSettings?.get().busyEnter, forceQueue)) {
|
|
19741
|
+
steerNow(text, true);
|
|
19742
|
+
return;
|
|
19743
|
+
}
|
|
19744
|
+
dispatchViaSession(text);
|
|
19745
|
+
};
|
|
18674
19746
|
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
|
-
},
|
|
19747
|
+
onSubmit: (text) => dispatchUserInput(text),
|
|
19748
|
+
onQueueSubmit: (text) => dispatchUserInput(text, true),
|
|
18712
19749
|
runOwned: (label, task, options) => {
|
|
18713
19750
|
runOwned(label, task, {
|
|
18714
19751
|
...options,
|
|
@@ -18724,52 +19761,7 @@ function apply(ctx, config) {
|
|
|
18724
19761
|
localShellController?.abort();
|
|
18725
19762
|
liveAgent?.cancel({ kind: "user" });
|
|
18726
19763
|
},
|
|
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
|
-
},
|
|
19764
|
+
onSteer: (text) => steerNow(text),
|
|
18773
19765
|
openExternalEditor: async (draft) => {
|
|
18774
19766
|
const [editor, ...editorArgs] = parseShellWords(process.env.VISUAL ?? process.env.EDITOR ?? "vi");
|
|
18775
19767
|
if (editor === void 0) throw new Error("empty editor command");
|
|
@@ -18858,31 +19850,50 @@ function apply(ctx, config) {
|
|
|
18858
19850
|
refreshQueue();
|
|
18859
19851
|
},
|
|
18860
19852
|
onOpenTasks: () => {
|
|
18861
|
-
if (
|
|
18862
|
-
let
|
|
18863
|
-
try {
|
|
18864
|
-
|
|
18865
|
-
} catch {
|
|
18866
|
-
return;
|
|
18867
|
-
}
|
|
18868
|
-
if (snapshots.length === 0) return;
|
|
19853
|
+
if (liveAgent === void 0) return;
|
|
19854
|
+
let jobSnapshots = [];
|
|
19855
|
+
if (jobs !== void 0) try {
|
|
19856
|
+
jobSnapshots = jobs.list(liveAgent);
|
|
19857
|
+
} catch {}
|
|
18869
19858
|
const now = Date.now();
|
|
18870
|
-
|
|
18871
|
-
const
|
|
18872
|
-
const
|
|
18873
|
-
if (
|
|
18874
|
-
if (
|
|
18875
|
-
|
|
18876
|
-
|
|
18877
|
-
|
|
18878
|
-
|
|
18879
|
-
|
|
18880
|
-
|
|
18881
|
-
|
|
18882
|
-
|
|
19859
|
+
let rows = buildTaskRows(jobSnapshots, []);
|
|
19860
|
+
const selectRow = (value) => {
|
|
19861
|
+
const row = rows.find((candidate) => candidate.value === value);
|
|
19862
|
+
if (row === void 0) return;
|
|
19863
|
+
if (row.kind === "subagent") {
|
|
19864
|
+
runOwned("subagent view from tasks", () => enterView(row.childId, row.label), {
|
|
19865
|
+
diag,
|
|
19866
|
+
sessionId: () => liveAgent?.session.id,
|
|
19867
|
+
onError: (error) => app.notify(`could not open the subagent view: ${safeErrorMessage(error)}`, "error")
|
|
19868
|
+
});
|
|
19869
|
+
return;
|
|
19870
|
+
}
|
|
19871
|
+
openJobView(row.jobId);
|
|
19872
|
+
};
|
|
19873
|
+
const pickerItems = (target) => target.map((row) => ({
|
|
19874
|
+
value: row.value,
|
|
19875
|
+
label: taskRowLabel(row),
|
|
19876
|
+
description: describeTaskRow(row, now),
|
|
19877
|
+
group: rowGroup(row)
|
|
19878
|
+
}));
|
|
19879
|
+
const handle = app.openPicker(pickerItems(rows), selectRow, () => {}, {
|
|
19880
|
+
header: "tasks · subagents",
|
|
18883
19881
|
enableSearch: true,
|
|
18884
19882
|
showHint: true
|
|
18885
19883
|
});
|
|
19884
|
+
if (subagents !== void 0) {
|
|
19885
|
+
const sessionId = liveAgent.session.id;
|
|
19886
|
+
const generation = sessionGeneration;
|
|
19887
|
+
runOwned("task browser children", () => subagents.listChildren(sessionId), {
|
|
19888
|
+
diag,
|
|
19889
|
+
sessionId: () => liveAgent?.session.id,
|
|
19890
|
+
onResult: (entries) => {
|
|
19891
|
+
if (sessionGeneration !== generation || liveAgent?.session.id !== sessionId) return;
|
|
19892
|
+
rows = buildTaskRows(jobSnapshots, entries);
|
|
19893
|
+
handle.setItems(pickerItems(rows));
|
|
19894
|
+
}
|
|
19895
|
+
});
|
|
19896
|
+
}
|
|
18886
19897
|
}
|
|
18887
19898
|
}, {
|
|
18888
19899
|
present,
|
|
@@ -18892,28 +19903,35 @@ function apply(ctx, config) {
|
|
|
18892
19903
|
theme: z.string(),
|
|
18893
19904
|
footer: z.string(),
|
|
18894
19905
|
fullscreen: z.string(),
|
|
19906
|
+
busyEnter: z.string(),
|
|
18895
19907
|
history: z.dict(z.array(z.string()))
|
|
18896
19908
|
}), { base: {
|
|
18897
19909
|
theme: "auto",
|
|
18898
19910
|
footer: "full",
|
|
18899
19911
|
fullscreen: "on",
|
|
19912
|
+
busyEnter: "queue",
|
|
18900
19913
|
history: {}
|
|
18901
19914
|
} });
|
|
19915
|
+
if (tuiSettings?.get().fullscreen === "on") app.setFullscreen(true);
|
|
18902
19916
|
const storedTheme = tuiSettings?.get().theme;
|
|
18903
19917
|
if (storedTheme === "auto") {
|
|
18904
|
-
runDetached("theme autodetect", () => app.autoDetectTheme(), { diag });
|
|
19918
|
+
runDetached("theme autodetect", () => app.autoDetectTheme({ shouldApply: () => tuiSettings?.get().theme === "auto" }), { diag });
|
|
18905
19919
|
app.onTerminalThemeChange((theme) => {
|
|
18906
19920
|
if (tuiSettings?.get().theme === "auto") app.applyTheme(theme);
|
|
18907
19921
|
});
|
|
18908
|
-
|
|
18909
|
-
else if (storedTheme
|
|
19922
|
+
app.trackTerminalTheme(true);
|
|
19923
|
+
} else if (storedTheme === "dark" || storedTheme === "light") {
|
|
19924
|
+
app.applyTheme(storedTheme);
|
|
19925
|
+
app.trackTerminalTheme(false);
|
|
19926
|
+
} else if (storedTheme?.startsWith("custom:")) {
|
|
18910
19927
|
const palette = loadCustomTheme(storedTheme.slice(7));
|
|
18911
19928
|
if (palette !== void 0) app.applyPalette(palette);
|
|
19929
|
+
app.trackTerminalTheme(false);
|
|
18912
19930
|
}
|
|
18913
19931
|
if (tuiSettings?.get().footer === "compact") app.setFooterPreset("compact");
|
|
18914
|
-
if (tuiSettings?.get().fullscreen === "on") app.setFullscreen(true);
|
|
18915
19932
|
refreshStatus();
|
|
18916
19933
|
let refreshTasks = () => {};
|
|
19934
|
+
let refreshAgents = () => {};
|
|
18917
19935
|
const jobs = ctx.get("jobs");
|
|
18918
19936
|
if (jobs !== void 0) {
|
|
18919
19937
|
refreshTasks = () => {
|
|
@@ -18928,9 +19946,36 @@ function apply(ctx, config) {
|
|
|
18928
19946
|
} catch {}
|
|
18929
19947
|
app.setTasks(tasks);
|
|
18930
19948
|
};
|
|
18931
|
-
jobs.onJobsChanged(() =>
|
|
19949
|
+
jobs.onJobsChanged(() => {
|
|
19950
|
+
refreshTasks();
|
|
19951
|
+
refreshAgents();
|
|
19952
|
+
});
|
|
18932
19953
|
refreshTasks();
|
|
18933
19954
|
}
|
|
19955
|
+
const subagents = ctx.get("subagents");
|
|
19956
|
+
if (subagents !== void 0) {
|
|
19957
|
+
refreshAgents = () => {
|
|
19958
|
+
if (liveAgent === void 0) {
|
|
19959
|
+
app.setAgents([]);
|
|
19960
|
+
return;
|
|
19961
|
+
}
|
|
19962
|
+
const sessionId = liveAgent.session.id;
|
|
19963
|
+
const generation = sessionGeneration;
|
|
19964
|
+
runOwned("task browser agents refresh", () => subagents.listChildren(sessionId), {
|
|
19965
|
+
diag,
|
|
19966
|
+
sessionId: () => liveAgent?.session.id,
|
|
19967
|
+
onResult: (entries) => {
|
|
19968
|
+
if (sessionGeneration !== generation || liveAgent?.session.id !== sessionId) return;
|
|
19969
|
+
app.setAgents(entries.filter((entry) => entry.kind === "child" && entry.activity === "running").map((entry) => ({
|
|
19970
|
+
id: entry.id,
|
|
19971
|
+
label: entry.label ?? entry.id,
|
|
19972
|
+
activity: entry.activity
|
|
19973
|
+
})));
|
|
19974
|
+
}
|
|
19975
|
+
});
|
|
19976
|
+
};
|
|
19977
|
+
refreshAgents();
|
|
19978
|
+
}
|
|
18934
19979
|
/**
|
|
18935
19980
|
* Open one job from the task browser: a bash job shows a STATUS viewer
|
|
18936
19981
|
* (never the output — the job's single read cursor belongs to the agent's
|
|
@@ -19032,6 +20077,8 @@ function apply(ctx, config) {
|
|
|
19032
20077
|
repaint(app, folder);
|
|
19033
20078
|
refreshStatus();
|
|
19034
20079
|
refreshQueue();
|
|
20080
|
+
refreshTasks();
|
|
20081
|
+
refreshAgents();
|
|
19035
20082
|
app.resetInputHistory(tuiSettings?.get().history[sessionCwd()] ?? []);
|
|
19036
20083
|
setTerminalTitle(`dsh-pi-tui · ${shortCwd(sessionCwd())} · ${agent.session.id}`);
|
|
19037
20084
|
updateWelcomeCard();
|
|
@@ -19127,6 +20174,12 @@ function apply(ctx, config) {
|
|
|
19127
20174
|
return sessionGeneration;
|
|
19128
20175
|
},
|
|
19129
20176
|
compose,
|
|
20177
|
+
get pendingPreset() {
|
|
20178
|
+
return pendingPreset;
|
|
20179
|
+
},
|
|
20180
|
+
set pendingPreset(id) {
|
|
20181
|
+
pendingPreset = id;
|
|
20182
|
+
},
|
|
19130
20183
|
switchSession,
|
|
19131
20184
|
swapTo: (next) => swapTo(next),
|
|
19132
20185
|
currentPreset,
|
|
@@ -19182,8 +20235,10 @@ function apply(ctx, config) {
|
|
|
19182
20235
|
return;
|
|
19183
20236
|
}
|
|
19184
20237
|
if (session.id !== liveAgent.session.id) return;
|
|
19185
|
-
if (event.type === "tool/call")
|
|
19186
|
-
|
|
20238
|
+
if (event.type === "tool/call") {
|
|
20239
|
+
callArgs.set(event.data.callId, typeof event.data.arguments === "string" ? event.data.arguments : JSON.stringify(event.data.arguments));
|
|
20240
|
+
if (typeof event.data.name === "string" && event.data.name.startsWith("subagent")) refreshAgents();
|
|
20241
|
+
} else if (event.type === "tool/result") callArgs.delete(event.data.message.content[0]?.toolCallId ?? "");
|
|
19187
20242
|
folder.apply([event]);
|
|
19188
20243
|
statsFolder.apply([event]);
|
|
19189
20244
|
if (event.type === "goal/change") goalText = foldGoal([event]);
|
|
@@ -19214,6 +20269,8 @@ function apply(ctx, config) {
|
|
|
19214
20269
|
});
|
|
19215
20270
|
} else if (event.type === "step/start") refreshStatus();
|
|
19216
20271
|
});
|
|
20272
|
+
ctx.on("subagent/start", () => refreshAgents());
|
|
20273
|
+
ctx.on("subagent/end", () => refreshAgents());
|
|
19217
20274
|
if (liveAgent !== void 0) {
|
|
19218
20275
|
app.setPlanMode(foldPlanMode(liveAgent.session.events));
|
|
19219
20276
|
app.setWorking(workingFromLog(liveAgent.session.events));
|
|
@@ -19273,4 +20330,4 @@ function apply(ctx, config) {
|
|
|
19273
20330
|
});
|
|
19274
20331
|
}
|
|
19275
20332
|
//#endregion
|
|
19276
|
-
export { Config, apply, composeAgent, dangerCommand, forkSeed, inject, name, recomposeBlank, recordedPreset, resumeCommand, runningProfile, subagentJobTranscriptId, subagentJobViewHint };
|
|
20333
|
+
export { Config, LOCAL_COMMANDS, SESSIONLESS_COMMANDS, apply, composeAgent, dangerCommand, forkSeed, inject, name, recomposeBlank, recordedPreset, resumeCommand, runningProfile, shouldSteerOnEnter, subagentJobTranscriptId, subagentJobViewHint };
|