@xmoon76/dsh-pi-tui 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -19
- package/README.zh-CN.md +21 -15
- package/dist/builtins.mjs +1 -1
- package/dist/extensions.mjs +1 -1
- package/dist/index.d.mts +16 -1
- package/dist/index.mjs +2 -2
- package/dist/{src-C_RdKmcj.mjs → src-BsSlpFFF.mjs} +916 -380
- package/package.json +1 -1
|
@@ -85,8 +85,121 @@ const TUI_TOOL_TITLES = {
|
|
|
85
85
|
"workflow-member": "Workflow Agent",
|
|
86
86
|
error: "Error",
|
|
87
87
|
interrupted: "Interrupted",
|
|
88
|
-
ask_user_question: "Question"
|
|
88
|
+
ask_user_question: "Question",
|
|
89
|
+
get_goal: "Read Goal",
|
|
90
|
+
create_goal: "Create Goal",
|
|
91
|
+
update_goal: "Update Goal"
|
|
89
92
|
};
|
|
93
|
+
/** The goal tool family (all three share the `{"goal":…}` result shape). */
|
|
94
|
+
const GOAL_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
95
|
+
"get_goal",
|
|
96
|
+
"create_goal",
|
|
97
|
+
"update_goal"
|
|
98
|
+
]);
|
|
99
|
+
/**
|
|
100
|
+
* Tools whose folded result preview must never leak the raw result JSON
|
|
101
|
+
* (web parity: the web's folded row shows only the args summary, never the
|
|
102
|
+
* result). The TUI keeps its result-preview row but shows a parsed summary
|
|
103
|
+
* instead — and nothing at all when the result cannot be parsed.
|
|
104
|
+
*
|
|
105
|
+
* `ralph`'s render already leads with a friendly line ("Ralph worker
|
|
106
|
+
* reported completion after N rounds.") before the JSON report, so its
|
|
107
|
+
* summary IS that first line. The agent-team family is deliberately NOT in
|
|
108
|
+
* the set: it is experimental (not in the production bundle) and two of its
|
|
109
|
+
* tools (`send_message`, `interrupt_agent`) share names with subagent tools
|
|
110
|
+
* whose render text is friendly — a name-keyed set cannot tell them apart.
|
|
111
|
+
*/
|
|
112
|
+
const FOLDED_JSON_RESULT_TOOLS = /* @__PURE__ */ new Set([
|
|
113
|
+
"ralph",
|
|
114
|
+
"schedule_create",
|
|
115
|
+
"schedule_list",
|
|
116
|
+
"schedule_delete",
|
|
117
|
+
"cordis_inspect_list",
|
|
118
|
+
"cordis_inspect_query",
|
|
119
|
+
"cordis_inspect_self"
|
|
120
|
+
]);
|
|
121
|
+
/** Parse a result text as JSON, tolerating a friendly prefix line before
|
|
122
|
+
* the JSON document (e.g. ralph's "…\nFinal report:\n{…}"). */
|
|
123
|
+
function parseJsonValue(text) {
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(text);
|
|
126
|
+
} catch {}
|
|
127
|
+
const brace = text.indexOf("{");
|
|
128
|
+
const bracket = text.indexOf("[");
|
|
129
|
+
const start = bracket !== -1 && (brace === -1 || bracket < brace) ? bracket : brace;
|
|
130
|
+
if (start > 0) try {
|
|
131
|
+
return JSON.parse(text.slice(start));
|
|
132
|
+
} catch {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** A non-empty string field, or undefined. */
|
|
137
|
+
function stringField(value) {
|
|
138
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The folded preview summary for one of {@link FOLDED_JSON_RESULT_TOOLS}:
|
|
142
|
+
* a short human phrase derived from the parsed result JSON. Returns
|
|
143
|
+
* undefined when nothing useful can be derived — the caller then shows NO
|
|
144
|
+
* folded result preview (web parity), never the raw JSON.
|
|
145
|
+
* @param name - the tool name.
|
|
146
|
+
* @param text - the settled result text.
|
|
147
|
+
*/
|
|
148
|
+
function foldedResultSummaryFor(name, text) {
|
|
149
|
+
if (name === "ralph") {
|
|
150
|
+
const line = firstLine(text);
|
|
151
|
+
if (line !== "" && !line.startsWith("{")) return line;
|
|
152
|
+
const parsed = parseJsonValue(text);
|
|
153
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
154
|
+
const record = parsed;
|
|
155
|
+
const status = stringField(record.status);
|
|
156
|
+
const rounds = typeof record.roundsStarted === "number" ? String(record.roundsStarted) : void 0;
|
|
157
|
+
const parts = [status, rounds === void 0 ? void 0 : `${rounds} rounds`].filter((part) => part !== void 0);
|
|
158
|
+
return parts.length === 0 ? void 0 : parts.join(" · ");
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const parsed = parseJsonValue(text);
|
|
163
|
+
if (name === "schedule_list") {
|
|
164
|
+
if (Array.isArray(parsed)) return parsed.length === 0 ? "no scheduled jobs" : `${parsed.length} scheduled`;
|
|
165
|
+
if (typeof parsed === "object" && parsed !== null) return stringField(parsed.code);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (name === "schedule_create") {
|
|
169
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
170
|
+
const record = parsed;
|
|
171
|
+
const parts = [stringField(record.kind), stringField(record.state)].filter((part) => part !== void 0);
|
|
172
|
+
return parts.length === 0 ? void 0 : parts.join(" · ");
|
|
173
|
+
}
|
|
174
|
+
if (name === "schedule_delete") {
|
|
175
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
176
|
+
const record = parsed;
|
|
177
|
+
if (record.deleted === true) return "deleted";
|
|
178
|
+
if (record.deleted === false) return "not found";
|
|
179
|
+
return stringField(record.code);
|
|
180
|
+
}
|
|
181
|
+
if (name === "cordis_inspect_list") {
|
|
182
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
183
|
+
const providers = parsed.providers;
|
|
184
|
+
if (Array.isArray(providers)) return providers.length === 0 ? "no providers" : `${providers.length} providers`;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (name === "cordis_inspect_query") {
|
|
188
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
189
|
+
const record = parsed;
|
|
190
|
+
const parts = [
|
|
191
|
+
stringField(record.platform),
|
|
192
|
+
stringField(record.provider),
|
|
193
|
+
stringField(record.method)
|
|
194
|
+
].filter((part) => part !== void 0);
|
|
195
|
+
return parts.length === 0 ? void 0 : parts.join(" · ");
|
|
196
|
+
}
|
|
197
|
+
if (name === "cordis_inspect_self") {
|
|
198
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
199
|
+
const mode = stringField(parsed.mode);
|
|
200
|
+
return mode === void 0 ? void 0 : `mode ${mode}`;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
90
203
|
/** Summary key preference per variant (args-derived). */
|
|
91
204
|
const SUMMARY_KEYS = {
|
|
92
205
|
bash: ["description", "command"],
|
|
@@ -172,6 +285,15 @@ function summarizeToolArgs(name, argsRaw) {
|
|
|
172
285
|
if (typeof question !== "string" || question === "") return void 0;
|
|
173
286
|
return firstLine(question);
|
|
174
287
|
}
|
|
288
|
+
if (name === "create_goal") {
|
|
289
|
+
const objective = args.objective;
|
|
290
|
+
return typeof objective === "string" && objective !== "" ? firstLine(objective) : void 0;
|
|
291
|
+
}
|
|
292
|
+
if (name === "update_goal") {
|
|
293
|
+
const action = args.action;
|
|
294
|
+
return typeof action === "string" && action !== "" ? action : void 0;
|
|
295
|
+
}
|
|
296
|
+
if (name === "get_goal") return "";
|
|
175
297
|
if (name !== "todo_write") return void 0;
|
|
176
298
|
const todos = args.todos;
|
|
177
299
|
if (!Array.isArray(todos)) return void 0;
|
|
@@ -421,6 +543,36 @@ function resultTextLines(blocks, error) {
|
|
|
421
543
|
return lines;
|
|
422
544
|
}
|
|
423
545
|
/**
|
|
546
|
+
* Parse and normalize the tool's `{"answers":[…]}` render text. A
|
|
547
|
+
* malformed entry (non-object) invalidates the WHOLE text — the Web's
|
|
548
|
+
* `answeredSummary` checks `answers.every(isAnswer)` the same way, and a
|
|
549
|
+
* half-valid set would make the count and the rendered rows disagree.
|
|
550
|
+
* Non-string `selected` members are dropped; missing id/custom normalize
|
|
551
|
+
* to empty.
|
|
552
|
+
* @param text - the settled result text.
|
|
553
|
+
* @returns the normalized entries, or undefined when unparseable/malformed.
|
|
554
|
+
*/
|
|
555
|
+
function parseAnswerEntries(text) {
|
|
556
|
+
let parsed;
|
|
557
|
+
try {
|
|
558
|
+
parsed = JSON.parse(text);
|
|
559
|
+
} catch {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
563
|
+
const answers = parsed.answers;
|
|
564
|
+
if (!Array.isArray(answers)) return void 0;
|
|
565
|
+
if (!answers.every((entry) => typeof entry === "object" && entry !== null)) return void 0;
|
|
566
|
+
return answers.map((entry) => {
|
|
567
|
+
const candidate = entry;
|
|
568
|
+
return {
|
|
569
|
+
id: typeof candidate.id === "string" ? candidate.id : "",
|
|
570
|
+
selected: Array.isArray(candidate.selected) ? candidate.selected.filter((item) => typeof item === "string") : [],
|
|
571
|
+
custom: typeof candidate.custom === "string" ? candidate.custom : ""
|
|
572
|
+
};
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
424
576
|
* The answered-count summary for a settled `ask_user_question` result
|
|
425
577
|
* (Web AskQuestionRow parity): parses the tool's `{"answers":[…]}` render
|
|
426
578
|
* text and counts the entries that actually carry an answer — a non-empty
|
|
@@ -432,6 +584,43 @@ function resultTextLines(blocks, error) {
|
|
|
432
584
|
* @returns the answered-count summary, or undefined when unparseable.
|
|
433
585
|
*/
|
|
434
586
|
function askAnswersSummary(text) {
|
|
587
|
+
const entries = parseAnswerEntries(text);
|
|
588
|
+
if (entries === void 0) return void 0;
|
|
589
|
+
return `${entries.filter((entry) => entry.selected.length > 0 || entry.custom !== "").length}/${entries.length} answered`;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* The per-question display lines for a settled `ask_user_question` result
|
|
593
|
+
* (the expanded card's body, complementing {@link askAnswersSummary}'s
|
|
594
|
+
* count). Parses the tool's `{"answers":[…]}` render text and renders one
|
|
595
|
+
* line per entry: `● <id> → <selected, custom>` for an answered question,
|
|
596
|
+
* `○ <id> — skipped` when neither is present. Shares the normalization with
|
|
597
|
+
* {@link askAnswersSummary}, so the rendered rows and the count always
|
|
598
|
+
* agree; an unparseable/malformed text returns undefined.
|
|
599
|
+
* @param text - the settled result text.
|
|
600
|
+
* @returns the display lines, or undefined when unparseable/malformed.
|
|
601
|
+
*/
|
|
602
|
+
function askAnswersLines(text) {
|
|
603
|
+
const entries = parseAnswerEntries(text);
|
|
604
|
+
if (entries === void 0) return void 0;
|
|
605
|
+
return entries.map((entry) => {
|
|
606
|
+
if (entry.selected.length > 0) return {
|
|
607
|
+
text: `● ${entry.id} → ${entry.selected.join(", ")}`,
|
|
608
|
+
skipped: false
|
|
609
|
+
};
|
|
610
|
+
if (entry.custom !== "") return {
|
|
611
|
+
text: `● ${entry.id} → ${entry.custom}`,
|
|
612
|
+
skipped: false
|
|
613
|
+
};
|
|
614
|
+
return {
|
|
615
|
+
text: `○ ${entry.id} — skipped`,
|
|
616
|
+
skipped: true
|
|
617
|
+
};
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
/** Parse a settled goal-tool result text, or undefined when it is not the
|
|
621
|
+
* expected `{"goal":…}` JSON (the caller then falls back to the generic
|
|
622
|
+
* presentation). */
|
|
623
|
+
function parseGoalResult(text) {
|
|
435
624
|
let parsed;
|
|
436
625
|
try {
|
|
437
626
|
parsed = JSON.parse(text);
|
|
@@ -439,14 +628,72 @@ function askAnswersSummary(text) {
|
|
|
439
628
|
return;
|
|
440
629
|
}
|
|
441
630
|
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
442
|
-
const
|
|
443
|
-
if (
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
631
|
+
const value = parsed;
|
|
632
|
+
if (value.goal === null) return { goal: null };
|
|
633
|
+
if (typeof value.goal !== "object" || value.goal === null) return void 0;
|
|
634
|
+
const goal = value.goal;
|
|
635
|
+
const blocked = typeof goal.blockedReason === "object" && goal.blockedReason !== null ? goal.blockedReason : void 0;
|
|
636
|
+
return {
|
|
637
|
+
goal: {
|
|
638
|
+
...typeof goal.id === "string" ? { id: goal.id } : {},
|
|
639
|
+
...typeof goal.revision === "number" ? { revision: goal.revision } : {},
|
|
640
|
+
...typeof goal.objective === "string" ? { objective: goal.objective } : {},
|
|
641
|
+
...typeof goal.phase === "string" ? { phase: goal.phase } : {},
|
|
642
|
+
...typeof goal.roundsStarted === "number" ? { roundsStarted: goal.roundsStarted } : {},
|
|
643
|
+
...typeof goal.maxGoalRounds === "number" ? { maxGoalRounds: goal.maxGoalRounds } : {},
|
|
644
|
+
...blocked !== void 0 ? { blockedReason: {
|
|
645
|
+
...typeof blocked.code === "string" ? { code: blocked.code } : {},
|
|
646
|
+
...typeof blocked.message === "string" ? { message: blocked.message } : {}
|
|
647
|
+
} } : {}
|
|
648
|
+
},
|
|
649
|
+
...typeof value.activation === "string" ? { activation: value.activation } : {}
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* The one-line folded summary for a settled goal-tool result: `phase … ·
|
|
654
|
+
* revision N · N/M rounds`, or `no goal set` for `{"goal":null}`.
|
|
655
|
+
* Returns undefined when the text is not the expected result JSON (the
|
|
656
|
+
* caller then falls back to the generic preview instead of inventing one).
|
|
657
|
+
* @param text - the settled result text.
|
|
658
|
+
*/
|
|
659
|
+
function goalResultSummary(text) {
|
|
660
|
+
const value = parseGoalResult(text);
|
|
661
|
+
if (value === void 0) return void 0;
|
|
662
|
+
const goal = value.goal;
|
|
663
|
+
if (goal === null) return "no goal set";
|
|
664
|
+
const parts = [];
|
|
665
|
+
if (goal.phase !== void 0) parts.push(`phase ${goal.phase}`);
|
|
666
|
+
if (goal.revision !== void 0) parts.push(`revision ${goal.revision}`);
|
|
667
|
+
if (goal.roundsStarted !== void 0) parts.push(goal.maxGoalRounds === void 0 ? `${goal.roundsStarted} rounds` : `${goal.roundsStarted}/${goal.maxGoalRounds} rounds`);
|
|
668
|
+
return parts.length === 0 ? "goal" : parts.join(" · ");
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* The expanded-card display lines for a settled goal-tool result: one field
|
|
672
|
+
* per line (`● objective: …`, `● phase: … · revision N`, `● rounds: N/M`,
|
|
673
|
+
* `● activation: …`, `● blocked: code — message`), or a single `no goal
|
|
674
|
+
* set` line for `{"goal":null}`. Returns undefined when the text is not the
|
|
675
|
+
* expected result JSON (the caller falls back to the generic presentation).
|
|
676
|
+
* @param text - the settled result text.
|
|
677
|
+
*/
|
|
678
|
+
function goalResultLines(text) {
|
|
679
|
+
const value = parseGoalResult(text);
|
|
680
|
+
if (value === void 0) return void 0;
|
|
681
|
+
const goal = value.goal;
|
|
682
|
+
if (goal === null) return ["no goal set"];
|
|
683
|
+
const lines = [];
|
|
684
|
+
if (goal.objective !== void 0 && goal.objective !== "") lines.push(`● objective: ${goal.objective}`);
|
|
685
|
+
const identity = [];
|
|
686
|
+
if (typeof goal.phase === "string" && goal.phase !== "") identity.push(goal.phase);
|
|
687
|
+
if (goal.revision !== void 0) identity.push(`revision ${goal.revision}`);
|
|
688
|
+
if (identity.length > 0) lines.push(`● ${identity.join(" · ")}`);
|
|
689
|
+
if (goal.roundsStarted !== void 0) lines.push(goal.maxGoalRounds === void 0 ? `● rounds: ${goal.roundsStarted}` : `● rounds: ${goal.roundsStarted}/${goal.maxGoalRounds}`);
|
|
690
|
+
const blocked = goal.blockedReason;
|
|
691
|
+
if (blocked !== void 0 && (blocked.code !== void 0 || blocked.message !== void 0)) {
|
|
692
|
+
const detail = [blocked.code, blocked.message].filter((part) => typeof part === "string" && part !== "").join(" — ");
|
|
693
|
+
lines.push(`● blocked: ${detail}`);
|
|
694
|
+
}
|
|
695
|
+
if (value.activation !== void 0) lines.push(`● activation: ${value.activation}`);
|
|
696
|
+
return lines;
|
|
450
697
|
}
|
|
451
698
|
/**
|
|
452
699
|
* The folded-card call preview for tools whose args carry a one-line
|
|
@@ -519,6 +766,41 @@ function toolPresenterFrom(get) {
|
|
|
519
766
|
}
|
|
520
767
|
};
|
|
521
768
|
}
|
|
769
|
+
/**
|
|
770
|
+
* Subagent-family tool names whose call args may carry an explicit
|
|
771
|
+
* model/provider override. Matched by REGISTERED name; unknown names are
|
|
772
|
+
* ignored so third-party subagent-like tools keep their default rendering.
|
|
773
|
+
*/
|
|
774
|
+
const SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
775
|
+
"subagent",
|
|
776
|
+
"subagent_route",
|
|
777
|
+
"subagent_router",
|
|
778
|
+
"subagent_fork"
|
|
779
|
+
]);
|
|
780
|
+
/**
|
|
781
|
+
* Extract the model · provider display line for a subagent-family tool call.
|
|
782
|
+
*
|
|
783
|
+
* COMPATIBILITY CONTRACT: the line renders ONLY when the call args actually
|
|
784
|
+
* carry a model/provider — the top-level `model`/`provider` shape
|
|
785
|
+
* (subagent_route / subagent_router) or the `agentOptions.model/provider`
|
|
786
|
+
* shape. Every other call returns `undefined` and the card renders exactly
|
|
787
|
+
* as before; unknown tool names and unparseable args are ignored. The
|
|
788
|
+
* official `subagent` tool's model lives in deployment config (never in
|
|
789
|
+
* the call args), so it naturally renders nothing.
|
|
790
|
+
* @param name - the tool's registered name.
|
|
791
|
+
* @param argsRaw - the raw tool-call arguments JSON ('' or malformed → none).
|
|
792
|
+
* @returns `model · provider` (either part alone when only one is present),
|
|
793
|
+
* or undefined when the call carries no model/provider.
|
|
794
|
+
*/
|
|
795
|
+
function subagentModelDisplay(name, argsRaw) {
|
|
796
|
+
if (!SUBAGENT_TOOL_NAMES.has(name)) return void 0;
|
|
797
|
+
const parsed = parseArgs(argsRaw);
|
|
798
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
799
|
+
const args = parsed;
|
|
800
|
+
const opts = typeof args.agentOptions === "object" && args.agentOptions !== null ? args.agentOptions : void 0;
|
|
801
|
+
const parts = [typeof args.model === "string" && args.model !== "" ? args.model : typeof opts?.model === "string" && opts.model !== "" ? opts.model : void 0, typeof args.provider === "string" && args.provider !== "" ? args.provider : typeof opts?.provider === "string" && opts.provider !== "" ? opts.provider : void 0].filter((part) => part !== void 0);
|
|
802
|
+
return parts.length === 0 ? void 0 : parts.join(" · ");
|
|
803
|
+
}
|
|
522
804
|
//#endregion
|
|
523
805
|
//#region src/context.ts
|
|
524
806
|
/**
|
|
@@ -13154,6 +13436,7 @@ var TaskBrowserPanel = class {
|
|
|
13154
13436
|
searchEnabled;
|
|
13155
13437
|
onSelect;
|
|
13156
13438
|
onCancel;
|
|
13439
|
+
onAction;
|
|
13157
13440
|
/** Live tick: bumped every second while a running row is visible. */
|
|
13158
13441
|
now = Date.now();
|
|
13159
13442
|
tickTimer;
|
|
@@ -13168,6 +13451,7 @@ var TaskBrowserPanel = class {
|
|
|
13168
13451
|
this.options = options;
|
|
13169
13452
|
this.onSelect = onSelect;
|
|
13170
13453
|
this.onCancel = onCancel;
|
|
13454
|
+
this.onAction = options.onAction;
|
|
13171
13455
|
this.requestRender = requestRender;
|
|
13172
13456
|
this.searchEnabled = options.enableSearch ?? false;
|
|
13173
13457
|
this.searchInput.onEscape = () => this.onCancel();
|
|
@@ -13230,6 +13514,18 @@ var TaskBrowserPanel = class {
|
|
|
13230
13514
|
const isNavUp = matchesKey(data, "up") || !this.searchEnabled && data === "k";
|
|
13231
13515
|
const isNavDown = matchesKey(data, "down") || !this.searchEnabled && data === "j";
|
|
13232
13516
|
const isPage = matchesKey(data, "pageUp") || matchesKey(data, "pageDown");
|
|
13517
|
+
if (data === "i" && this.onAction !== void 0 && !this.searchEnabled) {
|
|
13518
|
+
const item = this.filtered[this.selected];
|
|
13519
|
+
if (item !== void 0) this.onAction(item.value, "interrupt");
|
|
13520
|
+
return;
|
|
13521
|
+
}
|
|
13522
|
+
if (data === "i" && this.onAction !== void 0 && this.searchEnabled && (this.searchInput.getValue() ?? "") === "") {
|
|
13523
|
+
const item = this.filtered[this.selected];
|
|
13524
|
+
if (item !== void 0 && item.value.startsWith("agent:")) {
|
|
13525
|
+
this.onAction(item.value, "interrupt");
|
|
13526
|
+
return;
|
|
13527
|
+
}
|
|
13528
|
+
}
|
|
13233
13529
|
if (!isNavUp && !isNavDown && !isPage && !matchesKey(data, "enter") && !matchesKey(data, "escape") && this.searchEnabled) {
|
|
13234
13530
|
this.searchInput.handleInput(data);
|
|
13235
13531
|
this.applyFilter(this.searchInput.getValue() ?? "");
|
|
@@ -13270,7 +13566,7 @@ var TaskBrowserPanel = class {
|
|
|
13270
13566
|
if (query === "") this.filtered = this.items;
|
|
13271
13567
|
else {
|
|
13272
13568
|
const needle = query.toLowerCase();
|
|
13273
|
-
this.filtered = this.items.filter((item) => `${item.value}\n${item.label}\n${item.status}\n${item.detail ?? ""}`.toLowerCase().includes(needle));
|
|
13569
|
+
this.filtered = this.items.filter((item) => `${item.value}\n${item.label}\n${item.status}\n${item.detail ?? ""}\n${item.group ?? ""}`.toLowerCase().includes(needle));
|
|
13274
13570
|
}
|
|
13275
13571
|
this.selected = 0;
|
|
13276
13572
|
this.scroll = 0;
|
|
@@ -13346,7 +13642,7 @@ var TaskBrowserPanel = class {
|
|
|
13346
13642
|
return out;
|
|
13347
13643
|
}
|
|
13348
13644
|
hint() {
|
|
13349
|
-
return `${this.searchEnabled ? "type to filter · " : ""}↑↓ navigate · enter open · esc close`;
|
|
13645
|
+
return `${this.searchEnabled ? "type to filter · " : ""}${this.filtered.some((item) => item.value.startsWith("agent:")) ? "i interrupt · " : ""}↑↓ navigate · enter open · esc close`;
|
|
13350
13646
|
}
|
|
13351
13647
|
};
|
|
13352
13648
|
//#endregion
|
|
@@ -14120,6 +14416,298 @@ var QuestionFlow = class {
|
|
|
14120
14416
|
}
|
|
14121
14417
|
};
|
|
14122
14418
|
//#endregion
|
|
14419
|
+
//#region src/shell-words.ts
|
|
14420
|
+
/**
|
|
14421
|
+
* A small, reliable POSIX-ish shell word parser for external-command
|
|
14422
|
+
* strings like `$VISUAL`/`$EDITOR` (`code --wait`, `vim -f`, `sensible-editor
|
|
14423
|
+
* --nofork`) and `!` shell lines. It handles the quoting rules that matter
|
|
14424
|
+
* in practice — single quotes (literal), double quotes (backslash escapes,
|
|
14425
|
+
* everything else verbatim — no expansion), and backslash escapes outside
|
|
14426
|
+
* quotes — WITHOUT executing a shell.
|
|
14427
|
+
*
|
|
14428
|
+
* This deliberately replaces naive `command.split(/\s+/)` splitting: an
|
|
14429
|
+
* editor path with spaces or a quoted argument would be mangled into
|
|
14430
|
+
* phantom arguments.
|
|
14431
|
+
* @module @xmoon76/dsh-pi-tui/shell-words
|
|
14432
|
+
*/
|
|
14433
|
+
/**
|
|
14434
|
+
* Split a command line into words, POSIX-ish. Never throws: unbalanced
|
|
14435
|
+
* quotes are treated as a trailing literal word (matching how shells
|
|
14436
|
+
* consume the rest of the line). Backslashes before non-special characters
|
|
14437
|
+
* are dropped outside quotes; inside double quotes a backslash only escapes
|
|
14438
|
+
* `"`, `\`, and `$` and is kept verbatim before anything else.
|
|
14439
|
+
* @param line - the raw command line.
|
|
14440
|
+
* @returns the parsed words.
|
|
14441
|
+
*/
|
|
14442
|
+
function parseShellWords(line) {
|
|
14443
|
+
const words = [];
|
|
14444
|
+
let current = "";
|
|
14445
|
+
let inWord = false;
|
|
14446
|
+
let quote;
|
|
14447
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
14448
|
+
const char = line[index];
|
|
14449
|
+
if (quote === void 0 && char === "\\") {
|
|
14450
|
+
const next = line[index + 1];
|
|
14451
|
+
if (next !== void 0) {
|
|
14452
|
+
current += next;
|
|
14453
|
+
index += 1;
|
|
14454
|
+
} else current += "\\";
|
|
14455
|
+
inWord = true;
|
|
14456
|
+
continue;
|
|
14457
|
+
}
|
|
14458
|
+
if (quote === void 0 && (char === "'" || char === "\"")) {
|
|
14459
|
+
quote = char;
|
|
14460
|
+
inWord = true;
|
|
14461
|
+
continue;
|
|
14462
|
+
}
|
|
14463
|
+
if (quote === "\"" && char === "\\") {
|
|
14464
|
+
const next = line[index + 1];
|
|
14465
|
+
if (next === "\"" || next === "\\" || next === "$") {
|
|
14466
|
+
current += next;
|
|
14467
|
+
index += 1;
|
|
14468
|
+
} else current += "\\";
|
|
14469
|
+
inWord = true;
|
|
14470
|
+
continue;
|
|
14471
|
+
}
|
|
14472
|
+
if (quote === "'" && char === "'") {
|
|
14473
|
+
quote = void 0;
|
|
14474
|
+
continue;
|
|
14475
|
+
}
|
|
14476
|
+
if (quote === "\"" && char === "\"") {
|
|
14477
|
+
quote = void 0;
|
|
14478
|
+
continue;
|
|
14479
|
+
}
|
|
14480
|
+
if (quote === void 0 && (char === " " || char === " " || char === "\n")) {
|
|
14481
|
+
if (inWord) {
|
|
14482
|
+
words.push(current);
|
|
14483
|
+
current = "";
|
|
14484
|
+
inWord = false;
|
|
14485
|
+
}
|
|
14486
|
+
continue;
|
|
14487
|
+
}
|
|
14488
|
+
current += char;
|
|
14489
|
+
inWord = true;
|
|
14490
|
+
}
|
|
14491
|
+
if (inWord) words.push(current);
|
|
14492
|
+
return words;
|
|
14493
|
+
}
|
|
14494
|
+
//#endregion
|
|
14495
|
+
//#region src/shell-completion.ts
|
|
14496
|
+
/**
|
|
14497
|
+
* Bash completion for `!`/`!!` shell lines: a real-shell `compgen` bridge
|
|
14498
|
+
* (docs/input-and-card-polish.md §1). Command names come from
|
|
14499
|
+
* `compgen -A command` (cached per cwd+PATH for 30s), `$VAR` names from
|
|
14500
|
+
* `compgen -A variable`, and subcommands from a small per-command table
|
|
14501
|
+
* (`git --list-cmds`). Path completion stays with the fork's fd-backed
|
|
14502
|
+
* provider — the bridge never competes with it.
|
|
14503
|
+
*
|
|
14504
|
+
* Decisions that matter (design doc):
|
|
14505
|
+
* - `bash -lc`, never `-ic`: an interactive shell sources `.bashrc` on
|
|
14506
|
+
* every keystroke (slow, and runs user startup code). Alias completion
|
|
14507
|
+
* is therefore not provided — accepted limitation.
|
|
14508
|
+
* - One spawn per request, hard-capped at COMPGEN_TIMEOUT_MS and wired to
|
|
14509
|
+
* the editor's AbortSignal; the caller commits latest-only. A slow or
|
|
14510
|
+
* missing `bash` degrades to no suggestions, never an error.
|
|
14511
|
+
* - The completion prefix crosses to bash through an environment variable
|
|
14512
|
+
* (COMPGEN_WORD), never string interpolation — no shell injection from
|
|
14513
|
+
* the user's own draft.
|
|
14514
|
+
* @module @xmoon76/dsh-pi-tui/shell-completion
|
|
14515
|
+
*/
|
|
14516
|
+
/** Hard cap on one compgen spawn (ms). */
|
|
14517
|
+
const COMPGEN_TIMEOUT_MS = 300;
|
|
14518
|
+
/** Command-name cache TTL (ms): command sets change rarely. */
|
|
14519
|
+
const COMMAND_CACHE_TTL_MS = 3e4;
|
|
14520
|
+
/** Suggestion cap per request (the fork's own lists are capped too). */
|
|
14521
|
+
const MAX_SUGGESTIONS = 50;
|
|
14522
|
+
/**
|
|
14523
|
+
* Parse the editor line at the cursor for shell completion. Returns a
|
|
14524
|
+
* context only when the line is a `!`/`!!` line AND the cursor completes a
|
|
14525
|
+
* shell-bridge word (command name, subcommand, `$VAR`); a path position
|
|
14526
|
+
* returns undefined so the caller falls through to the fork's fd
|
|
14527
|
+
* completion — paths are not the bridge's job.
|
|
14528
|
+
* @param line - the full editor line.
|
|
14529
|
+
* @param col - the cursor column.
|
|
14530
|
+
*/
|
|
14531
|
+
function shellCompletionContext(line, col) {
|
|
14532
|
+
if (!line.startsWith("!")) return void 0;
|
|
14533
|
+
const prefixLength = line.startsWith("!!") ? 2 : 1;
|
|
14534
|
+
const before = line.slice(prefixLength, col);
|
|
14535
|
+
const lastSpace = Math.max(before.lastIndexOf(" "), before.lastIndexOf(" "));
|
|
14536
|
+
const currentWord = before.slice(lastSpace + 1);
|
|
14537
|
+
const priorWords = parseShellWords(lastSpace === -1 ? "" : before.slice(0, lastSpace));
|
|
14538
|
+
if (priorWords.length === 0) {
|
|
14539
|
+
if (currentWord.startsWith(".") || currentWord.startsWith("/") || currentWord.startsWith("~")) return void 0;
|
|
14540
|
+
return {
|
|
14541
|
+
kind: "command",
|
|
14542
|
+
prefix: currentWord,
|
|
14543
|
+
priorWords
|
|
14544
|
+
};
|
|
14545
|
+
}
|
|
14546
|
+
if (currentWord.startsWith("$")) return {
|
|
14547
|
+
kind: "variable",
|
|
14548
|
+
prefix: currentWord,
|
|
14549
|
+
priorWords
|
|
14550
|
+
};
|
|
14551
|
+
if (priorWords.length === 1 && SUBCOMMAND_TABLE[priorWords[0]] !== void 0) return {
|
|
14552
|
+
kind: "subcommand",
|
|
14553
|
+
prefix: currentWord,
|
|
14554
|
+
priorWords
|
|
14555
|
+
};
|
|
14556
|
+
}
|
|
14557
|
+
/**
|
|
14558
|
+
* Per-command subcommand completion. `lister` emits the live list (one
|
|
14559
|
+
* name per line, no headers) when the installed command supports it;
|
|
14560
|
+
* `fallback` covers older versions (e.g. git before 2.18 has no
|
|
14561
|
+
* `--list-cmds`) and is used when the lister produces nothing. Extend the
|
|
14562
|
+
* table with one entry per command.
|
|
14563
|
+
*/
|
|
14564
|
+
const SUBCOMMAND_TABLE = { git: {
|
|
14565
|
+
lister: "git --list-cmds 2>/dev/null",
|
|
14566
|
+
fallback: [
|
|
14567
|
+
"add",
|
|
14568
|
+
"am",
|
|
14569
|
+
"archive",
|
|
14570
|
+
"bisect",
|
|
14571
|
+
"branch",
|
|
14572
|
+
"bundle",
|
|
14573
|
+
"checkout",
|
|
14574
|
+
"cherry-pick",
|
|
14575
|
+
"clean",
|
|
14576
|
+
"clone",
|
|
14577
|
+
"commit",
|
|
14578
|
+
"config",
|
|
14579
|
+
"diff",
|
|
14580
|
+
"fetch",
|
|
14581
|
+
"init",
|
|
14582
|
+
"log",
|
|
14583
|
+
"merge",
|
|
14584
|
+
"mv",
|
|
14585
|
+
"pull",
|
|
14586
|
+
"push",
|
|
14587
|
+
"rebase",
|
|
14588
|
+
"reset",
|
|
14589
|
+
"restore",
|
|
14590
|
+
"revert",
|
|
14591
|
+
"rm",
|
|
14592
|
+
"show",
|
|
14593
|
+
"stash",
|
|
14594
|
+
"status",
|
|
14595
|
+
"switch",
|
|
14596
|
+
"tag"
|
|
14597
|
+
]
|
|
14598
|
+
} };
|
|
14599
|
+
let compgenRunner = (cwd, expression, prefix, signal) => runCompgenSpawn(cwd, expression, prefix, signal);
|
|
14600
|
+
function runCompgenSpawn(cwd, expression, prefix, signal) {
|
|
14601
|
+
return new Promise((resolve) => {
|
|
14602
|
+
const settle = (ok, lines) => {
|
|
14603
|
+
if (settled) return;
|
|
14604
|
+
settled = true;
|
|
14605
|
+
clearTimeout(timer);
|
|
14606
|
+
signal.removeEventListener("abort", onAbort);
|
|
14607
|
+
resolve({
|
|
14608
|
+
ok,
|
|
14609
|
+
lines
|
|
14610
|
+
});
|
|
14611
|
+
};
|
|
14612
|
+
let settled = false;
|
|
14613
|
+
const onAbort = () => {
|
|
14614
|
+
child.kill();
|
|
14615
|
+
settle(false, []);
|
|
14616
|
+
};
|
|
14617
|
+
const child = spawn("bash", ["-lc", expression], {
|
|
14618
|
+
cwd,
|
|
14619
|
+
stdio: [
|
|
14620
|
+
"ignore",
|
|
14621
|
+
"pipe",
|
|
14622
|
+
"pipe"
|
|
14623
|
+
],
|
|
14624
|
+
env: {
|
|
14625
|
+
...process.env,
|
|
14626
|
+
COMPGEN_WORD: prefix
|
|
14627
|
+
}
|
|
14628
|
+
});
|
|
14629
|
+
const timer = setTimeout(() => {
|
|
14630
|
+
child.kill("SIGKILL");
|
|
14631
|
+
settle(false, []);
|
|
14632
|
+
}, COMPGEN_TIMEOUT_MS);
|
|
14633
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
14634
|
+
let out = "";
|
|
14635
|
+
child.stdout.on("data", (chunk) => {
|
|
14636
|
+
out += chunk.toString("utf8");
|
|
14637
|
+
});
|
|
14638
|
+
child.stderr.on("data", () => {});
|
|
14639
|
+
child.on("error", () => settle(false, []));
|
|
14640
|
+
child.on("close", (code) => {
|
|
14641
|
+
settle(code === 0, out.split("\n").filter((line) => line !== ""));
|
|
14642
|
+
});
|
|
14643
|
+
});
|
|
14644
|
+
}
|
|
14645
|
+
/** Command-name cache, keyed by cwd + PATH (30s TTL). Only SUCCESSFUL runs
|
|
14646
|
+
* are cached — a failed run (timeout/abort/spawn error) must not suppress
|
|
14647
|
+
* completion for the whole TTL, so the next request retries the spawn. */
|
|
14648
|
+
const commandCache = /* @__PURE__ */ new Map();
|
|
14649
|
+
/** The cached `compgen -A command` list for one cwd+PATH, refreshed on
|
|
14650
|
+
* expiry or after a failed run. */
|
|
14651
|
+
async function cachedCommands(cwd, signal) {
|
|
14652
|
+
const key = `${cwd}\0${process.env.PATH ?? ""}`;
|
|
14653
|
+
const entry = commandCache.get(key);
|
|
14654
|
+
if (entry !== void 0 && entry.expires > Date.now()) return entry.commands;
|
|
14655
|
+
const run = await compgenRunner(cwd, "compgen -A command", "", signal);
|
|
14656
|
+
if (!run.ok) return [];
|
|
14657
|
+
commandCache.set(key, {
|
|
14658
|
+
expires: Date.now() + COMMAND_CACHE_TTL_MS,
|
|
14659
|
+
commands: run.lines
|
|
14660
|
+
});
|
|
14661
|
+
return run.lines;
|
|
14662
|
+
}
|
|
14663
|
+
/** Filter one candidate list to the prefix and cap it ('' prefix keeps all). */
|
|
14664
|
+
function matchesFor(prefix, candidates) {
|
|
14665
|
+
const matched = prefix === "" ? candidates : candidates.filter((name) => name.startsWith(prefix));
|
|
14666
|
+
if (matched.length === 0) return null;
|
|
14667
|
+
return {
|
|
14668
|
+
items: matched.slice(0, MAX_SUGGESTIONS).map((name) => ({
|
|
14669
|
+
value: name,
|
|
14670
|
+
label: name
|
|
14671
|
+
})),
|
|
14672
|
+
prefix
|
|
14673
|
+
};
|
|
14674
|
+
}
|
|
14675
|
+
/**
|
|
14676
|
+
* Suggest shell completions for a parsed `!` line context. Returns null
|
|
14677
|
+
* when nothing matches or the shell is unavailable — the editor then shows
|
|
14678
|
+
* no suggestions (the caller falls through to the fork's own provider).
|
|
14679
|
+
* @param context - the parsed cursor context.
|
|
14680
|
+
* @param cwd - the session workspace (compgen runs there).
|
|
14681
|
+
* @param options - the editor's request options (signal; force: an empty
|
|
14682
|
+
* command prefix lists the cached commands on explicit Tab, while a
|
|
14683
|
+
* natural trigger with an empty prefix stays quiet).
|
|
14684
|
+
*/
|
|
14685
|
+
async function suggestShellCompletion(context, cwd, options) {
|
|
14686
|
+
if (options.signal.aborted) return null;
|
|
14687
|
+
if (context.kind === "command") {
|
|
14688
|
+
if (context.prefix === "" && options.force !== true) return null;
|
|
14689
|
+
const commands = await cachedCommands(cwd, options.signal);
|
|
14690
|
+
return matchesFor(context.prefix, commands);
|
|
14691
|
+
}
|
|
14692
|
+
if (context.kind === "variable") {
|
|
14693
|
+
const run = await compgenRunner(cwd, "compgen -A variable -- \"$COMPGEN_WORD\"", context.prefix.slice(1), options.signal);
|
|
14694
|
+
if (!run.ok) return null;
|
|
14695
|
+
const items = run.lines.slice(0, MAX_SUGGESTIONS).map((name) => ({
|
|
14696
|
+
value: `$${name}`,
|
|
14697
|
+
label: `$${name}`
|
|
14698
|
+
}));
|
|
14699
|
+
return items.length === 0 ? null : {
|
|
14700
|
+
items,
|
|
14701
|
+
prefix: context.prefix
|
|
14702
|
+
};
|
|
14703
|
+
}
|
|
14704
|
+
const command = context.priorWords[0];
|
|
14705
|
+
const entry = SUBCOMMAND_TABLE[command];
|
|
14706
|
+
const run = await compgenRunner(cwd, entry.lister, "", options.signal);
|
|
14707
|
+
const candidates = run.ok && run.lines.length > 0 ? run.lines.map((line) => line.trim()).filter((line) => line !== "") : entry.fallback;
|
|
14708
|
+
return matchesFor(context.prefix, candidates);
|
|
14709
|
+
}
|
|
14710
|
+
//#endregion
|
|
14123
14711
|
//#region src/mentions.ts
|
|
14124
14712
|
/**
|
|
14125
14713
|
* @-file mention completion for the editor: a consumer-side wrapper over the
|
|
@@ -14270,13 +14858,19 @@ var MentionProvider = class {
|
|
|
14270
14858
|
this.inner = new CombinedAutocompleteProvider([...slashCommands], workDir, fdPath);
|
|
14271
14859
|
}
|
|
14272
14860
|
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
14273
|
-
const
|
|
14861
|
+
const currentLine = lines[cursorLine] ?? "";
|
|
14862
|
+
const atPrefix = extractAtPrefix(currentLine.slice(0, cursorCol));
|
|
14274
14863
|
if (atPrefix !== null) {
|
|
14275
14864
|
if (this.fdPath !== null) try {
|
|
14276
14865
|
return await this.inner.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
14277
14866
|
} catch {}
|
|
14278
14867
|
return fsMentionSuggestions(this.workDir, atPrefix, options.signal);
|
|
14279
14868
|
}
|
|
14869
|
+
const shellContext = shellCompletionContext(currentLine, cursorCol);
|
|
14870
|
+
if (shellContext !== void 0) {
|
|
14871
|
+
const suggestions = await suggestShellCompletion(shellContext, this.workDir, options);
|
|
14872
|
+
if (suggestions !== null) return suggestions;
|
|
14873
|
+
}
|
|
14280
14874
|
try {
|
|
14281
14875
|
return await this.inner.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
14282
14876
|
} catch {
|
|
@@ -14284,6 +14878,19 @@ var MentionProvider = class {
|
|
|
14284
14878
|
}
|
|
14285
14879
|
}
|
|
14286
14880
|
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
14881
|
+
const currentLine = lines[cursorLine] ?? "";
|
|
14882
|
+
if (shellCompletionContext(currentLine, cursorCol) !== void 0) {
|
|
14883
|
+
const before = currentLine.slice(0, cursorCol - prefix.length);
|
|
14884
|
+
const after = currentLine.slice(cursorCol);
|
|
14885
|
+
const newLine = `${before}${item.value} ${after}`;
|
|
14886
|
+
const newLines = [...lines];
|
|
14887
|
+
newLines[cursorLine] = newLine;
|
|
14888
|
+
return {
|
|
14889
|
+
lines: newLines,
|
|
14890
|
+
cursorLine,
|
|
14891
|
+
cursorCol: before.length + item.value.length + 1
|
|
14892
|
+
};
|
|
14893
|
+
}
|
|
14287
14894
|
return this.inner.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
14288
14895
|
}
|
|
14289
14896
|
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
|
|
@@ -14820,9 +15427,9 @@ function describeKey(key) {
|
|
|
14820
15427
|
* This inventory is the SINGLE authoritative list, kept in sync with the
|
|
14821
15428
|
* host's `matchesKey` lifecycle checks in tui-app.ts (Ctrl+C/D exit,
|
|
14822
15429
|
* Ctrl+S steer-all, Ctrl+F transcript search, Ctrl+Shift+F search, Ctrl+O
|
|
14823
|
-
* expand, Ctrl+T todo panel, Ctrl+G external editor, Ctrl+
|
|
14824
|
-
*
|
|
14825
|
-
*
|
|
15430
|
+
* expand, Ctrl+T todo panel, Ctrl+G external editor, Ctrl+Enter queue,
|
|
15431
|
+
* Enter submit, Esc cancel, Shift+Tab permission cycle, Alt+Up dequeue,
|
|
15432
|
+
* Alt+T thinking toggle). Every
|
|
14826
15433
|
* reserved binding here must match a host `matchesKey(data, ...)` call;
|
|
14827
15434
|
* when a new host lifecycle key lands, extend THIS list in the same
|
|
14828
15435
|
* commit.
|
|
@@ -14884,13 +15491,6 @@ const RESERVED_HOST_KEYS = [
|
|
|
14884
15491
|
shift: false,
|
|
14885
15492
|
super: false
|
|
14886
15493
|
},
|
|
14887
|
-
{
|
|
14888
|
-
key: "j",
|
|
14889
|
-
ctrl: true,
|
|
14890
|
-
alt: false,
|
|
14891
|
-
shift: false,
|
|
14892
|
-
super: false
|
|
14893
|
-
},
|
|
14894
15494
|
{
|
|
14895
15495
|
key: "enter",
|
|
14896
15496
|
ctrl: true,
|
|
@@ -16799,6 +17399,7 @@ var QuestionFrame = class extends Frame {
|
|
|
16799
17399
|
render(width) {
|
|
16800
17400
|
const rows = Math.max(1, this.heightOf());
|
|
16801
17401
|
this.lastTermRows = rows;
|
|
17402
|
+
this.lastTermColumns = width;
|
|
16802
17403
|
const frameRows = this.flow.isBodyExpanded() ? Math.max(10, Math.min(40, Math.floor(rows * .8))) : Math.max(10, Math.min(26, Math.floor(rows * .6)));
|
|
16803
17404
|
this.flow.setMaxRows(frameRows - 2);
|
|
16804
17405
|
const out = super.render(width);
|
|
@@ -16813,12 +17414,18 @@ var QuestionFrame = class extends Frame {
|
|
|
16813
17414
|
get termRows() {
|
|
16814
17415
|
return this.lastTermRows;
|
|
16815
17416
|
}
|
|
17417
|
+
/** The terminal width the last render used (a resize changes wrapping and
|
|
17418
|
+
* the flow's hit map, so clicks must wait for a fresh render too). */
|
|
17419
|
+
get termColumns() {
|
|
17420
|
+
return this.lastTermColumns;
|
|
17421
|
+
}
|
|
16816
17422
|
get focused() {
|
|
16817
17423
|
return this.flow.focused;
|
|
16818
17424
|
}
|
|
16819
17425
|
set focused(value) {
|
|
16820
17426
|
this.flow.focused = value;
|
|
16821
17427
|
}
|
|
17428
|
+
lastTermColumns = 0;
|
|
16822
17429
|
};
|
|
16823
17430
|
/** The session head card: identity facts, wrapped to the available width so
|
|
16824
17431
|
* nothing is truncated, framed with a box whose width matches the editor's
|
|
@@ -17136,6 +17743,9 @@ var TuiApp = class TuiApp {
|
|
|
17136
17743
|
todoItems = [];
|
|
17137
17744
|
/** Ctrl+T: whether the todo panel between transcript and editor is shown. */
|
|
17138
17745
|
todoPanelVisible = false;
|
|
17746
|
+
/** Fullscreen click on the todo panel (or the panel's own expand verb):
|
|
17747
|
+
* whether the panel shows the FULL list or the compact five rows. */
|
|
17748
|
+
todoExpanded = false;
|
|
17139
17749
|
/** The todo panel Text; empty when hidden. */
|
|
17140
17750
|
todoPanel;
|
|
17141
17751
|
/**
|
|
@@ -17298,8 +17908,11 @@ var TuiApp = class TuiApp {
|
|
|
17298
17908
|
workingMessageOverride;
|
|
17299
17909
|
/** Timestamp of the last Ctrl+C press, for the empty-editor exit chord. */
|
|
17300
17910
|
lastCtrlCAt;
|
|
17301
|
-
/** The empty-editor double-Ctrl+C exit window in ms
|
|
17302
|
-
|
|
17911
|
+
/** The empty-editor double-Ctrl+C exit window in ms. A 500ms window
|
|
17912
|
+
* silently misses a human-paced double press (0.6–1s apart), which
|
|
17913
|
+
* read as "the chord doesn't work"; 1.5s covers a natural double
|
|
17914
|
+
* press, and the armed state is announced by the hint. */
|
|
17915
|
+
static CTRL_C_EXIT_WINDOW_MS = 1500;
|
|
17303
17916
|
/** Session workspace root for path relativization (Web relativizeToCwd). */
|
|
17304
17917
|
workspaceRoot;
|
|
17305
17918
|
/** The tool presentation bridge, wired by the runner to the live registry. */
|
|
@@ -17772,6 +18385,7 @@ var TuiApp = class TuiApp {
|
|
|
17772
18385
|
this.clearNotify();
|
|
17773
18386
|
this.seatEditor().setText("");
|
|
17774
18387
|
this.editorSeatHolder.notifyChanged();
|
|
18388
|
+
this.requestRender();
|
|
17775
18389
|
this.events.onQueueSubmit(text);
|
|
17776
18390
|
return { consume: true };
|
|
17777
18391
|
}
|
|
@@ -17810,10 +18424,11 @@ var TuiApp = class TuiApp {
|
|
|
17810
18424
|
const draft = this.seatEditor().getText();
|
|
17811
18425
|
this.seatEditor().setText("");
|
|
17812
18426
|
this.editorSeatHolder.notifyChanged();
|
|
18427
|
+
this.requestRender();
|
|
17813
18428
|
this.events.onSteer?.(draft);
|
|
17814
18429
|
return { consume: true };
|
|
17815
18430
|
}
|
|
17816
|
-
if (matchesKey(data, "down")
|
|
18431
|
+
if (matchesKey(data, "down")) {
|
|
17817
18432
|
if (this.tasksActive && !this.activeScreen.hasOverlayEntries && this.seatEditor().getText().trim() === "") {
|
|
17818
18433
|
this.events.onOpenTasks?.();
|
|
17819
18434
|
return { consume: true };
|
|
@@ -17831,13 +18446,17 @@ var TuiApp = class TuiApp {
|
|
|
17831
18446
|
this.seatEditor().setText("");
|
|
17832
18447
|
this.editorSeatHolder.notifyChanged();
|
|
17833
18448
|
this.lastCtrlCAt = Date.now();
|
|
18449
|
+
this.requestRender();
|
|
17834
18450
|
return { consume: true };
|
|
17835
18451
|
}
|
|
17836
18452
|
const now = Date.now();
|
|
17837
18453
|
if (this.lastCtrlCAt !== void 0 && now - this.lastCtrlCAt < TuiApp.CTRL_C_EXIT_WINDOW_MS) {
|
|
17838
18454
|
this.lastCtrlCAt = void 0;
|
|
17839
18455
|
this.events.onExit();
|
|
17840
|
-
} else
|
|
18456
|
+
} else {
|
|
18457
|
+
this.lastCtrlCAt = now;
|
|
18458
|
+
this.notify("Press Ctrl+C again to exit", "info");
|
|
18459
|
+
}
|
|
17841
18460
|
return { consume: true };
|
|
17842
18461
|
}
|
|
17843
18462
|
if (matchesKey(data, "ctrl+d")) {
|
|
@@ -18388,7 +19007,7 @@ var TuiApp = class TuiApp {
|
|
|
18388
19007
|
handleFullscreenClick(x, y) {
|
|
18389
19008
|
const question = this.activeQuestions;
|
|
18390
19009
|
if (question?.frame !== void 0) {
|
|
18391
|
-
if (this.terminal.rows !== question.frame.termRows) return;
|
|
19010
|
+
if (this.terminal.rows !== question.frame.termRows || this.terminal.columns !== question.frame.termColumns) return;
|
|
18392
19011
|
const width = this.terminal.columns;
|
|
18393
19012
|
const height = this.terminal.rows;
|
|
18394
19013
|
const footerHeight = this.footer.render(width).length;
|
|
@@ -18400,10 +19019,28 @@ var TuiApp = class TuiApp {
|
|
|
18400
19019
|
question.flow.clickRow(y - seatTop - 1);
|
|
18401
19020
|
this.requestRender();
|
|
18402
19021
|
}
|
|
18403
|
-
return;
|
|
18404
19022
|
}
|
|
19023
|
+
return;
|
|
18405
19024
|
}
|
|
18406
19025
|
const width = this.terminal.columns;
|
|
19026
|
+
const height = this.terminal.rows;
|
|
19027
|
+
const footerHeight = this.footer.render(width).length;
|
|
19028
|
+
const editorHeight = this.editorSeat.render(width).length;
|
|
19029
|
+
const workingHeight = this.working.render(width).length;
|
|
19030
|
+
const queueHeight = this.queuePane.render(width).length;
|
|
19031
|
+
const goalHeight = this.goalLine.render(width).length;
|
|
19032
|
+
const todoHeight = this.todoPanel.render(width).length;
|
|
19033
|
+
const todoBottom = Math.max(0, Math.min(height, height - footerHeight - editorHeight - workingHeight - queueHeight - goalHeight));
|
|
19034
|
+
const todoTop = Math.max(0, todoBottom - todoHeight);
|
|
19035
|
+
const dockHeight = this.dock.render(width).length;
|
|
19036
|
+
if (dockHeight > 0 && y >= todoTop - dockHeight && y < todoTop) {
|
|
19037
|
+
this.toggleTodoPanel();
|
|
19038
|
+
return;
|
|
19039
|
+
}
|
|
19040
|
+
if (todoTop < todoBottom && y >= todoTop && y < todoBottom) {
|
|
19041
|
+
this.handleTodoPanelClick();
|
|
19042
|
+
return;
|
|
19043
|
+
}
|
|
18407
19044
|
const rowInScroll = y - this.header.render(width).length;
|
|
18408
19045
|
if (rowInScroll < 0) return;
|
|
18409
19046
|
const scrollTop = this.fullscreenScroll?.scrollTop ?? 0;
|
|
@@ -19551,7 +20188,18 @@ var TuiApp = class TuiApp {
|
|
|
19551
20188
|
const rows = [];
|
|
19552
20189
|
const callPreview = parseCallPreview(message.name, message.args);
|
|
19553
20190
|
const foldedCall = header.summary === "" ? foldedCallPreview(message.name, message.args) : "";
|
|
19554
|
-
|
|
20191
|
+
let resultPreview;
|
|
20192
|
+
if (message.name === "read") resultPreview = readFoldedPreview(message.result);
|
|
20193
|
+
else if (message.name === "ask_user_question") {
|
|
20194
|
+
const summary = message.error === void 0 ? askAnswersSummary(message.result) : void 0;
|
|
20195
|
+
resultPreview = summary === void 0 ? "" : ` — ${summary}`;
|
|
20196
|
+
} else if (GOAL_TOOL_NAMES.has(message.name)) {
|
|
20197
|
+
const summary = message.error === void 0 ? goalResultSummary(message.result) : void 0;
|
|
20198
|
+
resultPreview = summary === void 0 ? "" : ` — ${summary}`;
|
|
20199
|
+
} else if (FOLDED_JSON_RESULT_TOOLS.has(message.name)) {
|
|
20200
|
+
const summary = foldedResultSummaryFor(message.name, message.result);
|
|
20201
|
+
resultPreview = summary === void 0 || summary === "" ? "" : ` — ${summary}`;
|
|
20202
|
+
} else resultPreview = message.result === "" ? "" : ` — ${preview(message.result, 3)}`;
|
|
19555
20203
|
const callHead = foldedCall === "" ? "" : foldedCall;
|
|
19556
20204
|
const headWithPreview = `${head}${callHead}${resultPreview}`;
|
|
19557
20205
|
if (callPreview?.kind === "bash" && callPreview.command !== "") {
|
|
@@ -19723,6 +20371,8 @@ var TuiApp = class TuiApp {
|
|
|
19723
20371
|
return;
|
|
19724
20372
|
}
|
|
19725
20373
|
if (message.status === "running") {
|
|
20374
|
+
const modelLine = subagentModelDisplay(message.name, message.args);
|
|
20375
|
+
if (modelLine !== void 0) card.addChild(new Text(color.textDim(modelLine), 0, 0));
|
|
19726
20376
|
const callView = this.present?.call(message.name, message.args);
|
|
19727
20377
|
if (callView !== void 0) {
|
|
19728
20378
|
if (callView.card === "diff" && callView.diffs.length > 0) {
|
|
@@ -19752,10 +20402,25 @@ var TuiApp = class TuiApp {
|
|
|
19752
20402
|
const summary = message.status === "ok" ? askAnswersSummary(message.result) : void 0;
|
|
19753
20403
|
if (summary !== void 0) {
|
|
19754
20404
|
card.addChild(new Text(color.textDim(summary), 0, 0));
|
|
20405
|
+
const answerLines = askAnswersLines(message.result);
|
|
20406
|
+
if (answerLines !== void 0) for (const line of answerLines) card.addChild(new Text(` ${line.skipped ? color.textMuted(line.text) : color.textDim(line.text)}`, 0, 0));
|
|
20407
|
+
return;
|
|
20408
|
+
}
|
|
20409
|
+
}
|
|
20410
|
+
if (GOAL_TOOL_NAMES.has(message.name)) {
|
|
20411
|
+
if (message.error !== void 0) {
|
|
20412
|
+
card.addChild(new Text(color.textDim(`${message.error.name}: ${message.error.code}`), 0, 0));
|
|
20413
|
+
return;
|
|
20414
|
+
}
|
|
20415
|
+
const goalLines = message.status === "ok" ? goalResultLines(message.result) : void 0;
|
|
20416
|
+
if (goalLines !== void 0) {
|
|
20417
|
+
for (const line of goalLines) card.addChild(new Text(color.textDim(line), 0, 0));
|
|
19755
20418
|
return;
|
|
19756
20419
|
}
|
|
19757
20420
|
}
|
|
19758
20421
|
if (message.result === "" && (message.resultBlocks?.length ?? 0) === 0) return;
|
|
20422
|
+
const settledModelLine = subagentModelDisplay(message.name, message.args);
|
|
20423
|
+
if (settledModelLine !== void 0) card.addChild(new Text(color.textDim(settledModelLine), 0, 0));
|
|
19759
20424
|
const resultView = this.present?.result(message.name, message.args, {
|
|
19760
20425
|
content: message.resultBlocks ?? [],
|
|
19761
20426
|
isError: message.status === "error",
|
|
@@ -19957,19 +20622,42 @@ var TuiApp = class TuiApp {
|
|
|
19957
20622
|
/** Toggle the todo panel between the transcript and the editor. */
|
|
19958
20623
|
toggleTodoPanel() {
|
|
19959
20624
|
this.todoPanelVisible = !this.todoPanelVisible;
|
|
20625
|
+
if (!this.todoPanelVisible) this.todoExpanded = false;
|
|
19960
20626
|
this.renderTodoPanel();
|
|
19961
20627
|
this.renderDock();
|
|
19962
20628
|
this.requestRender();
|
|
19963
20629
|
return this.todoPanelVisible;
|
|
19964
20630
|
}
|
|
20631
|
+
/** Toggle the todo panel between the compact five rows and the full list
|
|
20632
|
+
* (fullscreen click on the panel's area). */
|
|
20633
|
+
toggleTodoExpanded() {
|
|
20634
|
+
if (!this.todoPanelVisible) return false;
|
|
20635
|
+
this.todoExpanded = !this.todoExpanded;
|
|
20636
|
+
this.renderTodoPanel();
|
|
20637
|
+
this.requestRender();
|
|
20638
|
+
return this.todoExpanded;
|
|
20639
|
+
}
|
|
20640
|
+
/** The fullscreen click loop over the todo panel's own rows: compact →
|
|
20641
|
+
* full list → back to the summary row (the panel closes). The mouse thus
|
|
20642
|
+
* opens AND closes the panel without Ctrl+T; the dock summary row itself
|
|
20643
|
+
* opens it (handleFullscreenClick's dock region). */
|
|
20644
|
+
handleTodoPanelClick() {
|
|
20645
|
+
if (this.todoExpanded) this.toggleTodoPanel();
|
|
20646
|
+
else this.toggleTodoExpanded();
|
|
20647
|
+
}
|
|
19965
20648
|
/** Whether the todo panel is currently shown. */
|
|
19966
20649
|
isTodoPanelVisible() {
|
|
19967
20650
|
return this.todoPanelVisible;
|
|
19968
20651
|
}
|
|
20652
|
+
/** Whether the todo panel is showing the full list (headless-test hook). */
|
|
20653
|
+
isTodoPanelExpanded() {
|
|
20654
|
+
return this.todoExpanded;
|
|
20655
|
+
}
|
|
19969
20656
|
/**
|
|
19970
20657
|
* Rebuild the todo panel text: a border rule + `Todo` title (both indented
|
|
19971
|
-
* one cell) plus up to five rows
|
|
19972
|
-
* completed (strikethrough)
|
|
20658
|
+
* one cell) plus up to five rows by default (in_progress first, then
|
|
20659
|
+
* pending, then completed (strikethrough)); the full list when expanded
|
|
20660
|
+
* (fullscreen click on the panel toggles).
|
|
19973
20661
|
*/
|
|
19974
20662
|
renderTodoPanel() {
|
|
19975
20663
|
if (!this.todoPanelVisible) {
|
|
@@ -19981,15 +20669,16 @@ var TuiApp = class TuiApp {
|
|
|
19981
20669
|
...this.todoItems.filter((todo) => todo.status === "in_progress"),
|
|
19982
20670
|
...this.todoItems.filter((todo) => todo.status === "pending"),
|
|
19983
20671
|
...this.todoItems.filter((todo) => todo.status === "completed")
|
|
19984
|
-
]
|
|
20672
|
+
];
|
|
20673
|
+
const shown = this.todoExpanded ? ordered : ordered.slice(0, 5);
|
|
19985
20674
|
const width = Math.max(1, this.terminal.columns);
|
|
19986
20675
|
const border = color.border(` ${"─".repeat(Math.max(0, width - 2))} `);
|
|
19987
20676
|
const title = color.textStrong(" Todo");
|
|
19988
|
-
if (
|
|
20677
|
+
if (shown.length === 0) {
|
|
19989
20678
|
this.todoPanel.setText([border, title].join("\n"));
|
|
19990
20679
|
return;
|
|
19991
20680
|
}
|
|
19992
|
-
const lines =
|
|
20681
|
+
const lines = shown.map((todo) => {
|
|
19993
20682
|
const body = todo.status === "completed" ? `\x1b[9m${todo.content}\x1b[29m` : todo.content;
|
|
19994
20683
|
return `${mark(todo)} ${body}`;
|
|
19995
20684
|
});
|
|
@@ -20136,6 +20825,8 @@ var TuiApp = class TuiApp {
|
|
|
20136
20825
|
this.renderQueuePane();
|
|
20137
20826
|
this.syncExtensionState();
|
|
20138
20827
|
}
|
|
20828
|
+
/** Notice rows shown in full before the `+N more` fold (user rows are never folded). */
|
|
20829
|
+
static MAX_NOTICE_ROWS = 5;
|
|
20139
20830
|
/** Rebuild the queue pane text from the current inbox rows. */
|
|
20140
20831
|
renderQueuePane() {
|
|
20141
20832
|
const items = this.queueItems;
|
|
@@ -20146,12 +20837,20 @@ var TuiApp = class TuiApp {
|
|
|
20146
20837
|
}
|
|
20147
20838
|
const width = Math.max(1, this.terminal.columns);
|
|
20148
20839
|
const lines = [color.border(` ${"─".repeat(Math.max(0, width - 2))} `)];
|
|
20149
|
-
|
|
20150
|
-
|
|
20151
|
-
|
|
20152
|
-
|
|
20153
|
-
|
|
20154
|
-
|
|
20840
|
+
const userRows = items.filter((item) => item.notice !== true);
|
|
20841
|
+
const noticeRows = items.filter((item) => item.notice === true);
|
|
20842
|
+
const shownNotices = noticeRows.slice(0, TuiApp.MAX_NOTICE_ROWS);
|
|
20843
|
+
for (const item of userRows) {
|
|
20844
|
+
const truncated = truncateToWidth(item.text.replace(/\s+/g, " ").trim(), Math.max(1, width - visibleWidth("❯ ")), "…");
|
|
20845
|
+
lines.push(`${color.accent("❯")} ${truncated}`);
|
|
20846
|
+
}
|
|
20847
|
+
for (const item of shownNotices) {
|
|
20848
|
+
const truncated = truncateToWidth(item.text.replace(/\s+/g, " ").trim(), Math.max(1, width - visibleWidth("⏳ ")), "…");
|
|
20849
|
+
lines.push(`${color.textDim("⏳")} ${color.textDim(truncated)}`);
|
|
20850
|
+
}
|
|
20851
|
+
const folded = noticeRows.length - shownNotices.length;
|
|
20852
|
+
if (folded > 0) lines.push(color.textDim(truncateToWidth(` +${folded} more notices pending · they deliver as the next turn runs`, Math.max(1, width - 2), "…")));
|
|
20853
|
+
const hint = userRows.length > 0 ? "ctrl+s to steer all · alt+↑ to edit all" : "notices deliver after the current task · /tasks to view";
|
|
20155
20854
|
lines.push(color.textDim(truncateToWidth(` ${hint}`, Math.max(1, width - 2), "…")));
|
|
20156
20855
|
this.queuePane.setText(lines.join("\n"));
|
|
20157
20856
|
this.requestRender();
|
|
@@ -20536,7 +21235,8 @@ var TuiApp = class TuiApp {
|
|
|
20536
21235
|
header: options.header,
|
|
20537
21236
|
noMatchText: options.noMatchText,
|
|
20538
21237
|
enableSearch: options.enableSearch,
|
|
20539
|
-
initialQuery: options.initialQuery
|
|
21238
|
+
initialQuery: options.initialQuery,
|
|
21239
|
+
onAction: options.onAction
|
|
20540
21240
|
}, (value) => {
|
|
20541
21241
|
close();
|
|
20542
21242
|
onSelect(value);
|
|
@@ -24807,7 +25507,7 @@ function sortCommands(commands) {
|
|
|
24807
25507
|
//#region src/commands.ts
|
|
24808
25508
|
/**
|
|
24809
25509
|
* The TUI-owned slash commands (/exit /settings /sessions /skill /model
|
|
24810
|
-
* /new /tasks /preset /
|
|
25510
|
+
* /new /tasks /preset /search /title /copy /export
|
|
24811
25511
|
* /fork /status /login /logout /help), extracted from the runner's
|
|
24812
25512
|
* monolithic apply() so the registration surface is testable and the runner
|
|
24813
25513
|
* closure shrinks. Every command reads the live runner state through the
|
|
@@ -25252,18 +25952,43 @@ function registerTuiCommands(runner, initial) {
|
|
|
25252
25952
|
}
|
|
25253
25953
|
};
|
|
25254
25954
|
refreshCompletions();
|
|
25955
|
+
/**
|
|
25956
|
+
* Register one TUI command plus its aliases (kimi parity: an alias is
|
|
25957
|
+
* another NAME of the same logical command). Every alias registers with
|
|
25958
|
+
* the host commands service — the shared handler by default, or its own
|
|
25959
|
+
* handler when the alias keeps a fast path (e.g. /resume's direct-resume
|
|
25960
|
+
* lookup) — so host dispatch, the completion catalog (aliases are
|
|
25961
|
+
* searchable: typing `resume` completes `/resume`) and the busy-Enter
|
|
25962
|
+
* gate all see it, while the command surface lists one logical command
|
|
25963
|
+
* and the docs mark the alias.
|
|
25964
|
+
* @param spec - the primary command; `aliases` register with the shared
|
|
25965
|
+
* handler unless `aliasHandlers` overrides one.
|
|
25966
|
+
*/
|
|
25967
|
+
const registerTuiCommand = (spec) => {
|
|
25968
|
+
commands.register({
|
|
25969
|
+
name: spec.name,
|
|
25970
|
+
description: spec.description,
|
|
25971
|
+
...spec.input === void 0 ? {} : { input: spec.input },
|
|
25972
|
+
handler: spec.handler
|
|
25973
|
+
});
|
|
25974
|
+
for (const alias of spec.aliases ?? []) {
|
|
25975
|
+
const handler = spec.aliasHandlers?.[alias] ?? spec.handler;
|
|
25976
|
+
commands.register({
|
|
25977
|
+
name: alias,
|
|
25978
|
+
description: `${spec.description} (alias of /${spec.name})`,
|
|
25979
|
+
...spec.input === void 0 ? {} : { input: spec.input },
|
|
25980
|
+
handler
|
|
25981
|
+
});
|
|
25982
|
+
}
|
|
25983
|
+
};
|
|
25255
25984
|
const exitHandler = () => {
|
|
25256
25985
|
runner.requestExit();
|
|
25257
25986
|
return { kind: "success" };
|
|
25258
25987
|
};
|
|
25259
|
-
|
|
25988
|
+
registerTuiCommand({
|
|
25260
25989
|
name: "exit",
|
|
25261
25990
|
description: "Quit the terminal UI (flush and exit)",
|
|
25262
|
-
|
|
25263
|
-
});
|
|
25264
|
-
commands.register({
|
|
25265
|
-
name: "quit",
|
|
25266
|
-
description: "Quit the terminal UI (flush and exit) — alias of /exit",
|
|
25991
|
+
aliases: ["quit"],
|
|
25267
25992
|
handler: exitHandler
|
|
25268
25993
|
});
|
|
25269
25994
|
commands.register({
|
|
@@ -25337,6 +26062,13 @@ function registerTuiCommands(runner, initial) {
|
|
|
25337
26062
|
currentValue: tuiSettings?.get().busyEnter ?? "queue",
|
|
25338
26063
|
values: ["queue", "steer"]
|
|
25339
26064
|
},
|
|
26065
|
+
{
|
|
26066
|
+
id: "local-shell-sandbox",
|
|
26067
|
+
label: "Local shell sandbox",
|
|
26068
|
+
description: "! / !! commands run outside the dsh sandbox (bypass, default) or under the sandbox policy",
|
|
26069
|
+
currentValue: tuiSettings?.get().localShellSandbox ?? "bypass",
|
|
26070
|
+
values: ["bypass", "sandbox"]
|
|
26071
|
+
},
|
|
25340
26072
|
{
|
|
25341
26073
|
id: "fullscreen",
|
|
25342
26074
|
label: "Fullscreen",
|
|
@@ -25464,6 +26196,14 @@ function registerTuiCommands(runner, initial) {
|
|
|
25464
26196
|
busyEnter: value
|
|
25465
26197
|
}), { notify: true });
|
|
25466
26198
|
}
|
|
26199
|
+
} else if (id === "local-shell-sandbox") {
|
|
26200
|
+
if (value === "bypass" || value === "sandbox") {
|
|
26201
|
+
const settings = tuiSettings;
|
|
26202
|
+
if (settings !== void 0) detach("settings local shell sandbox write", () => settings.replace({
|
|
26203
|
+
...settings.get(),
|
|
26204
|
+
localShellSandbox: value
|
|
26205
|
+
}), { notify: true });
|
|
26206
|
+
}
|
|
25467
26207
|
} else if (id === "fullscreen") {
|
|
25468
26208
|
if (value === "off" || value === "on") app.setFullscreen(value === "on");
|
|
25469
26209
|
}
|
|
@@ -25538,17 +26278,13 @@ function registerTuiCommands(runner, initial) {
|
|
|
25538
26278
|
});
|
|
25539
26279
|
return { kind: "success" };
|
|
25540
26280
|
};
|
|
25541
|
-
|
|
26281
|
+
registerTuiCommand({
|
|
25542
26282
|
name: "sessions",
|
|
25543
26283
|
description: "List, search, and switch persisted sessions",
|
|
25544
26284
|
input: { hint: "[query]" },
|
|
25545
|
-
handler: (invocation) => openSessionPicker(invocation, "sessions")
|
|
25546
|
-
|
|
25547
|
-
|
|
25548
|
-
name: "resume",
|
|
25549
|
-
description: "Resume a session by id or pick from the list (alias of /sessions)",
|
|
25550
|
-
input: { hint: "[id|query]" },
|
|
25551
|
-
handler: async (invocation) => {
|
|
26285
|
+
handler: (invocation) => openSessionPicker(invocation, "sessions"),
|
|
26286
|
+
aliases: ["resume"],
|
|
26287
|
+
aliasHandlers: { resume: async (invocation) => {
|
|
25552
26288
|
const raw = invocation.rawInput.trim();
|
|
25553
26289
|
if (raw !== "") {
|
|
25554
26290
|
const currentId = runner.liveAgent?.session.id;
|
|
@@ -25571,7 +26307,7 @@ function registerTuiCommands(runner, initial) {
|
|
|
25571
26307
|
}
|
|
25572
26308
|
}
|
|
25573
26309
|
return openSessionPicker(invocation, "resume");
|
|
25574
|
-
}
|
|
26310
|
+
} }
|
|
25575
26311
|
});
|
|
25576
26312
|
/** The workspace the live session runs in; fallback to the TUI's cwd. */
|
|
25577
26313
|
const sessionCwd = (agent) => agent.session.header.cwd ?? cwd;
|
|
@@ -25957,129 +26693,13 @@ function registerTuiCommands(runner, initial) {
|
|
|
25957
26693
|
};
|
|
25958
26694
|
}
|
|
25959
26695
|
});
|
|
25960
|
-
|
|
25961
|
-
name: "queue",
|
|
25962
|
-
description: "Manage queued input: edit, delete, steer, or insert",
|
|
25963
|
-
handler: async () => {
|
|
25964
|
-
const liveAgent = await requireAgent();
|
|
25965
|
-
const inbox = liveAgent.inbox;
|
|
25966
|
-
const queued = [...inbox.nextTurn.map((message, index) => ({
|
|
25967
|
-
message,
|
|
25968
|
-
slot: `next ${index + 1}`,
|
|
25969
|
-
list: "next-turn"
|
|
25970
|
-
})), ...inbox.nextStep.map((message, index) => ({
|
|
25971
|
-
message,
|
|
25972
|
-
slot: `steer ${index + 1}`,
|
|
25973
|
-
list: "next-step"
|
|
25974
|
-
}))];
|
|
25975
|
-
if (queued.length === 0) return {
|
|
25976
|
-
kind: "success",
|
|
25977
|
-
text: "no queued input"
|
|
25978
|
-
};
|
|
25979
|
-
app.openSettings(queued.map(({ message, slot }) => ({
|
|
25980
|
-
id: message.id,
|
|
25981
|
-
label: `[${slot}] ${textOf(message.content).replace(/\s+/g, " ").trim().slice(0, 40)}`,
|
|
25982
|
-
description: message.id,
|
|
25983
|
-
currentValue: "",
|
|
25984
|
-
submenu: (value, done) => new SettingsList([
|
|
25985
|
-
{
|
|
25986
|
-
id: "edit",
|
|
25987
|
-
label: "Edit",
|
|
25988
|
-
description: "Rewrite this queued message",
|
|
25989
|
-
currentValue: "",
|
|
25990
|
-
values: ["✓"]
|
|
25991
|
-
},
|
|
25992
|
-
{
|
|
25993
|
-
id: "delete",
|
|
25994
|
-
label: "Delete",
|
|
25995
|
-
description: "Remove it from the queue",
|
|
25996
|
-
currentValue: "",
|
|
25997
|
-
values: ["✓"]
|
|
25998
|
-
},
|
|
25999
|
-
{
|
|
26000
|
-
id: "steer",
|
|
26001
|
-
label: "Steer",
|
|
26002
|
-
description: "Send it now: into the running turn, or start one when idle",
|
|
26003
|
-
currentValue: "",
|
|
26004
|
-
values: ["✓"]
|
|
26005
|
-
},
|
|
26006
|
-
{
|
|
26007
|
-
id: "insert",
|
|
26008
|
-
label: "Insert before",
|
|
26009
|
-
description: "Queue a new message ahead of this one",
|
|
26010
|
-
currentValue: "",
|
|
26011
|
-
values: ["✓"]
|
|
26012
|
-
}
|
|
26013
|
-
], 6, settingsListTheme(), (action) => done(action), () => done(), {})
|
|
26014
|
-
})), (id, action) => {
|
|
26015
|
-
const target = queued.find((item) => item.message.id === id);
|
|
26016
|
-
if (target === void 0) return;
|
|
26017
|
-
const message = target.message;
|
|
26018
|
-
if (action === "delete") {
|
|
26019
|
-
inbox.remove(message.id);
|
|
26020
|
-
app.notify("queued message deleted", "info");
|
|
26021
|
-
} else if (action === "steer") {
|
|
26022
|
-
inbox.remove(message.id);
|
|
26023
|
-
if (liveAgent.status === "running") liveAgent.steer(message);
|
|
26024
|
-
else liveAgent.followup(message);
|
|
26025
|
-
app.notify("steering queued message", "info");
|
|
26026
|
-
} else if (action === "edit" || action === "insert") runOwned("queued message edit", () => app.askQuestions([{
|
|
26027
|
-
id: "q",
|
|
26028
|
-
question: action === "edit" ? "Edit queued message:" : "New message to insert:"
|
|
26029
|
-
}]), {
|
|
26030
|
-
diag: runner.diag,
|
|
26031
|
-
sessionId: () => runner.liveAgent?.session.id,
|
|
26032
|
-
onResult: (answers) => {
|
|
26033
|
-
const text = answers[0]?.custom?.trim() ?? "";
|
|
26034
|
-
if (text === "") return;
|
|
26035
|
-
const next = createUserMessage({
|
|
26036
|
-
content: [{
|
|
26037
|
-
type: "text",
|
|
26038
|
-
text
|
|
26039
|
-
}],
|
|
26040
|
-
source: { kind: "user" }
|
|
26041
|
-
});
|
|
26042
|
-
if (action === "edit") {
|
|
26043
|
-
inbox.replace(message.id, next);
|
|
26044
|
-
app.notify("queued message updated", "info");
|
|
26045
|
-
} else {
|
|
26046
|
-
const position = (target.list === "next-turn" ? inbox.nextTurn : inbox.nextStep).findIndex((item) => item.id === message.id);
|
|
26047
|
-
if (position < 0) return;
|
|
26048
|
-
inbox.splice(target.list, position, 0, [next]);
|
|
26049
|
-
app.notify("message inserted before the selected one", "info");
|
|
26050
|
-
}
|
|
26051
|
-
}
|
|
26052
|
-
});
|
|
26053
|
-
}, () => {});
|
|
26054
|
-
return { kind: "success" };
|
|
26055
|
-
}
|
|
26056
|
-
});
|
|
26057
|
-
commands.register({
|
|
26696
|
+
registerTuiCommand({
|
|
26058
26697
|
name: "tasks",
|
|
26059
|
-
description: "
|
|
26698
|
+
description: "Browse background jobs and subagents for this session (search filters rows)",
|
|
26699
|
+
aliases: ["subagents"],
|
|
26060
26700
|
handler: async () => {
|
|
26061
|
-
|
|
26062
|
-
|
|
26063
|
-
if (jobs === void 0) return {
|
|
26064
|
-
kind: "error",
|
|
26065
|
-
text: "jobs service unavailable"
|
|
26066
|
-
};
|
|
26067
|
-
const snapshots = jobs.list(liveAgent);
|
|
26068
|
-
if (snapshots.length === 0) return {
|
|
26069
|
-
kind: "error",
|
|
26070
|
-
text: "no background jobs"
|
|
26071
|
-
};
|
|
26072
|
-
app.openTaskBrowser(snapshots.map((job) => ({
|
|
26073
|
-
value: job.id,
|
|
26074
|
-
label: `${job.kind} · ${job.label}`,
|
|
26075
|
-
status: job.status,
|
|
26076
|
-
detail: job.detail,
|
|
26077
|
-
startedAt: job.startedAt,
|
|
26078
|
-
group: "jobs"
|
|
26079
|
-
})), (jobId) => runner.openJobView(jobId), () => {}, {
|
|
26080
|
-
header: "tasks",
|
|
26081
|
-
enableSearch: true
|
|
26082
|
-
});
|
|
26701
|
+
await requireAgent();
|
|
26702
|
+
runner.openTasksBrowser();
|
|
26083
26703
|
return { kind: "success" };
|
|
26084
26704
|
}
|
|
26085
26705
|
});
|
|
@@ -26271,56 +26891,6 @@ function registerTuiCommands(runner, initial) {
|
|
|
26271
26891
|
return { kind: "success" };
|
|
26272
26892
|
}
|
|
26273
26893
|
});
|
|
26274
|
-
commands.register({
|
|
26275
|
-
name: "subagents",
|
|
26276
|
-
description: "List child agents; view a transcript or interrupt one",
|
|
26277
|
-
handler: async () => {
|
|
26278
|
-
const liveAgent = await requireAgent();
|
|
26279
|
-
const subagents = ctx.get("subagents");
|
|
26280
|
-
if (subagents === void 0) return {
|
|
26281
|
-
kind: "error",
|
|
26282
|
-
text: "subagent service unavailable"
|
|
26283
|
-
};
|
|
26284
|
-
const children = (await subagents.listChildren(liveAgent.session.id)).filter((child) => child.kind === "child");
|
|
26285
|
-
if (children.length === 0) return {
|
|
26286
|
-
kind: "success",
|
|
26287
|
-
text: "no subagents for this session"
|
|
26288
|
-
};
|
|
26289
|
-
const labelOf = (child) => child.label ?? child.id;
|
|
26290
|
-
const closeSubagents = app.openSettings(children.map((child) => ({
|
|
26291
|
-
id: child.id,
|
|
26292
|
-
label: labelOf(child),
|
|
26293
|
-
description: `${child.mode} · ${child.activity}${child.hasChildren ? " · has children" : ""}`,
|
|
26294
|
-
currentValue: "",
|
|
26295
|
-
submenu: (value, done) => new SettingsList([{
|
|
26296
|
-
id: "view",
|
|
26297
|
-
label: "View transcript",
|
|
26298
|
-
description: "Watch this session read-only (Esc to return)",
|
|
26299
|
-
currentValue: "",
|
|
26300
|
-
values: ["✓"]
|
|
26301
|
-
}, {
|
|
26302
|
-
id: "interrupt",
|
|
26303
|
-
label: "Interrupt",
|
|
26304
|
-
description: "Cancel the child agent",
|
|
26305
|
-
currentValue: "",
|
|
26306
|
-
values: ["✓"]
|
|
26307
|
-
}], 6, settingsListTheme(), (id) => done(id), () => done(), {})
|
|
26308
|
-
})), (childId, action) => {
|
|
26309
|
-
const child = children.find((candidate) => candidate.id === childId);
|
|
26310
|
-
if (child === void 0) return;
|
|
26311
|
-
closeSubagents();
|
|
26312
|
-
if (action === "view") detach("subagent view", () => runner.enterView(child.id, labelOf(child)), { notify: true });
|
|
26313
|
-
else if (action === "interrupt") {
|
|
26314
|
-
subagents.interrupt(child.id, {
|
|
26315
|
-
kind: "user",
|
|
26316
|
-
parentSessionId: liveAgent.session.id
|
|
26317
|
-
});
|
|
26318
|
-
app.notify(`interrupting ${labelOf(child)}`, "info");
|
|
26319
|
-
}
|
|
26320
|
-
}, () => {});
|
|
26321
|
-
return { kind: "success" };
|
|
26322
|
-
}
|
|
26323
|
-
});
|
|
26324
26894
|
commands.register({
|
|
26325
26895
|
name: "search",
|
|
26326
26896
|
description: "Search persisted sessions for text and switch to a hit",
|
|
@@ -26412,16 +26982,11 @@ function registerTuiCommands(runner, initial) {
|
|
|
26412
26982
|
};
|
|
26413
26983
|
}
|
|
26414
26984
|
};
|
|
26415
|
-
|
|
26985
|
+
registerTuiCommand({
|
|
26416
26986
|
name: "title",
|
|
26417
26987
|
description: "Set the session title; without an argument, regenerate it from the conversation (overwrites the current title)",
|
|
26418
26988
|
input: { hint: "<title>" },
|
|
26419
|
-
|
|
26420
|
-
});
|
|
26421
|
-
commands.register({
|
|
26422
|
-
name: "rename",
|
|
26423
|
-
description: "Alias of /title: set the session title, or regenerate it without an argument (overwrites the current title)",
|
|
26424
|
-
input: { hint: "<title>" },
|
|
26989
|
+
aliases: ["rename"],
|
|
26425
26990
|
handler: titleHandler
|
|
26426
26991
|
});
|
|
26427
26992
|
commands.register({
|
|
@@ -27460,6 +28025,17 @@ function shellCommandOf(text) {
|
|
|
27460
28025
|
return text.replace(/^!+/, "").trim();
|
|
27461
28026
|
}
|
|
27462
28027
|
/**
|
|
28028
|
+
* The local-shell sandbox preference for user-typed `!`/`!!` commands:
|
|
28029
|
+
* 'sandbox' routes them through the dsh shell capability's policy,
|
|
28030
|
+
* anything else (including an absent settings document) is 'bypass' — the
|
|
28031
|
+
* pi/kimi default that runs the user's own commands outside the sandbox
|
|
28032
|
+
* (the sandbox guards the model's autonomous commands, not the user's).
|
|
28033
|
+
* @param settings - the TUI settings document's fields, when present.
|
|
28034
|
+
*/
|
|
28035
|
+
function localShellSandboxPreferenceOf(settings) {
|
|
28036
|
+
return settings?.localShellSandbox === "sandbox" ? "sandbox" : "bypass";
|
|
28037
|
+
}
|
|
28038
|
+
/**
|
|
27463
28039
|
* The model-facing submission text: the command echoed `$`-style (kimi
|
|
27464
28040
|
* ShellExecution parity) followed by the settled card result (output +
|
|
27465
28041
|
* `[exit N]` / truncation lines).
|
|
@@ -27678,82 +28254,6 @@ function formatBytes(bytes) {
|
|
|
27678
28254
|
return `${(bytes / 1048576).toFixed(1)} MiB`;
|
|
27679
28255
|
}
|
|
27680
28256
|
//#endregion
|
|
27681
|
-
//#region src/shell-words.ts
|
|
27682
|
-
/**
|
|
27683
|
-
* A small, reliable POSIX-ish shell word parser for external-command
|
|
27684
|
-
* strings like `$VISUAL`/`$EDITOR` (`code --wait`, `vim -f`, `sensible-editor
|
|
27685
|
-
* --nofork`) and `!` shell lines. It handles the quoting rules that matter
|
|
27686
|
-
* in practice — single quotes (literal), double quotes (backslash escapes,
|
|
27687
|
-
* everything else verbatim — no expansion), and backslash escapes outside
|
|
27688
|
-
* quotes — WITHOUT executing a shell.
|
|
27689
|
-
*
|
|
27690
|
-
* This deliberately replaces naive `command.split(/\s+/)` splitting: an
|
|
27691
|
-
* editor path with spaces or a quoted argument would be mangled into
|
|
27692
|
-
* phantom arguments.
|
|
27693
|
-
* @module @xmoon76/dsh-pi-tui/shell-words
|
|
27694
|
-
*/
|
|
27695
|
-
/**
|
|
27696
|
-
* Split a command line into words, POSIX-ish. Never throws: unbalanced
|
|
27697
|
-
* quotes are treated as a trailing literal word (matching how shells
|
|
27698
|
-
* consume the rest of the line). Backslashes before non-special characters
|
|
27699
|
-
* are dropped outside quotes; inside double quotes a backslash only escapes
|
|
27700
|
-
* `"`, `\`, and `$` and is kept verbatim before anything else.
|
|
27701
|
-
* @param line - the raw command line.
|
|
27702
|
-
* @returns the parsed words.
|
|
27703
|
-
*/
|
|
27704
|
-
function parseShellWords(line) {
|
|
27705
|
-
const words = [];
|
|
27706
|
-
let current = "";
|
|
27707
|
-
let inWord = false;
|
|
27708
|
-
let quote;
|
|
27709
|
-
for (let index = 0; index < line.length; index += 1) {
|
|
27710
|
-
const char = line[index];
|
|
27711
|
-
if (quote === void 0 && char === "\\") {
|
|
27712
|
-
const next = line[index + 1];
|
|
27713
|
-
if (next !== void 0) {
|
|
27714
|
-
current += next;
|
|
27715
|
-
index += 1;
|
|
27716
|
-
} else current += "\\";
|
|
27717
|
-
inWord = true;
|
|
27718
|
-
continue;
|
|
27719
|
-
}
|
|
27720
|
-
if (quote === void 0 && (char === "'" || char === "\"")) {
|
|
27721
|
-
quote = char;
|
|
27722
|
-
inWord = true;
|
|
27723
|
-
continue;
|
|
27724
|
-
}
|
|
27725
|
-
if (quote === "\"" && char === "\\") {
|
|
27726
|
-
const next = line[index + 1];
|
|
27727
|
-
if (next === "\"" || next === "\\" || next === "$") {
|
|
27728
|
-
current += next;
|
|
27729
|
-
index += 1;
|
|
27730
|
-
} else current += "\\";
|
|
27731
|
-
inWord = true;
|
|
27732
|
-
continue;
|
|
27733
|
-
}
|
|
27734
|
-
if (quote === "'" && char === "'") {
|
|
27735
|
-
quote = void 0;
|
|
27736
|
-
continue;
|
|
27737
|
-
}
|
|
27738
|
-
if (quote === "\"" && char === "\"") {
|
|
27739
|
-
quote = void 0;
|
|
27740
|
-
continue;
|
|
27741
|
-
}
|
|
27742
|
-
if (quote === void 0 && (char === " " || char === " " || char === "\n")) {
|
|
27743
|
-
if (inWord) {
|
|
27744
|
-
words.push(current);
|
|
27745
|
-
current = "";
|
|
27746
|
-
inWord = false;
|
|
27747
|
-
}
|
|
27748
|
-
continue;
|
|
27749
|
-
}
|
|
27750
|
-
current += char;
|
|
27751
|
-
inWord = true;
|
|
27752
|
-
}
|
|
27753
|
-
if (inWord) words.push(current);
|
|
27754
|
-
return words;
|
|
27755
|
-
}
|
|
27756
|
-
//#endregion
|
|
27757
28257
|
//#region src/skill-catalog-refresh.ts
|
|
27758
28258
|
/** Whether two targets are the same owner (no transition needed). The
|
|
27759
28259
|
* preset identity is the caller-provided presetId compared EXACTLY —
|
|
@@ -28294,7 +28794,6 @@ const LOCAL_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
28294
28794
|
"model",
|
|
28295
28795
|
"new",
|
|
28296
28796
|
"preset",
|
|
28297
|
-
"queue",
|
|
28298
28797
|
"quit",
|
|
28299
28798
|
"reload",
|
|
28300
28799
|
"rename",
|
|
@@ -28431,15 +28930,30 @@ function subagentJobTranscriptId(snapshot) {
|
|
|
28431
28930
|
/** Viewer body for a subagent job with no uniquely matched child. */
|
|
28432
28931
|
function subagentJobViewHint(status, detail) {
|
|
28433
28932
|
return [
|
|
28434
|
-
`status: ${status}${status === "running" || status === "stopping" ? " — running in the background; its transcript updates live in /
|
|
28933
|
+
`status: ${status}${status === "running" || status === "stopping" ? " — running in the background; its transcript updates live in /tasks" : ` — this subagent finished${detail === void 0 ? "" : ` (${detail})`}`}`,
|
|
28435
28934
|
"",
|
|
28436
28935
|
"The job record does not carry the child session id, so this job cannot",
|
|
28437
28936
|
"be matched to its child from the task browser (a same-label foreground",
|
|
28438
|
-
"run would be indistinguishable). Open /
|
|
28937
|
+
"run would be indistinguishable). Open /tasks and pick the child by",
|
|
28439
28938
|
"its label to read the transcript."
|
|
28440
28939
|
].join("\n");
|
|
28441
28940
|
}
|
|
28442
28941
|
/**
|
|
28942
|
+
* Whether an inbox message is USER-ORIGIN input — the queue pane's steerable
|
|
28943
|
+
* `❯` rows. Everything else is injected context, not the user's own queued
|
|
28944
|
+
* input, and must never read as one: plugin notices (background-job
|
|
28945
|
+
* completions), `subagent-report` relays (a child's active report, e.g.
|
|
28946
|
+
* "Background subagent X reported:"), injected skill/agent instructions,
|
|
28947
|
+
* goal messages. The web makes the same cut (`placement: source.kind ===
|
|
28948
|
+
* 'user' ? 'steering' : 'context'`), and this deployment's queue pane has
|
|
28949
|
+
* the same rule: only user-origin rows are steerable. A sourceless row
|
|
28950
|
+
* (undefined) is treated as user input — plain rows never carry a source.
|
|
28951
|
+
* @param source - the message source projection, or undefined for a plain row.
|
|
28952
|
+
*/
|
|
28953
|
+
function isUserQueueInput(source) {
|
|
28954
|
+
return source === void 0 || source.kind === "user";
|
|
28955
|
+
}
|
|
28956
|
+
/**
|
|
28443
28957
|
* Whether a plain submitted draft is the quit word: exactly `exit` (trimmed,
|
|
28444
28958
|
* lowercase). The runner intercepts this BEFORE any session creation or
|
|
28445
28959
|
* submission (shell muscle memory); anything else — `exit!`, `Exit`, or a
|
|
@@ -28511,7 +29025,7 @@ function foldQueueRows(messages, mode, notified) {
|
|
|
28511
29025
|
id: message.id,
|
|
28512
29026
|
text: textOf(message.content),
|
|
28513
29027
|
mode,
|
|
28514
|
-
notice: source
|
|
29028
|
+
notice: !isUserQueueInput(source)
|
|
28515
29029
|
});
|
|
28516
29030
|
}
|
|
28517
29031
|
return {
|
|
@@ -29292,9 +29806,11 @@ function apply(ctx, config) {
|
|
|
29292
29806
|
/**
|
|
29293
29807
|
* The LIVE session's workspace: each session carries its own header cwd
|
|
29294
29808
|
* (fixed at creation, e.g. a session birthed by the web in another
|
|
29295
|
-
* directory). The footer/welcome/completions
|
|
29296
|
-
* session switch
|
|
29297
|
-
*
|
|
29809
|
+
* directory). The footer/welcome/completions AND `!`/`!!` shell runs
|
|
29810
|
+
* follow THIS cwd, so a session switch moves the whole surface with the
|
|
29811
|
+
* session (pi parity: `executeBash` runs in the session cwd) and a
|
|
29812
|
+
* shell command executes where the completions suggest files; `cwd`
|
|
29813
|
+
* (the process cwd) stays for launch-relative concerns (/export paths).
|
|
29298
29814
|
*/
|
|
29299
29815
|
const sessionCwd = () => liveAgent?.session.header.cwd ?? cwd;
|
|
29300
29816
|
/** The footer model label: the live selection (with effort) when one exists. */
|
|
@@ -29466,11 +29982,13 @@ function apply(ctx, config) {
|
|
|
29466
29982
|
});
|
|
29467
29983
|
if (includeInContext && !localSignal.aborted) submitResult(result);
|
|
29468
29984
|
};
|
|
29469
|
-
const
|
|
29985
|
+
const sandboxPreference = localShellSandboxPreferenceOf(tuiSettings?.get());
|
|
29986
|
+
const shell = sandboxPreference === "sandbox" ? ctx.get("shell") : void 0;
|
|
29987
|
+
if (shell === void 0 && sandboxPreference === "sandbox") app.notify("local shell sandbox unavailable in this composition — running unsandboxed", "error");
|
|
29470
29988
|
if (shell !== void 0) {
|
|
29471
29989
|
const spec = shell.resolve({
|
|
29472
29990
|
command,
|
|
29473
|
-
workdir:
|
|
29991
|
+
workdir: sessionCwd(),
|
|
29474
29992
|
signal: localSignal
|
|
29475
29993
|
});
|
|
29476
29994
|
runOwned("local shell", () => shell.run(spec), {
|
|
@@ -29500,7 +30018,7 @@ function apply(ctx, config) {
|
|
|
29500
30018
|
return;
|
|
29501
30019
|
}
|
|
29502
30020
|
const child = spawn(command, {
|
|
29503
|
-
cwd,
|
|
30021
|
+
cwd: sessionCwd(),
|
|
29504
30022
|
stdio: [
|
|
29505
30023
|
"ignore",
|
|
29506
30024
|
"pipe",
|
|
@@ -30063,8 +30581,7 @@ function apply(ctx, config) {
|
|
|
30063
30581
|
},
|
|
30064
30582
|
onDequeue: () => {
|
|
30065
30583
|
if (liveAgent === void 0) return;
|
|
30066
|
-
const
|
|
30067
|
-
const queued = [...liveAgent.inbox.nextTurn, ...liveAgent.inbox.nextStep].filter((message) => !isNotice(message));
|
|
30584
|
+
const queued = [...liveAgent.inbox.nextTurn, ...liveAgent.inbox.nextStep].filter((message) => isUserQueueInput(message.source));
|
|
30068
30585
|
if (queued.length === 0) return;
|
|
30069
30586
|
for (const message of queued) liveAgent.inbox.remove(message.id);
|
|
30070
30587
|
const queuedText = queued.map((message) => textOf(message.content)).join("\n\n");
|
|
@@ -30072,58 +30589,7 @@ function apply(ctx, config) {
|
|
|
30072
30589
|
app.setDraft([queuedText, current].filter((part) => part.trim() !== "").join("\n\n"));
|
|
30073
30590
|
refreshQueue();
|
|
30074
30591
|
},
|
|
30075
|
-
onOpenTasks: () =>
|
|
30076
|
-
if (liveAgent === void 0) return;
|
|
30077
|
-
let jobSnapshots = [];
|
|
30078
|
-
if (jobs !== void 0) try {
|
|
30079
|
-
jobSnapshots = jobs.list(liveAgent);
|
|
30080
|
-
} catch {}
|
|
30081
|
-
let rows = buildTaskRows(jobSnapshots, []);
|
|
30082
|
-
const selectRow = (value) => {
|
|
30083
|
-
const row = rows.find((candidate) => candidate.value === value);
|
|
30084
|
-
if (row === void 0) return;
|
|
30085
|
-
if (row.kind === "subagent") {
|
|
30086
|
-
runOwned("subagent view from tasks", () => enterView(row.childId, row.label), {
|
|
30087
|
-
diag,
|
|
30088
|
-
sessionId: () => liveAgent?.session.id,
|
|
30089
|
-
onError: (error) => app.notify(`could not open the subagent view: ${safeErrorMessage(error)}`, "error")
|
|
30090
|
-
});
|
|
30091
|
-
return;
|
|
30092
|
-
}
|
|
30093
|
-
openJobView(row.jobId);
|
|
30094
|
-
};
|
|
30095
|
-
const taskPanelItems = (target) => target.map((row) => row.kind === "job" ? {
|
|
30096
|
-
value: row.value,
|
|
30097
|
-
label: taskRowLabel(row),
|
|
30098
|
-
status: row.status,
|
|
30099
|
-
detail: row.detail,
|
|
30100
|
-
startedAt: row.startedAt,
|
|
30101
|
-
group: rowGroup(row)
|
|
30102
|
-
} : {
|
|
30103
|
-
value: row.value,
|
|
30104
|
-
label: taskRowLabel(row),
|
|
30105
|
-
status: row.activity,
|
|
30106
|
-
detail: row.hasChildren ? "has children" : void 0,
|
|
30107
|
-
group: rowGroup(row)
|
|
30108
|
-
});
|
|
30109
|
-
const handle = app.openTaskBrowser(taskPanelItems(rows), selectRow, () => {}, {
|
|
30110
|
-
header: "tasks · subagents",
|
|
30111
|
-
enableSearch: true
|
|
30112
|
-
});
|
|
30113
|
-
if (subagents !== void 0) {
|
|
30114
|
-
const sessionId = liveAgent.session.id;
|
|
30115
|
-
const generation = sessionGeneration;
|
|
30116
|
-
runOwned("task browser children", () => subagents.listChildren(sessionId), {
|
|
30117
|
-
diag,
|
|
30118
|
-
sessionId: () => liveAgent?.session.id,
|
|
30119
|
-
onResult: (entries) => {
|
|
30120
|
-
if (sessionGeneration !== generation || liveAgent?.session.id !== sessionId) return;
|
|
30121
|
-
rows = buildTaskRows(jobSnapshots, entries);
|
|
30122
|
-
handle.setItems(taskPanelItems(rows));
|
|
30123
|
-
}
|
|
30124
|
-
});
|
|
30125
|
-
}
|
|
30126
|
-
}
|
|
30592
|
+
onOpenTasks: () => openTasksBrowser()
|
|
30127
30593
|
}, {
|
|
30128
30594
|
present,
|
|
30129
30595
|
workspaceRoot: cwd,
|
|
@@ -30142,6 +30608,73 @@ function apply(ctx, config) {
|
|
|
30142
30608
|
unstableInputsRevision: () => extensionService?._unstableInputsRevision() ?? 0,
|
|
30143
30609
|
unstableFailSafeRelease: () => extensionService?._unstableEmergencyRelease()
|
|
30144
30610
|
});
|
|
30611
|
+
const openTasksBrowser = () => {
|
|
30612
|
+
if (liveAgent === void 0) return;
|
|
30613
|
+
let jobSnapshots = [];
|
|
30614
|
+
if (jobs !== void 0) try {
|
|
30615
|
+
jobSnapshots = jobs.list(liveAgent);
|
|
30616
|
+
} catch {}
|
|
30617
|
+
let rows = buildTaskRows(jobSnapshots, []);
|
|
30618
|
+
const selectRow = (value) => {
|
|
30619
|
+
const row = rows.find((candidate) => candidate.value === value);
|
|
30620
|
+
if (row === void 0) return;
|
|
30621
|
+
if (row.kind === "subagent") {
|
|
30622
|
+
runOwned("subagent view from tasks", () => enterView(row.childId, row.label), {
|
|
30623
|
+
diag,
|
|
30624
|
+
sessionId: () => liveAgent?.session.id,
|
|
30625
|
+
onError: (error) => app.notify(`could not open the subagent view: ${safeErrorMessage(error)}`, "error")
|
|
30626
|
+
});
|
|
30627
|
+
return;
|
|
30628
|
+
}
|
|
30629
|
+
openJobView(row.jobId);
|
|
30630
|
+
};
|
|
30631
|
+
const actionRow = (value, action) => {
|
|
30632
|
+
const row = rows.find((candidate) => candidate.value === value);
|
|
30633
|
+
if (row === void 0 || row.kind !== "subagent") return;
|
|
30634
|
+
const service = ctx.get("subagents");
|
|
30635
|
+
if (service === void 0 || liveAgent === void 0) {
|
|
30636
|
+
app.notify("subagent service unavailable", "error");
|
|
30637
|
+
return;
|
|
30638
|
+
}
|
|
30639
|
+
service.interrupt(row.childId, {
|
|
30640
|
+
kind: "user",
|
|
30641
|
+
parentSessionId: liveAgent.session.id
|
|
30642
|
+
});
|
|
30643
|
+
app.notify(`interrupting ${row.label}`, "info");
|
|
30644
|
+
};
|
|
30645
|
+
const taskPanelItems = (target) => target.map((row) => row.kind === "job" ? {
|
|
30646
|
+
value: row.value,
|
|
30647
|
+
label: taskRowLabel(row),
|
|
30648
|
+
status: row.status,
|
|
30649
|
+
detail: row.detail,
|
|
30650
|
+
startedAt: row.startedAt,
|
|
30651
|
+
group: rowGroup(row)
|
|
30652
|
+
} : {
|
|
30653
|
+
value: row.value,
|
|
30654
|
+
label: taskRowLabel(row),
|
|
30655
|
+
status: row.activity,
|
|
30656
|
+
detail: row.hasChildren ? "has children" : void 0,
|
|
30657
|
+
group: rowGroup(row)
|
|
30658
|
+
});
|
|
30659
|
+
const handle = app.openTaskBrowser(taskPanelItems(rows), selectRow, () => {}, {
|
|
30660
|
+
header: "tasks · subagents",
|
|
30661
|
+
enableSearch: true,
|
|
30662
|
+
onAction: actionRow
|
|
30663
|
+
});
|
|
30664
|
+
if (subagents !== void 0) {
|
|
30665
|
+
const sessionId = liveAgent.session.id;
|
|
30666
|
+
const generation = sessionGeneration;
|
|
30667
|
+
runOwned("task browser children", () => subagents.listChildren(sessionId), {
|
|
30668
|
+
diag,
|
|
30669
|
+
sessionId: () => liveAgent?.session.id,
|
|
30670
|
+
onResult: (entries) => {
|
|
30671
|
+
if (sessionGeneration !== generation || liveAgent?.session.id !== sessionId) return;
|
|
30672
|
+
rows = buildTaskRows(jobSnapshots, entries);
|
|
30673
|
+
handle.setItems(taskPanelItems(rows));
|
|
30674
|
+
}
|
|
30675
|
+
});
|
|
30676
|
+
}
|
|
30677
|
+
};
|
|
30145
30678
|
if (extensionHost !== void 0 && extensionService !== void 0) {
|
|
30146
30679
|
app.setRendererErrorSink(({ id, error, slot }) => {
|
|
30147
30680
|
const message = safeErrorMessage(error).replace(/\s+/g, " ").slice(0, 200);
|
|
@@ -30186,12 +30719,14 @@ function apply(ctx, config) {
|
|
|
30186
30719
|
theme: z.string(),
|
|
30187
30720
|
footer: z.string(),
|
|
30188
30721
|
fullscreen: z.string(),
|
|
30189
|
-
busyEnter: z.string()
|
|
30722
|
+
busyEnter: z.string(),
|
|
30723
|
+
localShellSandbox: z.string()
|
|
30190
30724
|
}), { base: {
|
|
30191
30725
|
theme: "auto",
|
|
30192
30726
|
footer: "full",
|
|
30193
30727
|
fullscreen: "on",
|
|
30194
|
-
busyEnter: "queue"
|
|
30728
|
+
busyEnter: "queue",
|
|
30729
|
+
localShellSandbox: "bypass"
|
|
30195
30730
|
} });
|
|
30196
30731
|
if (tuiSettings?.get().fullscreen === "on") app.setFullscreen(true);
|
|
30197
30732
|
const storedTheme = tuiSettings?.get().theme;
|
|
@@ -30305,7 +30840,7 @@ function apply(ctx, config) {
|
|
|
30305
30840
|
* (never the output — the job's single read cursor belongs to the agent's
|
|
30306
30841
|
* job_output; consuming it from the UI would leave the model an
|
|
30307
30842
|
* incomplete result and could swallow the completion notice); a subagent
|
|
30308
|
-
* job shows the status viewer with a /
|
|
30843
|
+
* job shows the status viewer with a /tasks hint. The job record
|
|
30309
30844
|
* carries no child session id, so label/order/time heuristics cannot
|
|
30310
30845
|
* distinguish a background child from a same-label foreground one-shot;
|
|
30311
30846
|
* the task browser therefore never opens a transcript by guess.
|
|
@@ -30338,7 +30873,7 @@ function apply(ctx, config) {
|
|
|
30338
30873
|
};
|
|
30339
30874
|
/**
|
|
30340
30875
|
* Status-only viewer for one job (never touches the read cursor). The
|
|
30341
|
-
* subagent variant appends the /
|
|
30876
|
+
* subagent variant appends the /tasks hint because a transcript
|
|
30342
30877
|
* cannot always be matched; the bash variant is pure status.
|
|
30343
30878
|
*/
|
|
30344
30879
|
const openJobStatusViewer = (jobId, title, snapshot) => {
|
|
@@ -30608,6 +31143,7 @@ function apply(ctx, config) {
|
|
|
30608
31143
|
refreshStatus,
|
|
30609
31144
|
updateWelcomeCard,
|
|
30610
31145
|
openJobView,
|
|
31146
|
+
openTasksBrowser,
|
|
30611
31147
|
enterView,
|
|
30612
31148
|
requestExit,
|
|
30613
31149
|
exit
|
|
@@ -30848,4 +31384,4 @@ function apply(ctx, config) {
|
|
|
30848
31384
|
});
|
|
30849
31385
|
}
|
|
30850
31386
|
//#endregion
|
|
30851
|
-
export {
|
|
31387
|
+
export { apply$1 as A, runningProfile as C, subagentJobViewHint as D, subagentJobTranscriptId as E, describeKey as F, name$1 as M, PI_TUI_EXTENSIONS_SERVICE as N, subagentNoticeIsFailure as O, API_VERSION as P, resumeCommand as S, shouldSteerOnEnter as T, matchPendingSubagentCall as _, SessionLockRefusedError as a, recordedPreset as b, compactingFromLog as c, foldCompactionEvent as d, foldQueueRows as f, isUserQueueInput as g, isSubagentSettlementNotice as h, SESSIONLESS_COMMANDS as i, inject$1 as j, forkSeed as k, composeAgent as l, isPlainExitPrompt as m, HOST_COMMAND_CATALOG as n, apply as o, inject as p, LOCAL_COMMANDS as r, busyAfterTurnBoundary as s, Config as t, dangerCommand as u, name as v, shouldConsumeAdvertisedMiss as w, resolveInitialCatalog as x, recomposeBlank as y };
|