@xmoon76/dsh-pi-tui 0.2.1 → 0.3.0
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 +30 -20
- package/README.zh-CN.md +24 -16
- package/cordis.patch.yml +13 -1
- package/dist/builtins.mjs +4 -3
- package/dist/extensions.mjs +1 -1
- package/dist/index.d.mts +26 -1
- package/dist/index.mjs +2 -2
- package/dist/{src-C_RdKmcj.mjs → src-ClAKiW0z.mjs} +1446 -451
- package/package.json +62 -60
|
@@ -31,6 +31,8 @@ import { fileURLToPath } from "node:url";
|
|
|
31
31
|
import { EventEmitter } from "events";
|
|
32
32
|
import { Service } from "@deepseek-ai/cordis";
|
|
33
33
|
import { isUserInvocable, renderSkillContent } from "@deepseek-ai/dsh-skill";
|
|
34
|
+
import { AuthorizationDeclinedError } from "@deepseek-ai/dsh-authorization";
|
|
35
|
+
import { credentialKeyId, credentialKeyScope } from "@deepseek-ai/dsh-credentials";
|
|
34
36
|
//#region src/present.ts
|
|
35
37
|
/** Figma row titles per variant (design literals, not translatable copy). */
|
|
36
38
|
const VARIANT_TITLES = {
|
|
@@ -85,8 +87,121 @@ const TUI_TOOL_TITLES = {
|
|
|
85
87
|
"workflow-member": "Workflow Agent",
|
|
86
88
|
error: "Error",
|
|
87
89
|
interrupted: "Interrupted",
|
|
88
|
-
ask_user_question: "Question"
|
|
90
|
+
ask_user_question: "Question",
|
|
91
|
+
get_goal: "Read Goal",
|
|
92
|
+
create_goal: "Create Goal",
|
|
93
|
+
update_goal: "Update Goal"
|
|
89
94
|
};
|
|
95
|
+
/** The goal tool family (all three share the `{"goal":…}` result shape). */
|
|
96
|
+
const GOAL_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
97
|
+
"get_goal",
|
|
98
|
+
"create_goal",
|
|
99
|
+
"update_goal"
|
|
100
|
+
]);
|
|
101
|
+
/**
|
|
102
|
+
* Tools whose folded result preview must never leak the raw result JSON
|
|
103
|
+
* (web parity: the web's folded row shows only the args summary, never the
|
|
104
|
+
* result). The TUI keeps its result-preview row but shows a parsed summary
|
|
105
|
+
* instead — and nothing at all when the result cannot be parsed.
|
|
106
|
+
*
|
|
107
|
+
* `ralph`'s render already leads with a friendly line ("Ralph worker
|
|
108
|
+
* reported completion after N rounds.") before the JSON report, so its
|
|
109
|
+
* summary IS that first line. The agent-team family is deliberately NOT in
|
|
110
|
+
* the set: it is experimental (not in the production bundle) and two of its
|
|
111
|
+
* tools (`send_message`, `interrupt_agent`) share names with subagent tools
|
|
112
|
+
* whose render text is friendly — a name-keyed set cannot tell them apart.
|
|
113
|
+
*/
|
|
114
|
+
const FOLDED_JSON_RESULT_TOOLS = /* @__PURE__ */ new Set([
|
|
115
|
+
"ralph",
|
|
116
|
+
"schedule_create",
|
|
117
|
+
"schedule_list",
|
|
118
|
+
"schedule_delete",
|
|
119
|
+
"cordis_inspect_list",
|
|
120
|
+
"cordis_inspect_query",
|
|
121
|
+
"cordis_inspect_self"
|
|
122
|
+
]);
|
|
123
|
+
/** Parse a result text as JSON, tolerating a friendly prefix line before
|
|
124
|
+
* the JSON document (e.g. ralph's "…\nFinal report:\n{…}"). */
|
|
125
|
+
function parseJsonValue(text) {
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(text);
|
|
128
|
+
} catch {}
|
|
129
|
+
const brace = text.indexOf("{");
|
|
130
|
+
const bracket = text.indexOf("[");
|
|
131
|
+
const start = bracket !== -1 && (brace === -1 || bracket < brace) ? bracket : brace;
|
|
132
|
+
if (start > 0) try {
|
|
133
|
+
return JSON.parse(text.slice(start));
|
|
134
|
+
} catch {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** A non-empty string field, or undefined. */
|
|
139
|
+
function stringField(value) {
|
|
140
|
+
return typeof value === "string" && value !== "" ? value : void 0;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The folded preview summary for one of {@link FOLDED_JSON_RESULT_TOOLS}:
|
|
144
|
+
* a short human phrase derived from the parsed result JSON. Returns
|
|
145
|
+
* undefined when nothing useful can be derived — the caller then shows NO
|
|
146
|
+
* folded result preview (web parity), never the raw JSON.
|
|
147
|
+
* @param name - the tool name.
|
|
148
|
+
* @param text - the settled result text.
|
|
149
|
+
*/
|
|
150
|
+
function foldedResultSummaryFor(name, text) {
|
|
151
|
+
if (name === "ralph") {
|
|
152
|
+
const line = firstLine(text);
|
|
153
|
+
if (line !== "" && !line.startsWith("{")) return line;
|
|
154
|
+
const parsed = parseJsonValue(text);
|
|
155
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
156
|
+
const record = parsed;
|
|
157
|
+
const status = stringField(record.status);
|
|
158
|
+
const rounds = typeof record.roundsStarted === "number" ? String(record.roundsStarted) : void 0;
|
|
159
|
+
const parts = [status, rounds === void 0 ? void 0 : `${rounds} rounds`].filter((part) => part !== void 0);
|
|
160
|
+
return parts.length === 0 ? void 0 : parts.join(" · ");
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const parsed = parseJsonValue(text);
|
|
165
|
+
if (name === "schedule_list") {
|
|
166
|
+
if (Array.isArray(parsed)) return parsed.length === 0 ? "no scheduled jobs" : `${parsed.length} scheduled`;
|
|
167
|
+
if (typeof parsed === "object" && parsed !== null) return stringField(parsed.code);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (name === "schedule_create") {
|
|
171
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
172
|
+
const record = parsed;
|
|
173
|
+
const parts = [stringField(record.kind), stringField(record.state)].filter((part) => part !== void 0);
|
|
174
|
+
return parts.length === 0 ? void 0 : parts.join(" · ");
|
|
175
|
+
}
|
|
176
|
+
if (name === "schedule_delete") {
|
|
177
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
178
|
+
const record = parsed;
|
|
179
|
+
if (record.deleted === true) return "deleted";
|
|
180
|
+
if (record.deleted === false) return "not found";
|
|
181
|
+
return stringField(record.code);
|
|
182
|
+
}
|
|
183
|
+
if (name === "cordis_inspect_list") {
|
|
184
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
185
|
+
const providers = parsed.providers;
|
|
186
|
+
if (Array.isArray(providers)) return providers.length === 0 ? "no providers" : `${providers.length} providers`;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (name === "cordis_inspect_query") {
|
|
190
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
191
|
+
const record = parsed;
|
|
192
|
+
const parts = [
|
|
193
|
+
stringField(record.platform),
|
|
194
|
+
stringField(record.provider),
|
|
195
|
+
stringField(record.method)
|
|
196
|
+
].filter((part) => part !== void 0);
|
|
197
|
+
return parts.length === 0 ? void 0 : parts.join(" · ");
|
|
198
|
+
}
|
|
199
|
+
if (name === "cordis_inspect_self") {
|
|
200
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
201
|
+
const mode = stringField(parsed.mode);
|
|
202
|
+
return mode === void 0 ? void 0 : `mode ${mode}`;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
90
205
|
/** Summary key preference per variant (args-derived). */
|
|
91
206
|
const SUMMARY_KEYS = {
|
|
92
207
|
bash: ["description", "command"],
|
|
@@ -172,6 +287,15 @@ function summarizeToolArgs(name, argsRaw) {
|
|
|
172
287
|
if (typeof question !== "string" || question === "") return void 0;
|
|
173
288
|
return firstLine(question);
|
|
174
289
|
}
|
|
290
|
+
if (name === "create_goal") {
|
|
291
|
+
const objective = args.objective;
|
|
292
|
+
return typeof objective === "string" && objective !== "" ? firstLine(objective) : void 0;
|
|
293
|
+
}
|
|
294
|
+
if (name === "update_goal") {
|
|
295
|
+
const action = args.action;
|
|
296
|
+
return typeof action === "string" && action !== "" ? action : void 0;
|
|
297
|
+
}
|
|
298
|
+
if (name === "get_goal") return "";
|
|
175
299
|
if (name !== "todo_write") return void 0;
|
|
176
300
|
const todos = args.todos;
|
|
177
301
|
if (!Array.isArray(todos)) return void 0;
|
|
@@ -421,6 +545,36 @@ function resultTextLines(blocks, error) {
|
|
|
421
545
|
return lines;
|
|
422
546
|
}
|
|
423
547
|
/**
|
|
548
|
+
* Parse and normalize the tool's `{"answers":[…]}` render text. A
|
|
549
|
+
* malformed entry (non-object) invalidates the WHOLE text — the Web's
|
|
550
|
+
* `answeredSummary` checks `answers.every(isAnswer)` the same way, and a
|
|
551
|
+
* half-valid set would make the count and the rendered rows disagree.
|
|
552
|
+
* Non-string `selected` members are dropped; missing id/custom normalize
|
|
553
|
+
* to empty.
|
|
554
|
+
* @param text - the settled result text.
|
|
555
|
+
* @returns the normalized entries, or undefined when unparseable/malformed.
|
|
556
|
+
*/
|
|
557
|
+
function parseAnswerEntries(text) {
|
|
558
|
+
let parsed;
|
|
559
|
+
try {
|
|
560
|
+
parsed = JSON.parse(text);
|
|
561
|
+
} catch {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
565
|
+
const answers = parsed.answers;
|
|
566
|
+
if (!Array.isArray(answers)) return void 0;
|
|
567
|
+
if (!answers.every((entry) => typeof entry === "object" && entry !== null)) return void 0;
|
|
568
|
+
return answers.map((entry) => {
|
|
569
|
+
const candidate = entry;
|
|
570
|
+
return {
|
|
571
|
+
id: typeof candidate.id === "string" ? candidate.id : "",
|
|
572
|
+
selected: Array.isArray(candidate.selected) ? candidate.selected.filter((item) => typeof item === "string") : [],
|
|
573
|
+
custom: typeof candidate.custom === "string" ? candidate.custom : ""
|
|
574
|
+
};
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
424
578
|
* The answered-count summary for a settled `ask_user_question` result
|
|
425
579
|
* (Web AskQuestionRow parity): parses the tool's `{"answers":[…]}` render
|
|
426
580
|
* text and counts the entries that actually carry an answer — a non-empty
|
|
@@ -432,6 +586,43 @@ function resultTextLines(blocks, error) {
|
|
|
432
586
|
* @returns the answered-count summary, or undefined when unparseable.
|
|
433
587
|
*/
|
|
434
588
|
function askAnswersSummary(text) {
|
|
589
|
+
const entries = parseAnswerEntries(text);
|
|
590
|
+
if (entries === void 0) return void 0;
|
|
591
|
+
return `${entries.filter((entry) => entry.selected.length > 0 || entry.custom !== "").length}/${entries.length} answered`;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* The per-question display lines for a settled `ask_user_question` result
|
|
595
|
+
* (the expanded card's body, complementing {@link askAnswersSummary}'s
|
|
596
|
+
* count). Parses the tool's `{"answers":[…]}` render text and renders one
|
|
597
|
+
* line per entry: `● <id> → <selected, custom>` for an answered question,
|
|
598
|
+
* `○ <id> — skipped` when neither is present. Shares the normalization with
|
|
599
|
+
* {@link askAnswersSummary}, so the rendered rows and the count always
|
|
600
|
+
* agree; an unparseable/malformed text returns undefined.
|
|
601
|
+
* @param text - the settled result text.
|
|
602
|
+
* @returns the display lines, or undefined when unparseable/malformed.
|
|
603
|
+
*/
|
|
604
|
+
function askAnswersLines(text) {
|
|
605
|
+
const entries = parseAnswerEntries(text);
|
|
606
|
+
if (entries === void 0) return void 0;
|
|
607
|
+
return entries.map((entry) => {
|
|
608
|
+
if (entry.selected.length > 0) return {
|
|
609
|
+
text: `● ${entry.id} → ${entry.selected.join(", ")}`,
|
|
610
|
+
skipped: false
|
|
611
|
+
};
|
|
612
|
+
if (entry.custom !== "") return {
|
|
613
|
+
text: `● ${entry.id} → ${entry.custom}`,
|
|
614
|
+
skipped: false
|
|
615
|
+
};
|
|
616
|
+
return {
|
|
617
|
+
text: `○ ${entry.id} — skipped`,
|
|
618
|
+
skipped: true
|
|
619
|
+
};
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
/** Parse a settled goal-tool result text, or undefined when it is not the
|
|
623
|
+
* expected `{"goal":…}` JSON (the caller then falls back to the generic
|
|
624
|
+
* presentation). */
|
|
625
|
+
function parseGoalResult(text) {
|
|
435
626
|
let parsed;
|
|
436
627
|
try {
|
|
437
628
|
parsed = JSON.parse(text);
|
|
@@ -439,14 +630,72 @@ function askAnswersSummary(text) {
|
|
|
439
630
|
return;
|
|
440
631
|
}
|
|
441
632
|
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
442
|
-
const
|
|
443
|
-
if (
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
633
|
+
const value = parsed;
|
|
634
|
+
if (value.goal === null) return { goal: null };
|
|
635
|
+
if (typeof value.goal !== "object" || value.goal === null) return void 0;
|
|
636
|
+
const goal = value.goal;
|
|
637
|
+
const blocked = typeof goal.blockedReason === "object" && goal.blockedReason !== null ? goal.blockedReason : void 0;
|
|
638
|
+
return {
|
|
639
|
+
goal: {
|
|
640
|
+
...typeof goal.id === "string" ? { id: goal.id } : {},
|
|
641
|
+
...typeof goal.revision === "number" ? { revision: goal.revision } : {},
|
|
642
|
+
...typeof goal.objective === "string" ? { objective: goal.objective } : {},
|
|
643
|
+
...typeof goal.phase === "string" ? { phase: goal.phase } : {},
|
|
644
|
+
...typeof goal.roundsStarted === "number" ? { roundsStarted: goal.roundsStarted } : {},
|
|
645
|
+
...typeof goal.maxGoalRounds === "number" ? { maxGoalRounds: goal.maxGoalRounds } : {},
|
|
646
|
+
...blocked !== void 0 ? { blockedReason: {
|
|
647
|
+
...typeof blocked.code === "string" ? { code: blocked.code } : {},
|
|
648
|
+
...typeof blocked.message === "string" ? { message: blocked.message } : {}
|
|
649
|
+
} } : {}
|
|
650
|
+
},
|
|
651
|
+
...typeof value.activation === "string" ? { activation: value.activation } : {}
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* The one-line folded summary for a settled goal-tool result: `phase … ·
|
|
656
|
+
* revision N · N/M rounds`, or `no goal set` for `{"goal":null}`.
|
|
657
|
+
* Returns undefined when the text is not the expected result JSON (the
|
|
658
|
+
* caller then falls back to the generic preview instead of inventing one).
|
|
659
|
+
* @param text - the settled result text.
|
|
660
|
+
*/
|
|
661
|
+
function goalResultSummary(text) {
|
|
662
|
+
const value = parseGoalResult(text);
|
|
663
|
+
if (value === void 0) return void 0;
|
|
664
|
+
const goal = value.goal;
|
|
665
|
+
if (goal === null) return "no goal set";
|
|
666
|
+
const parts = [];
|
|
667
|
+
if (goal.phase !== void 0) parts.push(`phase ${goal.phase}`);
|
|
668
|
+
if (goal.revision !== void 0) parts.push(`revision ${goal.revision}`);
|
|
669
|
+
if (goal.roundsStarted !== void 0) parts.push(goal.maxGoalRounds === void 0 ? `${goal.roundsStarted} rounds` : `${goal.roundsStarted}/${goal.maxGoalRounds} rounds`);
|
|
670
|
+
return parts.length === 0 ? "goal" : parts.join(" · ");
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* The expanded-card display lines for a settled goal-tool result: one field
|
|
674
|
+
* per line (`● objective: …`, `● phase: … · revision N`, `● rounds: N/M`,
|
|
675
|
+
* `● activation: …`, `● blocked: code — message`), or a single `no goal
|
|
676
|
+
* set` line for `{"goal":null}`. Returns undefined when the text is not the
|
|
677
|
+
* expected result JSON (the caller falls back to the generic presentation).
|
|
678
|
+
* @param text - the settled result text.
|
|
679
|
+
*/
|
|
680
|
+
function goalResultLines(text) {
|
|
681
|
+
const value = parseGoalResult(text);
|
|
682
|
+
if (value === void 0) return void 0;
|
|
683
|
+
const goal = value.goal;
|
|
684
|
+
if (goal === null) return ["no goal set"];
|
|
685
|
+
const lines = [];
|
|
686
|
+
if (goal.objective !== void 0 && goal.objective !== "") lines.push(`● objective: ${goal.objective}`);
|
|
687
|
+
const identity = [];
|
|
688
|
+
if (typeof goal.phase === "string" && goal.phase !== "") identity.push(goal.phase);
|
|
689
|
+
if (goal.revision !== void 0) identity.push(`revision ${goal.revision}`);
|
|
690
|
+
if (identity.length > 0) lines.push(`● ${identity.join(" · ")}`);
|
|
691
|
+
if (goal.roundsStarted !== void 0) lines.push(goal.maxGoalRounds === void 0 ? `● rounds: ${goal.roundsStarted}` : `● rounds: ${goal.roundsStarted}/${goal.maxGoalRounds}`);
|
|
692
|
+
const blocked = goal.blockedReason;
|
|
693
|
+
if (blocked !== void 0 && (blocked.code !== void 0 || blocked.message !== void 0)) {
|
|
694
|
+
const detail = [blocked.code, blocked.message].filter((part) => typeof part === "string" && part !== "").join(" — ");
|
|
695
|
+
lines.push(`● blocked: ${detail}`);
|
|
696
|
+
}
|
|
697
|
+
if (value.activation !== void 0) lines.push(`● activation: ${value.activation}`);
|
|
698
|
+
return lines;
|
|
450
699
|
}
|
|
451
700
|
/**
|
|
452
701
|
* The folded-card call preview for tools whose args carry a one-line
|
|
@@ -519,6 +768,41 @@ function toolPresenterFrom(get) {
|
|
|
519
768
|
}
|
|
520
769
|
};
|
|
521
770
|
}
|
|
771
|
+
/**
|
|
772
|
+
* Subagent-family tool names whose call args may carry an explicit
|
|
773
|
+
* model/provider override. Matched by REGISTERED name; unknown names are
|
|
774
|
+
* ignored so third-party subagent-like tools keep their default rendering.
|
|
775
|
+
*/
|
|
776
|
+
const SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
777
|
+
"subagent",
|
|
778
|
+
"subagent_route",
|
|
779
|
+
"subagent_router",
|
|
780
|
+
"subagent_fork"
|
|
781
|
+
]);
|
|
782
|
+
/**
|
|
783
|
+
* Extract the model · provider display line for a subagent-family tool call.
|
|
784
|
+
*
|
|
785
|
+
* COMPATIBILITY CONTRACT: the line renders ONLY when the call args actually
|
|
786
|
+
* carry a model/provider — the top-level `model`/`provider` shape
|
|
787
|
+
* (subagent_route / subagent_router) or the `agentOptions.model/provider`
|
|
788
|
+
* shape. Every other call returns `undefined` and the card renders exactly
|
|
789
|
+
* as before; unknown tool names and unparseable args are ignored. The
|
|
790
|
+
* official `subagent` tool's model lives in deployment config (never in
|
|
791
|
+
* the call args), so it naturally renders nothing.
|
|
792
|
+
* @param name - the tool's registered name.
|
|
793
|
+
* @param argsRaw - the raw tool-call arguments JSON ('' or malformed → none).
|
|
794
|
+
* @returns `model · provider` (either part alone when only one is present),
|
|
795
|
+
* or undefined when the call carries no model/provider.
|
|
796
|
+
*/
|
|
797
|
+
function subagentModelDisplay(name, argsRaw) {
|
|
798
|
+
if (!SUBAGENT_TOOL_NAMES.has(name)) return void 0;
|
|
799
|
+
const parsed = parseArgs(argsRaw);
|
|
800
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
801
|
+
const args = parsed;
|
|
802
|
+
const opts = typeof args.agentOptions === "object" && args.agentOptions !== null ? args.agentOptions : void 0;
|
|
803
|
+
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);
|
|
804
|
+
return parts.length === 0 ? void 0 : parts.join(" · ");
|
|
805
|
+
}
|
|
522
806
|
//#endregion
|
|
523
807
|
//#region src/context.ts
|
|
524
808
|
/**
|
|
@@ -13154,6 +13438,7 @@ var TaskBrowserPanel = class {
|
|
|
13154
13438
|
searchEnabled;
|
|
13155
13439
|
onSelect;
|
|
13156
13440
|
onCancel;
|
|
13441
|
+
onAction;
|
|
13157
13442
|
/** Live tick: bumped every second while a running row is visible. */
|
|
13158
13443
|
now = Date.now();
|
|
13159
13444
|
tickTimer;
|
|
@@ -13168,6 +13453,7 @@ var TaskBrowserPanel = class {
|
|
|
13168
13453
|
this.options = options;
|
|
13169
13454
|
this.onSelect = onSelect;
|
|
13170
13455
|
this.onCancel = onCancel;
|
|
13456
|
+
this.onAction = options.onAction;
|
|
13171
13457
|
this.requestRender = requestRender;
|
|
13172
13458
|
this.searchEnabled = options.enableSearch ?? false;
|
|
13173
13459
|
this.searchInput.onEscape = () => this.onCancel();
|
|
@@ -13230,6 +13516,18 @@ var TaskBrowserPanel = class {
|
|
|
13230
13516
|
const isNavUp = matchesKey(data, "up") || !this.searchEnabled && data === "k";
|
|
13231
13517
|
const isNavDown = matchesKey(data, "down") || !this.searchEnabled && data === "j";
|
|
13232
13518
|
const isPage = matchesKey(data, "pageUp") || matchesKey(data, "pageDown");
|
|
13519
|
+
if (data === "i" && this.onAction !== void 0 && !this.searchEnabled) {
|
|
13520
|
+
const item = this.filtered[this.selected];
|
|
13521
|
+
if (item !== void 0) this.onAction(item.value, "interrupt");
|
|
13522
|
+
return;
|
|
13523
|
+
}
|
|
13524
|
+
if (data === "i" && this.onAction !== void 0 && this.searchEnabled && (this.searchInput.getValue() ?? "") === "") {
|
|
13525
|
+
const item = this.filtered[this.selected];
|
|
13526
|
+
if (item !== void 0 && item.value.startsWith("agent:")) {
|
|
13527
|
+
this.onAction(item.value, "interrupt");
|
|
13528
|
+
return;
|
|
13529
|
+
}
|
|
13530
|
+
}
|
|
13233
13531
|
if (!isNavUp && !isNavDown && !isPage && !matchesKey(data, "enter") && !matchesKey(data, "escape") && this.searchEnabled) {
|
|
13234
13532
|
this.searchInput.handleInput(data);
|
|
13235
13533
|
this.applyFilter(this.searchInput.getValue() ?? "");
|
|
@@ -13270,7 +13568,7 @@ var TaskBrowserPanel = class {
|
|
|
13270
13568
|
if (query === "") this.filtered = this.items;
|
|
13271
13569
|
else {
|
|
13272
13570
|
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));
|
|
13571
|
+
this.filtered = this.items.filter((item) => `${item.value}\n${item.label}\n${item.status}\n${item.detail ?? ""}\n${item.group ?? ""}`.toLowerCase().includes(needle));
|
|
13274
13572
|
}
|
|
13275
13573
|
this.selected = 0;
|
|
13276
13574
|
this.scroll = 0;
|
|
@@ -13346,7 +13644,7 @@ var TaskBrowserPanel = class {
|
|
|
13346
13644
|
return out;
|
|
13347
13645
|
}
|
|
13348
13646
|
hint() {
|
|
13349
|
-
return `${this.searchEnabled ? "type to filter · " : ""}↑↓ navigate · enter open · esc close`;
|
|
13647
|
+
return `${this.searchEnabled ? "type to filter · " : ""}${this.filtered.some((item) => item.value.startsWith("agent:")) ? "i interrupt · " : ""}↑↓ navigate · enter open · esc close`;
|
|
13350
13648
|
}
|
|
13351
13649
|
};
|
|
13352
13650
|
//#endregion
|
|
@@ -13718,6 +14016,11 @@ var QuestionFlow = class {
|
|
|
13718
14016
|
if (row.key === OTHER_ROW && this.editingOther) {
|
|
13719
14017
|
const inputLine = this.otherInput.render(Math.max(1, width - visibleWidth(prefix)))[0] ?? "";
|
|
13720
14018
|
const stripped = inputLine.startsWith("> ") ? inputLine.slice(2) : inputLine;
|
|
14019
|
+
if (question.masked === true && this.otherInput.getValue() !== "") {
|
|
14020
|
+
lines.push(prefix + color.textDim("•".repeat(this.otherInput.getValue().length)));
|
|
14021
|
+
hits.push(OTHER_ROW);
|
|
14022
|
+
continue;
|
|
14023
|
+
}
|
|
13721
14024
|
lines.push(prefix + (this.otherInput.getValue() === "" ? color.textDim(row.label) : stripped));
|
|
13722
14025
|
hits.push(OTHER_ROW);
|
|
13723
14026
|
continue;
|
|
@@ -14030,7 +14333,7 @@ var QuestionFlow = class {
|
|
|
14030
14333
|
const question = this.questions[qi];
|
|
14031
14334
|
const draft = this.drafts[qi];
|
|
14032
14335
|
if (question === void 0 || draft === void 0) continue;
|
|
14033
|
-
const value = draft.skipped ? "(skipped)" : draft.custom !== "" && question.multiSelect !== true ? draft.custom : [...draft.selected].join(", ") + (draft.custom !== "" ? ` + ${draft.custom}` : "");
|
|
14336
|
+
const value = draft.skipped ? "(skipped)" : draft.custom !== "" && question.multiSelect !== true ? question.masked === true ? "•".repeat(draft.custom.length) : draft.custom : [...draft.selected].join(", ") + (draft.custom !== "" ? ` + ${draft.custom}` : "");
|
|
14034
14337
|
reviewBudget = appendWrappedBudgeted(lines, `${color.textDim(`Q${qi + 1}`)} `, " ", question.question, safeWidth, reviewBudget);
|
|
14035
14338
|
if (reviewBudget > 0) reviewBudget = appendWrappedBudgeted(lines, " ", " ", value === "" ? color.textDim("(no answer)") : value, safeWidth, reviewBudget);
|
|
14036
14339
|
}
|
|
@@ -14091,7 +14394,8 @@ var QuestionFlow = class {
|
|
|
14091
14394
|
if (optionless) {
|
|
14092
14395
|
const inputLine = this.otherInput.render(Math.max(1, safeWidth - 2))[0] ?? "";
|
|
14093
14396
|
const stripped = inputLine.startsWith("> ") ? inputLine.slice(2) : inputLine;
|
|
14094
|
-
|
|
14397
|
+
if (question.masked === true && this.otherInput.getValue() !== "") lines.push(` ${color.textDim("•".repeat(this.otherInput.getValue().length))}`);
|
|
14398
|
+
else lines.push(this.otherInput.getValue() === "" ? color.textDim(" Type your answer…") : ` ${stripped}`);
|
|
14095
14399
|
}
|
|
14096
14400
|
if (draft.skipped) lines.push(color.textDim("(skipped)"));
|
|
14097
14401
|
lines.push("");
|
|
@@ -14120,6 +14424,305 @@ var QuestionFlow = class {
|
|
|
14120
14424
|
}
|
|
14121
14425
|
};
|
|
14122
14426
|
//#endregion
|
|
14427
|
+
//#region src/shell-words.ts
|
|
14428
|
+
/**
|
|
14429
|
+
* A small, reliable POSIX-ish shell word parser for external-command
|
|
14430
|
+
* strings like `$VISUAL`/`$EDITOR` (`code --wait`, `vim -f`, `sensible-editor
|
|
14431
|
+
* --nofork`) and `!` shell lines. It handles the quoting rules that matter
|
|
14432
|
+
* in practice — single quotes (literal), double quotes (backslash escapes,
|
|
14433
|
+
* everything else verbatim — no expansion), and backslash escapes outside
|
|
14434
|
+
* quotes — WITHOUT executing a shell.
|
|
14435
|
+
*
|
|
14436
|
+
* This deliberately replaces naive `command.split(/\s+/)` splitting: an
|
|
14437
|
+
* editor path with spaces or a quoted argument would be mangled into
|
|
14438
|
+
* phantom arguments.
|
|
14439
|
+
* @module @xmoon76/dsh-pi-tui/shell-words
|
|
14440
|
+
*/
|
|
14441
|
+
/**
|
|
14442
|
+
* Split a command line into words, POSIX-ish. Never throws: unbalanced
|
|
14443
|
+
* quotes are treated as a trailing literal word (matching how shells
|
|
14444
|
+
* consume the rest of the line). Backslashes before non-special characters
|
|
14445
|
+
* are dropped outside quotes; inside double quotes a backslash only escapes
|
|
14446
|
+
* `"`, `\`, and `$` and is kept verbatim before anything else.
|
|
14447
|
+
* @param line - the raw command line.
|
|
14448
|
+
* @returns the parsed words.
|
|
14449
|
+
*/
|
|
14450
|
+
function parseShellWords(line) {
|
|
14451
|
+
const words = [];
|
|
14452
|
+
let current = "";
|
|
14453
|
+
let inWord = false;
|
|
14454
|
+
let quote;
|
|
14455
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
14456
|
+
const char = line[index];
|
|
14457
|
+
if (quote === void 0 && char === "\\") {
|
|
14458
|
+
const next = line[index + 1];
|
|
14459
|
+
if (next !== void 0) {
|
|
14460
|
+
current += next;
|
|
14461
|
+
index += 1;
|
|
14462
|
+
} else current += "\\";
|
|
14463
|
+
inWord = true;
|
|
14464
|
+
continue;
|
|
14465
|
+
}
|
|
14466
|
+
if (quote === void 0 && (char === "'" || char === "\"")) {
|
|
14467
|
+
quote = char;
|
|
14468
|
+
inWord = true;
|
|
14469
|
+
continue;
|
|
14470
|
+
}
|
|
14471
|
+
if (quote === "\"" && char === "\\") {
|
|
14472
|
+
const next = line[index + 1];
|
|
14473
|
+
if (next === "\"" || next === "\\" || next === "$") {
|
|
14474
|
+
current += next;
|
|
14475
|
+
index += 1;
|
|
14476
|
+
} else current += "\\";
|
|
14477
|
+
inWord = true;
|
|
14478
|
+
continue;
|
|
14479
|
+
}
|
|
14480
|
+
if (quote === "'" && char === "'") {
|
|
14481
|
+
quote = void 0;
|
|
14482
|
+
continue;
|
|
14483
|
+
}
|
|
14484
|
+
if (quote === "\"" && char === "\"") {
|
|
14485
|
+
quote = void 0;
|
|
14486
|
+
continue;
|
|
14487
|
+
}
|
|
14488
|
+
if (quote === void 0 && (char === " " || char === " " || char === "\n")) {
|
|
14489
|
+
if (inWord) {
|
|
14490
|
+
words.push(current);
|
|
14491
|
+
current = "";
|
|
14492
|
+
inWord = false;
|
|
14493
|
+
}
|
|
14494
|
+
continue;
|
|
14495
|
+
}
|
|
14496
|
+
current += char;
|
|
14497
|
+
inWord = true;
|
|
14498
|
+
}
|
|
14499
|
+
if (inWord) words.push(current);
|
|
14500
|
+
return words;
|
|
14501
|
+
}
|
|
14502
|
+
//#endregion
|
|
14503
|
+
//#region src/shell-completion.ts
|
|
14504
|
+
/**
|
|
14505
|
+
* Bash completion for `!`/`!!` shell lines: a real-shell `compgen` bridge
|
|
14506
|
+
* (docs/input-and-card-polish.md §1). Command names come from
|
|
14507
|
+
* `compgen -A command` (cached per cwd+PATH for 30s), `$VAR` names from
|
|
14508
|
+
* `compgen -A variable`, and subcommands from a small per-command table
|
|
14509
|
+
* (`git --list-cmds`). Path completion stays with the fork's fd-backed
|
|
14510
|
+
* provider — the bridge never competes with it.
|
|
14511
|
+
*
|
|
14512
|
+
* Decisions that matter (design doc):
|
|
14513
|
+
* - `bash -lc`, never `-ic`: an interactive shell sources `.bashrc` on
|
|
14514
|
+
* every keystroke (slow, and runs user startup code). Alias completion
|
|
14515
|
+
* is therefore not provided — accepted limitation.
|
|
14516
|
+
* - One spawn per request, hard-capped at COMPGEN_TIMEOUT_MS and wired to
|
|
14517
|
+
* the editor's AbortSignal; the caller commits latest-only. A slow or
|
|
14518
|
+
* missing `bash` degrades to no suggestions, never an error.
|
|
14519
|
+
* - The completion prefix crosses to bash through an environment variable
|
|
14520
|
+
* (COMPGEN_WORD), never string interpolation — no shell injection from
|
|
14521
|
+
* the user's own draft.
|
|
14522
|
+
* @module @xmoon76/dsh-pi-tui/shell-completion
|
|
14523
|
+
*/
|
|
14524
|
+
/**
|
|
14525
|
+
* Hard cap on one compgen spawn (ms). GitHub Actions runners are markedly
|
|
14526
|
+
* slower at a `bash -lc` cold start (measured 260-310ms in CI vs ~60ms
|
|
14527
|
+
* locally — a login shell sources /etc/profile, which is heavy on the
|
|
14528
|
+
* runner image), so a single tight cap made the suite flaky there (three
|
|
14529
|
+
* CI failures, every one at the 300ms boundary). The cap is therefore
|
|
14530
|
+
* raised under GitHub Actions only; local runs keep the tight cap.
|
|
14531
|
+
*/
|
|
14532
|
+
const COMPGEN_TIMEOUT_MS = process.env.GITHUB_ACTIONS === "true" ? 1500 : 300;
|
|
14533
|
+
/** Command-name cache TTL (ms): command sets change rarely. */
|
|
14534
|
+
const COMMAND_CACHE_TTL_MS = 3e4;
|
|
14535
|
+
/** Suggestion cap per request (the fork's own lists are capped too). */
|
|
14536
|
+
const MAX_SUGGESTIONS = 50;
|
|
14537
|
+
/**
|
|
14538
|
+
* Parse the editor line at the cursor for shell completion. Returns a
|
|
14539
|
+
* context only when the line is a `!`/`!!` line AND the cursor completes a
|
|
14540
|
+
* shell-bridge word (command name, subcommand, `$VAR`); a path position
|
|
14541
|
+
* returns undefined so the caller falls through to the fork's fd
|
|
14542
|
+
* completion — paths are not the bridge's job.
|
|
14543
|
+
* @param line - the full editor line.
|
|
14544
|
+
* @param col - the cursor column.
|
|
14545
|
+
*/
|
|
14546
|
+
function shellCompletionContext(line, col) {
|
|
14547
|
+
if (!line.startsWith("!")) return void 0;
|
|
14548
|
+
const prefixLength = line.startsWith("!!") ? 2 : 1;
|
|
14549
|
+
const before = line.slice(prefixLength, col);
|
|
14550
|
+
const lastSpace = Math.max(before.lastIndexOf(" "), before.lastIndexOf(" "));
|
|
14551
|
+
const currentWord = before.slice(lastSpace + 1);
|
|
14552
|
+
const priorWords = parseShellWords(lastSpace === -1 ? "" : before.slice(0, lastSpace));
|
|
14553
|
+
if (priorWords.length === 0) {
|
|
14554
|
+
if (currentWord.startsWith(".") || currentWord.startsWith("/") || currentWord.startsWith("~")) return void 0;
|
|
14555
|
+
return {
|
|
14556
|
+
kind: "command",
|
|
14557
|
+
prefix: currentWord,
|
|
14558
|
+
priorWords
|
|
14559
|
+
};
|
|
14560
|
+
}
|
|
14561
|
+
if (currentWord.startsWith("$")) return {
|
|
14562
|
+
kind: "variable",
|
|
14563
|
+
prefix: currentWord,
|
|
14564
|
+
priorWords
|
|
14565
|
+
};
|
|
14566
|
+
if (priorWords.length === 1 && SUBCOMMAND_TABLE[priorWords[0]] !== void 0) return {
|
|
14567
|
+
kind: "subcommand",
|
|
14568
|
+
prefix: currentWord,
|
|
14569
|
+
priorWords
|
|
14570
|
+
};
|
|
14571
|
+
}
|
|
14572
|
+
/**
|
|
14573
|
+
* Per-command subcommand completion. `lister` emits the live list (one
|
|
14574
|
+
* name per line, no headers) when the installed command supports it;
|
|
14575
|
+
* `fallback` covers older versions (e.g. git before 2.18 has no
|
|
14576
|
+
* `--list-cmds`) and is used when the lister produces nothing. Extend the
|
|
14577
|
+
* table with one entry per command.
|
|
14578
|
+
*/
|
|
14579
|
+
const SUBCOMMAND_TABLE = { git: {
|
|
14580
|
+
lister: "git --list-cmds 2>/dev/null",
|
|
14581
|
+
fallback: [
|
|
14582
|
+
"add",
|
|
14583
|
+
"am",
|
|
14584
|
+
"archive",
|
|
14585
|
+
"bisect",
|
|
14586
|
+
"branch",
|
|
14587
|
+
"bundle",
|
|
14588
|
+
"checkout",
|
|
14589
|
+
"cherry-pick",
|
|
14590
|
+
"clean",
|
|
14591
|
+
"clone",
|
|
14592
|
+
"commit",
|
|
14593
|
+
"config",
|
|
14594
|
+
"diff",
|
|
14595
|
+
"fetch",
|
|
14596
|
+
"init",
|
|
14597
|
+
"log",
|
|
14598
|
+
"merge",
|
|
14599
|
+
"mv",
|
|
14600
|
+
"pull",
|
|
14601
|
+
"push",
|
|
14602
|
+
"rebase",
|
|
14603
|
+
"reset",
|
|
14604
|
+
"restore",
|
|
14605
|
+
"revert",
|
|
14606
|
+
"rm",
|
|
14607
|
+
"show",
|
|
14608
|
+
"stash",
|
|
14609
|
+
"status",
|
|
14610
|
+
"switch",
|
|
14611
|
+
"tag"
|
|
14612
|
+
]
|
|
14613
|
+
} };
|
|
14614
|
+
let compgenRunner = (cwd, expression, prefix, signal) => runCompgenSpawn(cwd, expression, prefix, signal);
|
|
14615
|
+
function runCompgenSpawn(cwd, expression, prefix, signal) {
|
|
14616
|
+
return new Promise((resolve) => {
|
|
14617
|
+
const settle = (ok, lines) => {
|
|
14618
|
+
if (settled) return;
|
|
14619
|
+
settled = true;
|
|
14620
|
+
clearTimeout(timer);
|
|
14621
|
+
signal.removeEventListener("abort", onAbort);
|
|
14622
|
+
resolve({
|
|
14623
|
+
ok,
|
|
14624
|
+
lines
|
|
14625
|
+
});
|
|
14626
|
+
};
|
|
14627
|
+
let settled = false;
|
|
14628
|
+
const onAbort = () => {
|
|
14629
|
+
child.kill();
|
|
14630
|
+
settle(false, []);
|
|
14631
|
+
};
|
|
14632
|
+
const child = spawn("bash", ["-lc", expression], {
|
|
14633
|
+
cwd,
|
|
14634
|
+
stdio: [
|
|
14635
|
+
"ignore",
|
|
14636
|
+
"pipe",
|
|
14637
|
+
"pipe"
|
|
14638
|
+
],
|
|
14639
|
+
env: {
|
|
14640
|
+
...process.env,
|
|
14641
|
+
COMPGEN_WORD: prefix
|
|
14642
|
+
}
|
|
14643
|
+
});
|
|
14644
|
+
const timer = setTimeout(() => {
|
|
14645
|
+
child.kill("SIGKILL");
|
|
14646
|
+
settle(false, []);
|
|
14647
|
+
}, COMPGEN_TIMEOUT_MS);
|
|
14648
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
14649
|
+
let out = "";
|
|
14650
|
+
child.stdout.on("data", (chunk) => {
|
|
14651
|
+
out += chunk.toString("utf8");
|
|
14652
|
+
});
|
|
14653
|
+
child.stderr.on("data", () => {});
|
|
14654
|
+
child.on("error", () => settle(false, []));
|
|
14655
|
+
child.on("close", (code) => {
|
|
14656
|
+
settle(code === 0, out.split("\n").filter((line) => line !== ""));
|
|
14657
|
+
});
|
|
14658
|
+
});
|
|
14659
|
+
}
|
|
14660
|
+
/** Command-name cache, keyed by cwd + PATH (30s TTL). Only SUCCESSFUL runs
|
|
14661
|
+
* are cached — a failed run (timeout/abort/spawn error) must not suppress
|
|
14662
|
+
* completion for the whole TTL, so the next request retries the spawn. */
|
|
14663
|
+
const commandCache = /* @__PURE__ */ new Map();
|
|
14664
|
+
/** The cached `compgen -A command` list for one cwd+PATH, refreshed on
|
|
14665
|
+
* expiry or after a failed run. */
|
|
14666
|
+
async function cachedCommands(cwd, signal) {
|
|
14667
|
+
const key = `${cwd}\0${process.env.PATH ?? ""}`;
|
|
14668
|
+
const entry = commandCache.get(key);
|
|
14669
|
+
if (entry !== void 0 && entry.expires > Date.now()) return entry.commands;
|
|
14670
|
+
const run = await compgenRunner(cwd, "compgen -A command", "", signal);
|
|
14671
|
+
if (!run.ok) return [];
|
|
14672
|
+
commandCache.set(key, {
|
|
14673
|
+
expires: Date.now() + COMMAND_CACHE_TTL_MS,
|
|
14674
|
+
commands: run.lines
|
|
14675
|
+
});
|
|
14676
|
+
return run.lines;
|
|
14677
|
+
}
|
|
14678
|
+
/** Filter one candidate list to the prefix and cap it ('' prefix keeps all). */
|
|
14679
|
+
function matchesFor(prefix, candidates) {
|
|
14680
|
+
const matched = prefix === "" ? candidates : candidates.filter((name) => name.startsWith(prefix));
|
|
14681
|
+
if (matched.length === 0) return null;
|
|
14682
|
+
return {
|
|
14683
|
+
items: matched.slice(0, MAX_SUGGESTIONS).map((name) => ({
|
|
14684
|
+
value: name,
|
|
14685
|
+
label: name
|
|
14686
|
+
})),
|
|
14687
|
+
prefix
|
|
14688
|
+
};
|
|
14689
|
+
}
|
|
14690
|
+
/**
|
|
14691
|
+
* Suggest shell completions for a parsed `!` line context. Returns null
|
|
14692
|
+
* when nothing matches or the shell is unavailable — the editor then shows
|
|
14693
|
+
* no suggestions (the caller falls through to the fork's own provider).
|
|
14694
|
+
* @param context - the parsed cursor context.
|
|
14695
|
+
* @param cwd - the session workspace (compgen runs there).
|
|
14696
|
+
* @param options - the editor's request options (signal; force: an empty
|
|
14697
|
+
* command prefix lists the cached commands on explicit Tab, while a
|
|
14698
|
+
* natural trigger with an empty prefix stays quiet).
|
|
14699
|
+
*/
|
|
14700
|
+
async function suggestShellCompletion(context, cwd, options) {
|
|
14701
|
+
if (options.signal.aborted) return null;
|
|
14702
|
+
if (context.kind === "command") {
|
|
14703
|
+
if (context.prefix === "" && options.force !== true) return null;
|
|
14704
|
+
const commands = await cachedCommands(cwd, options.signal);
|
|
14705
|
+
return matchesFor(context.prefix, commands);
|
|
14706
|
+
}
|
|
14707
|
+
if (context.kind === "variable") {
|
|
14708
|
+
const run = await compgenRunner(cwd, "compgen -A variable -- \"$COMPGEN_WORD\"", context.prefix.slice(1), options.signal);
|
|
14709
|
+
if (!run.ok) return null;
|
|
14710
|
+
const items = run.lines.slice(0, MAX_SUGGESTIONS).map((name) => ({
|
|
14711
|
+
value: `$${name}`,
|
|
14712
|
+
label: `$${name}`
|
|
14713
|
+
}));
|
|
14714
|
+
return items.length === 0 ? null : {
|
|
14715
|
+
items,
|
|
14716
|
+
prefix: context.prefix
|
|
14717
|
+
};
|
|
14718
|
+
}
|
|
14719
|
+
const command = context.priorWords[0];
|
|
14720
|
+
const entry = SUBCOMMAND_TABLE[command];
|
|
14721
|
+
const run = await compgenRunner(cwd, entry.lister, "", options.signal);
|
|
14722
|
+
const candidates = run.ok && run.lines.length > 0 ? run.lines.map((line) => line.trim()).filter((line) => line !== "") : entry.fallback;
|
|
14723
|
+
return matchesFor(context.prefix, candidates);
|
|
14724
|
+
}
|
|
14725
|
+
//#endregion
|
|
14123
14726
|
//#region src/mentions.ts
|
|
14124
14727
|
/**
|
|
14125
14728
|
* @-file mention completion for the editor: a consumer-side wrapper over the
|
|
@@ -14270,13 +14873,19 @@ var MentionProvider = class {
|
|
|
14270
14873
|
this.inner = new CombinedAutocompleteProvider([...slashCommands], workDir, fdPath);
|
|
14271
14874
|
}
|
|
14272
14875
|
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
14273
|
-
const
|
|
14876
|
+
const currentLine = lines[cursorLine] ?? "";
|
|
14877
|
+
const atPrefix = extractAtPrefix(currentLine.slice(0, cursorCol));
|
|
14274
14878
|
if (atPrefix !== null) {
|
|
14275
14879
|
if (this.fdPath !== null) try {
|
|
14276
14880
|
return await this.inner.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
14277
14881
|
} catch {}
|
|
14278
14882
|
return fsMentionSuggestions(this.workDir, atPrefix, options.signal);
|
|
14279
14883
|
}
|
|
14884
|
+
const shellContext = shellCompletionContext(currentLine, cursorCol);
|
|
14885
|
+
if (shellContext !== void 0) {
|
|
14886
|
+
const suggestions = await suggestShellCompletion(shellContext, this.workDir, options);
|
|
14887
|
+
if (suggestions !== null) return suggestions;
|
|
14888
|
+
}
|
|
14280
14889
|
try {
|
|
14281
14890
|
return await this.inner.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
14282
14891
|
} catch {
|
|
@@ -14284,6 +14893,19 @@ var MentionProvider = class {
|
|
|
14284
14893
|
}
|
|
14285
14894
|
}
|
|
14286
14895
|
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
14896
|
+
const currentLine = lines[cursorLine] ?? "";
|
|
14897
|
+
if (shellCompletionContext(currentLine, cursorCol) !== void 0) {
|
|
14898
|
+
const before = currentLine.slice(0, cursorCol - prefix.length);
|
|
14899
|
+
const after = currentLine.slice(cursorCol);
|
|
14900
|
+
const newLine = `${before}${item.value} ${after}`;
|
|
14901
|
+
const newLines = [...lines];
|
|
14902
|
+
newLines[cursorLine] = newLine;
|
|
14903
|
+
return {
|
|
14904
|
+
lines: newLines,
|
|
14905
|
+
cursorLine,
|
|
14906
|
+
cursorCol: before.length + item.value.length + 1
|
|
14907
|
+
};
|
|
14908
|
+
}
|
|
14287
14909
|
return this.inner.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
14288
14910
|
}
|
|
14289
14911
|
shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
|
|
@@ -14820,9 +15442,9 @@ function describeKey(key) {
|
|
|
14820
15442
|
* This inventory is the SINGLE authoritative list, kept in sync with the
|
|
14821
15443
|
* host's `matchesKey` lifecycle checks in tui-app.ts (Ctrl+C/D exit,
|
|
14822
15444
|
* 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
|
-
*
|
|
15445
|
+
* expand, Ctrl+T todo panel, Ctrl+G external editor, Ctrl+Enter queue,
|
|
15446
|
+
* Enter submit, Esc cancel, Shift+Tab permission cycle, Alt+Up dequeue,
|
|
15447
|
+
* Alt+T thinking toggle). Every
|
|
14826
15448
|
* reserved binding here must match a host `matchesKey(data, ...)` call;
|
|
14827
15449
|
* when a new host lifecycle key lands, extend THIS list in the same
|
|
14828
15450
|
* commit.
|
|
@@ -14884,13 +15506,6 @@ const RESERVED_HOST_KEYS = [
|
|
|
14884
15506
|
shift: false,
|
|
14885
15507
|
super: false
|
|
14886
15508
|
},
|
|
14887
|
-
{
|
|
14888
|
-
key: "j",
|
|
14889
|
-
ctrl: true,
|
|
14890
|
-
alt: false,
|
|
14891
|
-
shift: false,
|
|
14892
|
-
super: false
|
|
14893
|
-
},
|
|
14894
15509
|
{
|
|
14895
15510
|
key: "enter",
|
|
14896
15511
|
ctrl: true,
|
|
@@ -16799,6 +17414,7 @@ var QuestionFrame = class extends Frame {
|
|
|
16799
17414
|
render(width) {
|
|
16800
17415
|
const rows = Math.max(1, this.heightOf());
|
|
16801
17416
|
this.lastTermRows = rows;
|
|
17417
|
+
this.lastTermColumns = width;
|
|
16802
17418
|
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
17419
|
this.flow.setMaxRows(frameRows - 2);
|
|
16804
17420
|
const out = super.render(width);
|
|
@@ -16813,12 +17429,18 @@ var QuestionFrame = class extends Frame {
|
|
|
16813
17429
|
get termRows() {
|
|
16814
17430
|
return this.lastTermRows;
|
|
16815
17431
|
}
|
|
17432
|
+
/** The terminal width the last render used (a resize changes wrapping and
|
|
17433
|
+
* the flow's hit map, so clicks must wait for a fresh render too). */
|
|
17434
|
+
get termColumns() {
|
|
17435
|
+
return this.lastTermColumns;
|
|
17436
|
+
}
|
|
16816
17437
|
get focused() {
|
|
16817
17438
|
return this.flow.focused;
|
|
16818
17439
|
}
|
|
16819
17440
|
set focused(value) {
|
|
16820
17441
|
this.flow.focused = value;
|
|
16821
17442
|
}
|
|
17443
|
+
lastTermColumns = 0;
|
|
16822
17444
|
};
|
|
16823
17445
|
/** The session head card: identity facts, wrapped to the available width so
|
|
16824
17446
|
* nothing is truncated, framed with a box whose width matches the editor's
|
|
@@ -17136,6 +17758,9 @@ var TuiApp = class TuiApp {
|
|
|
17136
17758
|
todoItems = [];
|
|
17137
17759
|
/** Ctrl+T: whether the todo panel between transcript and editor is shown. */
|
|
17138
17760
|
todoPanelVisible = false;
|
|
17761
|
+
/** Fullscreen click on the todo panel (or the panel's own expand verb):
|
|
17762
|
+
* whether the panel shows the FULL list or the compact five rows. */
|
|
17763
|
+
todoExpanded = false;
|
|
17139
17764
|
/** The todo panel Text; empty when hidden. */
|
|
17140
17765
|
todoPanel;
|
|
17141
17766
|
/**
|
|
@@ -17298,8 +17923,11 @@ var TuiApp = class TuiApp {
|
|
|
17298
17923
|
workingMessageOverride;
|
|
17299
17924
|
/** Timestamp of the last Ctrl+C press, for the empty-editor exit chord. */
|
|
17300
17925
|
lastCtrlCAt;
|
|
17301
|
-
/** The empty-editor double-Ctrl+C exit window in ms
|
|
17302
|
-
|
|
17926
|
+
/** The empty-editor double-Ctrl+C exit window in ms. A 500ms window
|
|
17927
|
+
* silently misses a human-paced double press (0.6–1s apart), which
|
|
17928
|
+
* read as "the chord doesn't work"; 1.5s covers a natural double
|
|
17929
|
+
* press, and the armed state is announced by the hint. */
|
|
17930
|
+
static CTRL_C_EXIT_WINDOW_MS = 1500;
|
|
17303
17931
|
/** Session workspace root for path relativization (Web relativizeToCwd). */
|
|
17304
17932
|
workspaceRoot;
|
|
17305
17933
|
/** The tool presentation bridge, wired by the runner to the live registry. */
|
|
@@ -17772,6 +18400,7 @@ var TuiApp = class TuiApp {
|
|
|
17772
18400
|
this.clearNotify();
|
|
17773
18401
|
this.seatEditor().setText("");
|
|
17774
18402
|
this.editorSeatHolder.notifyChanged();
|
|
18403
|
+
this.requestRender();
|
|
17775
18404
|
this.events.onQueueSubmit(text);
|
|
17776
18405
|
return { consume: true };
|
|
17777
18406
|
}
|
|
@@ -17810,10 +18439,11 @@ var TuiApp = class TuiApp {
|
|
|
17810
18439
|
const draft = this.seatEditor().getText();
|
|
17811
18440
|
this.seatEditor().setText("");
|
|
17812
18441
|
this.editorSeatHolder.notifyChanged();
|
|
18442
|
+
this.requestRender();
|
|
17813
18443
|
this.events.onSteer?.(draft);
|
|
17814
18444
|
return { consume: true };
|
|
17815
18445
|
}
|
|
17816
|
-
if (matchesKey(data, "down")
|
|
18446
|
+
if (matchesKey(data, "down")) {
|
|
17817
18447
|
if (this.tasksActive && !this.activeScreen.hasOverlayEntries && this.seatEditor().getText().trim() === "") {
|
|
17818
18448
|
this.events.onOpenTasks?.();
|
|
17819
18449
|
return { consume: true };
|
|
@@ -17831,13 +18461,17 @@ var TuiApp = class TuiApp {
|
|
|
17831
18461
|
this.seatEditor().setText("");
|
|
17832
18462
|
this.editorSeatHolder.notifyChanged();
|
|
17833
18463
|
this.lastCtrlCAt = Date.now();
|
|
18464
|
+
this.requestRender();
|
|
17834
18465
|
return { consume: true };
|
|
17835
18466
|
}
|
|
17836
18467
|
const now = Date.now();
|
|
17837
18468
|
if (this.lastCtrlCAt !== void 0 && now - this.lastCtrlCAt < TuiApp.CTRL_C_EXIT_WINDOW_MS) {
|
|
17838
18469
|
this.lastCtrlCAt = void 0;
|
|
17839
18470
|
this.events.onExit();
|
|
17840
|
-
} else
|
|
18471
|
+
} else {
|
|
18472
|
+
this.lastCtrlCAt = now;
|
|
18473
|
+
this.notify("Press Ctrl+C again to exit", "info");
|
|
18474
|
+
}
|
|
17841
18475
|
return { consume: true };
|
|
17842
18476
|
}
|
|
17843
18477
|
if (matchesKey(data, "ctrl+d")) {
|
|
@@ -18388,7 +19022,7 @@ var TuiApp = class TuiApp {
|
|
|
18388
19022
|
handleFullscreenClick(x, y) {
|
|
18389
19023
|
const question = this.activeQuestions;
|
|
18390
19024
|
if (question?.frame !== void 0) {
|
|
18391
|
-
if (this.terminal.rows !== question.frame.termRows) return;
|
|
19025
|
+
if (this.terminal.rows !== question.frame.termRows || this.terminal.columns !== question.frame.termColumns) return;
|
|
18392
19026
|
const width = this.terminal.columns;
|
|
18393
19027
|
const height = this.terminal.rows;
|
|
18394
19028
|
const footerHeight = this.footer.render(width).length;
|
|
@@ -18400,10 +19034,28 @@ var TuiApp = class TuiApp {
|
|
|
18400
19034
|
question.flow.clickRow(y - seatTop - 1);
|
|
18401
19035
|
this.requestRender();
|
|
18402
19036
|
}
|
|
18403
|
-
return;
|
|
18404
19037
|
}
|
|
19038
|
+
return;
|
|
18405
19039
|
}
|
|
18406
19040
|
const width = this.terminal.columns;
|
|
19041
|
+
const height = this.terminal.rows;
|
|
19042
|
+
const footerHeight = this.footer.render(width).length;
|
|
19043
|
+
const editorHeight = this.editorSeat.render(width).length;
|
|
19044
|
+
const workingHeight = this.working.render(width).length;
|
|
19045
|
+
const queueHeight = this.queuePane.render(width).length;
|
|
19046
|
+
const goalHeight = this.goalLine.render(width).length;
|
|
19047
|
+
const todoHeight = this.todoPanel.render(width).length;
|
|
19048
|
+
const todoBottom = Math.max(0, Math.min(height, height - footerHeight - editorHeight - workingHeight - queueHeight - goalHeight));
|
|
19049
|
+
const todoTop = Math.max(0, todoBottom - todoHeight);
|
|
19050
|
+
const dockHeight = this.dock.render(width).length;
|
|
19051
|
+
if (dockHeight > 0 && y >= todoTop - dockHeight && y < todoTop) {
|
|
19052
|
+
this.toggleTodoPanel();
|
|
19053
|
+
return;
|
|
19054
|
+
}
|
|
19055
|
+
if (todoTop < todoBottom && y >= todoTop && y < todoBottom) {
|
|
19056
|
+
this.handleTodoPanelClick();
|
|
19057
|
+
return;
|
|
19058
|
+
}
|
|
18407
19059
|
const rowInScroll = y - this.header.render(width).length;
|
|
18408
19060
|
if (rowInScroll < 0) return;
|
|
18409
19061
|
const scrollTop = this.fullscreenScroll?.scrollTop ?? 0;
|
|
@@ -19551,7 +20203,18 @@ var TuiApp = class TuiApp {
|
|
|
19551
20203
|
const rows = [];
|
|
19552
20204
|
const callPreview = parseCallPreview(message.name, message.args);
|
|
19553
20205
|
const foldedCall = header.summary === "" ? foldedCallPreview(message.name, message.args) : "";
|
|
19554
|
-
|
|
20206
|
+
let resultPreview;
|
|
20207
|
+
if (message.name === "read") resultPreview = readFoldedPreview(message.result);
|
|
20208
|
+
else if (message.name === "ask_user_question") {
|
|
20209
|
+
const summary = message.error === void 0 ? askAnswersSummary(message.result) : void 0;
|
|
20210
|
+
resultPreview = summary === void 0 ? "" : ` — ${summary}`;
|
|
20211
|
+
} else if (GOAL_TOOL_NAMES.has(message.name)) {
|
|
20212
|
+
const summary = message.error === void 0 ? goalResultSummary(message.result) : void 0;
|
|
20213
|
+
resultPreview = summary === void 0 ? "" : ` — ${summary}`;
|
|
20214
|
+
} else if (FOLDED_JSON_RESULT_TOOLS.has(message.name)) {
|
|
20215
|
+
const summary = foldedResultSummaryFor(message.name, message.result);
|
|
20216
|
+
resultPreview = summary === void 0 || summary === "" ? "" : ` — ${summary}`;
|
|
20217
|
+
} else resultPreview = message.result === "" ? "" : ` — ${preview(message.result, 3)}`;
|
|
19555
20218
|
const callHead = foldedCall === "" ? "" : foldedCall;
|
|
19556
20219
|
const headWithPreview = `${head}${callHead}${resultPreview}`;
|
|
19557
20220
|
if (callPreview?.kind === "bash" && callPreview.command !== "") {
|
|
@@ -19723,6 +20386,8 @@ var TuiApp = class TuiApp {
|
|
|
19723
20386
|
return;
|
|
19724
20387
|
}
|
|
19725
20388
|
if (message.status === "running") {
|
|
20389
|
+
const modelLine = subagentModelDisplay(message.name, message.args);
|
|
20390
|
+
if (modelLine !== void 0) card.addChild(new Text(color.textDim(modelLine), 0, 0));
|
|
19726
20391
|
const callView = this.present?.call(message.name, message.args);
|
|
19727
20392
|
if (callView !== void 0) {
|
|
19728
20393
|
if (callView.card === "diff" && callView.diffs.length > 0) {
|
|
@@ -19752,10 +20417,25 @@ var TuiApp = class TuiApp {
|
|
|
19752
20417
|
const summary = message.status === "ok" ? askAnswersSummary(message.result) : void 0;
|
|
19753
20418
|
if (summary !== void 0) {
|
|
19754
20419
|
card.addChild(new Text(color.textDim(summary), 0, 0));
|
|
20420
|
+
const answerLines = askAnswersLines(message.result);
|
|
20421
|
+
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));
|
|
20422
|
+
return;
|
|
20423
|
+
}
|
|
20424
|
+
}
|
|
20425
|
+
if (GOAL_TOOL_NAMES.has(message.name)) {
|
|
20426
|
+
if (message.error !== void 0) {
|
|
20427
|
+
card.addChild(new Text(color.textDim(`${message.error.name}: ${message.error.code}`), 0, 0));
|
|
20428
|
+
return;
|
|
20429
|
+
}
|
|
20430
|
+
const goalLines = message.status === "ok" ? goalResultLines(message.result) : void 0;
|
|
20431
|
+
if (goalLines !== void 0) {
|
|
20432
|
+
for (const line of goalLines) card.addChild(new Text(color.textDim(line), 0, 0));
|
|
19755
20433
|
return;
|
|
19756
20434
|
}
|
|
19757
20435
|
}
|
|
19758
20436
|
if (message.result === "" && (message.resultBlocks?.length ?? 0) === 0) return;
|
|
20437
|
+
const settledModelLine = subagentModelDisplay(message.name, message.args);
|
|
20438
|
+
if (settledModelLine !== void 0) card.addChild(new Text(color.textDim(settledModelLine), 0, 0));
|
|
19759
20439
|
const resultView = this.present?.result(message.name, message.args, {
|
|
19760
20440
|
content: message.resultBlocks ?? [],
|
|
19761
20441
|
isError: message.status === "error",
|
|
@@ -19957,19 +20637,42 @@ var TuiApp = class TuiApp {
|
|
|
19957
20637
|
/** Toggle the todo panel between the transcript and the editor. */
|
|
19958
20638
|
toggleTodoPanel() {
|
|
19959
20639
|
this.todoPanelVisible = !this.todoPanelVisible;
|
|
20640
|
+
if (!this.todoPanelVisible) this.todoExpanded = false;
|
|
19960
20641
|
this.renderTodoPanel();
|
|
19961
20642
|
this.renderDock();
|
|
19962
20643
|
this.requestRender();
|
|
19963
20644
|
return this.todoPanelVisible;
|
|
19964
20645
|
}
|
|
20646
|
+
/** Toggle the todo panel between the compact five rows and the full list
|
|
20647
|
+
* (fullscreen click on the panel's area). */
|
|
20648
|
+
toggleTodoExpanded() {
|
|
20649
|
+
if (!this.todoPanelVisible) return false;
|
|
20650
|
+
this.todoExpanded = !this.todoExpanded;
|
|
20651
|
+
this.renderTodoPanel();
|
|
20652
|
+
this.requestRender();
|
|
20653
|
+
return this.todoExpanded;
|
|
20654
|
+
}
|
|
20655
|
+
/** The fullscreen click loop over the todo panel's own rows: compact →
|
|
20656
|
+
* full list → back to the summary row (the panel closes). The mouse thus
|
|
20657
|
+
* opens AND closes the panel without Ctrl+T; the dock summary row itself
|
|
20658
|
+
* opens it (handleFullscreenClick's dock region). */
|
|
20659
|
+
handleTodoPanelClick() {
|
|
20660
|
+
if (this.todoExpanded) this.toggleTodoPanel();
|
|
20661
|
+
else this.toggleTodoExpanded();
|
|
20662
|
+
}
|
|
19965
20663
|
/** Whether the todo panel is currently shown. */
|
|
19966
20664
|
isTodoPanelVisible() {
|
|
19967
20665
|
return this.todoPanelVisible;
|
|
19968
20666
|
}
|
|
20667
|
+
/** Whether the todo panel is showing the full list (headless-test hook). */
|
|
20668
|
+
isTodoPanelExpanded() {
|
|
20669
|
+
return this.todoExpanded;
|
|
20670
|
+
}
|
|
19969
20671
|
/**
|
|
19970
20672
|
* Rebuild the todo panel text: a border rule + `Todo` title (both indented
|
|
19971
|
-
* one cell) plus up to five rows
|
|
19972
|
-
* completed (strikethrough)
|
|
20673
|
+
* one cell) plus up to five rows by default (in_progress first, then
|
|
20674
|
+
* pending, then completed (strikethrough)); the full list when expanded
|
|
20675
|
+
* (fullscreen click on the panel toggles).
|
|
19973
20676
|
*/
|
|
19974
20677
|
renderTodoPanel() {
|
|
19975
20678
|
if (!this.todoPanelVisible) {
|
|
@@ -19981,15 +20684,16 @@ var TuiApp = class TuiApp {
|
|
|
19981
20684
|
...this.todoItems.filter((todo) => todo.status === "in_progress"),
|
|
19982
20685
|
...this.todoItems.filter((todo) => todo.status === "pending"),
|
|
19983
20686
|
...this.todoItems.filter((todo) => todo.status === "completed")
|
|
19984
|
-
]
|
|
20687
|
+
];
|
|
20688
|
+
const shown = this.todoExpanded ? ordered : ordered.slice(0, 5);
|
|
19985
20689
|
const width = Math.max(1, this.terminal.columns);
|
|
19986
20690
|
const border = color.border(` ${"─".repeat(Math.max(0, width - 2))} `);
|
|
19987
20691
|
const title = color.textStrong(" Todo");
|
|
19988
|
-
if (
|
|
20692
|
+
if (shown.length === 0) {
|
|
19989
20693
|
this.todoPanel.setText([border, title].join("\n"));
|
|
19990
20694
|
return;
|
|
19991
20695
|
}
|
|
19992
|
-
const lines =
|
|
20696
|
+
const lines = shown.map((todo) => {
|
|
19993
20697
|
const body = todo.status === "completed" ? `\x1b[9m${todo.content}\x1b[29m` : todo.content;
|
|
19994
20698
|
return `${mark(todo)} ${body}`;
|
|
19995
20699
|
});
|
|
@@ -20136,6 +20840,8 @@ var TuiApp = class TuiApp {
|
|
|
20136
20840
|
this.renderQueuePane();
|
|
20137
20841
|
this.syncExtensionState();
|
|
20138
20842
|
}
|
|
20843
|
+
/** Notice rows shown in full before the `+N more` fold (user rows are never folded). */
|
|
20844
|
+
static MAX_NOTICE_ROWS = 5;
|
|
20139
20845
|
/** Rebuild the queue pane text from the current inbox rows. */
|
|
20140
20846
|
renderQueuePane() {
|
|
20141
20847
|
const items = this.queueItems;
|
|
@@ -20146,12 +20852,20 @@ var TuiApp = class TuiApp {
|
|
|
20146
20852
|
}
|
|
20147
20853
|
const width = Math.max(1, this.terminal.columns);
|
|
20148
20854
|
const lines = [color.border(` ${"─".repeat(Math.max(0, width - 2))} `)];
|
|
20149
|
-
|
|
20150
|
-
|
|
20151
|
-
|
|
20152
|
-
|
|
20153
|
-
|
|
20154
|
-
|
|
20855
|
+
const userRows = items.filter((item) => item.notice !== true);
|
|
20856
|
+
const noticeRows = items.filter((item) => item.notice === true);
|
|
20857
|
+
const shownNotices = noticeRows.slice(0, TuiApp.MAX_NOTICE_ROWS);
|
|
20858
|
+
for (const item of userRows) {
|
|
20859
|
+
const truncated = truncateToWidth(item.text.replace(/\s+/g, " ").trim(), Math.max(1, width - visibleWidth("❯ ")), "…");
|
|
20860
|
+
lines.push(`${color.accent("❯")} ${truncated}`);
|
|
20861
|
+
}
|
|
20862
|
+
for (const item of shownNotices) {
|
|
20863
|
+
const truncated = truncateToWidth(item.text.replace(/\s+/g, " ").trim(), Math.max(1, width - visibleWidth("⏳ ")), "…");
|
|
20864
|
+
lines.push(`${color.textDim("⏳")} ${color.textDim(truncated)}`);
|
|
20865
|
+
}
|
|
20866
|
+
const folded = noticeRows.length - shownNotices.length;
|
|
20867
|
+
if (folded > 0) lines.push(color.textDim(truncateToWidth(` +${folded} more notices pending · they deliver as the next turn runs`, Math.max(1, width - 2), "…")));
|
|
20868
|
+
const hint = userRows.length > 0 ? "ctrl+s to steer all · alt+↑ to edit all" : "notices deliver after the current task · /tasks to view";
|
|
20155
20869
|
lines.push(color.textDim(truncateToWidth(` ${hint}`, Math.max(1, width - 2), "…")));
|
|
20156
20870
|
this.queuePane.setText(lines.join("\n"));
|
|
20157
20871
|
this.requestRender();
|
|
@@ -20536,7 +21250,8 @@ var TuiApp = class TuiApp {
|
|
|
20536
21250
|
header: options.header,
|
|
20537
21251
|
noMatchText: options.noMatchText,
|
|
20538
21252
|
enableSearch: options.enableSearch,
|
|
20539
|
-
initialQuery: options.initialQuery
|
|
21253
|
+
initialQuery: options.initialQuery,
|
|
21254
|
+
onAction: options.onAction
|
|
20540
21255
|
}, (value) => {
|
|
20541
21256
|
close();
|
|
20542
21257
|
onSelect(value);
|
|
@@ -20875,7 +21590,8 @@ var TuiApp = class TuiApp {
|
|
|
20875
21590
|
...question.detail !== void 0 ? { detail: question.detail } : {},
|
|
20876
21591
|
...question.options !== void 0 ? { options: question.options } : {},
|
|
20877
21592
|
...question.multiSelect !== void 0 ? { multiSelect: question.multiSelect } : {},
|
|
20878
|
-
...question.intent !== void 0 ? { intent: question.intent } : {}
|
|
21593
|
+
...question.intent !== void 0 ? { intent: question.intent } : {},
|
|
21594
|
+
...question.masked !== void 0 ? { masked: question.masked } : {}
|
|
20879
21595
|
})), (answers) => this.settleQuestions(state, answers), () => this.settleQuestions(state, void 0)),
|
|
20880
21596
|
suspendedOverlays: /* @__PURE__ */ new Set(),
|
|
20881
21597
|
resolve,
|
|
@@ -24475,6 +25191,7 @@ function providerOptionsFor(entries, readSection) {
|
|
|
24475
25191
|
route: "deepseek-official",
|
|
24476
25192
|
configured: true,
|
|
24477
25193
|
declared: false,
|
|
25194
|
+
namesCredential: true,
|
|
24478
25195
|
group: "configured",
|
|
24479
25196
|
settingsNs: "",
|
|
24480
25197
|
settingsPath: []
|
|
@@ -24486,13 +25203,15 @@ function providerOptionsFor(entries, readSection) {
|
|
|
24486
25203
|
const profile = entry.settingsPath.length === 0 ? section : getPath(section, entry.settingsPath);
|
|
24487
25204
|
const configured = profile !== void 0;
|
|
24488
25205
|
const namedRef = configured && typeof profile === "object" && profile !== null && typeof profile.apiKeyEnv === "string" ? profile.apiKeyEnv : void 0;
|
|
24489
|
-
const
|
|
25206
|
+
const namesCredential = namedRef !== void 0 && namedRef !== "";
|
|
25207
|
+
const ref = namesCredential ? namedRef : deriveKeyRef(entry.provider);
|
|
24490
25208
|
push({
|
|
24491
25209
|
label: entry.displayName,
|
|
24492
25210
|
ref,
|
|
24493
25211
|
route: entry.provider,
|
|
24494
25212
|
configured,
|
|
24495
25213
|
declared: entry.declared === true,
|
|
25214
|
+
namesCredential,
|
|
24496
25215
|
group: configured ? "configured" : entry.declared === true ? "custom" : "available",
|
|
24497
25216
|
settingsNs: entry.settingsNs,
|
|
24498
25217
|
settingsPath: [...entry.settingsPath]
|
|
@@ -24537,7 +25256,185 @@ function resolveCredentialArg(arg, options) {
|
|
|
24537
25256
|
if (route !== void 0) return route.ref;
|
|
24538
25257
|
if (/^[a-zA-Z][a-zA-Z0-9_]*$/.test(trimmed)) return trimmed.toUpperCase();
|
|
24539
25258
|
}
|
|
24540
|
-
|
|
25259
|
+
/** Map the seam's entries to /login targets, deriving the route from the
|
|
25260
|
+
* key's scope (llm-pi-ai flows address the provider route). */
|
|
25261
|
+
function authorizationTargets(entries) {
|
|
25262
|
+
return entries.map((entry) => ({
|
|
25263
|
+
kind: "authorization",
|
|
25264
|
+
route: credentialKeyScope(entry.key) === "llm-pi-ai" ? credentialKeyId(entry.key) : void 0,
|
|
25265
|
+
key: entry.key,
|
|
25266
|
+
label: entry.label,
|
|
25267
|
+
methods: entry.methods,
|
|
25268
|
+
inFlight: entry.inFlight
|
|
25269
|
+
}));
|
|
25270
|
+
}
|
|
25271
|
+
/** The flow target for a provider route, when one is registered. */
|
|
25272
|
+
function flowForRoute(targets, route) {
|
|
25273
|
+
return targets.find((target) => target.route === route);
|
|
25274
|
+
}
|
|
25275
|
+
/**
|
|
25276
|
+
* Merge the reference targets and the authorization flows into the /login
|
|
25277
|
+
* picker set, applying the rc.1 dedup rule per route:
|
|
25278
|
+
*
|
|
25279
|
+
* - explicit `apiKeyEnv` profile → the reference target wins (the flow is
|
|
25280
|
+
* NOT offered as a second default entry for the same route — requests
|
|
25281
|
+
* resolve the reference first and a "successful" flow login would leave
|
|
25282
|
+
* the reference unset, failing the next request);
|
|
25283
|
+
* - keyless route with a flow → the authorization target wins (no derived
|
|
25284
|
+
* `<ROUTE>_API_KEY` entry is offered as a default);
|
|
25285
|
+
* - keyless route without a flow → the derived reference fallback.
|
|
25286
|
+
*
|
|
25287
|
+
* Flows whose key maps no option route (foreign scopes, or catalog routes
|
|
25288
|
+
* the llm directory does not expose) are appended as standalone targets.
|
|
25289
|
+
*/
|
|
25290
|
+
function mergeLoginTargets(options, targets) {
|
|
25291
|
+
const flowByRoute = /* @__PURE__ */ new Map();
|
|
25292
|
+
const standalone = [];
|
|
25293
|
+
for (const target of targets) if (target.route !== void 0) flowByRoute.set(target.route, target);
|
|
25294
|
+
else standalone.push(target);
|
|
25295
|
+
const merged = [];
|
|
25296
|
+
const covered = /* @__PURE__ */ new Set();
|
|
25297
|
+
for (const option of options) {
|
|
25298
|
+
const flow = flowByRoute.get(option.route);
|
|
25299
|
+
merged.push(option.namesCredential || flow === void 0 ? {
|
|
25300
|
+
kind: "reference",
|
|
25301
|
+
route: option.route,
|
|
25302
|
+
label: option.label,
|
|
25303
|
+
ref: option.ref,
|
|
25304
|
+
configured: option.configured,
|
|
25305
|
+
declared: option.declared,
|
|
25306
|
+
namesCredential: option.namesCredential
|
|
25307
|
+
} : flow);
|
|
25308
|
+
covered.add(option.route);
|
|
25309
|
+
}
|
|
25310
|
+
for (const target of targets) if (target.route === void 0 || !covered.has(target.route)) merged.push(target);
|
|
25311
|
+
return merged;
|
|
25312
|
+
}
|
|
25313
|
+
/**
|
|
25314
|
+
* The one user-facing notice body: the message first, then the page to open
|
|
25315
|
+
* and the code to enter, each on its own line. Never truncated — the
|
|
25316
|
+
* output panel wraps, so the URL stays copyable. Notices never carry
|
|
25317
|
+
* secrets.
|
|
25318
|
+
*/
|
|
25319
|
+
function formatAuthorizationNotice(notice) {
|
|
25320
|
+
const lines = [notice.message];
|
|
25321
|
+
if (notice.url !== void 0 && notice.url !== "") lines.push("", "Open this page to continue:", notice.url);
|
|
25322
|
+
if (notice.code !== void 0 && notice.code !== "") lines.push("", `Code: ${notice.code}`);
|
|
25323
|
+
return lines.join("\n");
|
|
25324
|
+
}
|
|
25325
|
+
/**
|
|
25326
|
+
* Build the interaction half of one authorization attempt. One instance per
|
|
25327
|
+
* attempt: the notice panel is reused across progress notices, and
|
|
25328
|
+
* `close()` hides it when the attempt settles (the caller's finally).
|
|
25329
|
+
*
|
|
25330
|
+
* Prompt mapping:
|
|
25331
|
+
* - `text` and `secret` go through the question flow's free-text row;
|
|
25332
|
+
* `secret` is rendered masked (the real value never leaves the input's
|
|
25333
|
+
* memory and is never logged, put in history, or shown anywhere else);
|
|
25334
|
+
* - `select` goes through the picker and returns the chosen option's `id`,
|
|
25335
|
+
* never its label;
|
|
25336
|
+
* - the user closing the question is a decline → `AuthorizationDeclinedError`;
|
|
25337
|
+
* - the prompt's OWN signal withdrawing it is NOT a decline: the rejection
|
|
25338
|
+
* is passed through as-is so the flow can tell a refusal from a race.
|
|
25339
|
+
*/
|
|
25340
|
+
function createAuthorizationInteraction(app) {
|
|
25341
|
+
let noticeHandle;
|
|
25342
|
+
let noticeBody = "";
|
|
25343
|
+
const showNotice = (notice) => {
|
|
25344
|
+
noticeBody = formatAuthorizationNotice(notice);
|
|
25345
|
+
if (noticeHandle === void 0) noticeHandle = app.openOutputViewer({
|
|
25346
|
+
title: "Sign in",
|
|
25347
|
+
initial: noticeBody,
|
|
25348
|
+
refresh: () => noticeBody,
|
|
25349
|
+
onClose: () => {
|
|
25350
|
+
noticeHandle = void 0;
|
|
25351
|
+
}
|
|
25352
|
+
});
|
|
25353
|
+
};
|
|
25354
|
+
const close = () => {
|
|
25355
|
+
noticeHandle?.();
|
|
25356
|
+
noticeHandle = void 0;
|
|
25357
|
+
};
|
|
25358
|
+
return {
|
|
25359
|
+
interaction: {
|
|
25360
|
+
notify: (notice) => {
|
|
25361
|
+
showNotice(notice);
|
|
25362
|
+
},
|
|
25363
|
+
prompt: async (prompt) => {
|
|
25364
|
+
if (prompt.kind === "select") {
|
|
25365
|
+
const signal = prompt.signal;
|
|
25366
|
+
return await new Promise((resolve, reject) => {
|
|
25367
|
+
if (signal?.aborted === true) {
|
|
25368
|
+
reject(cancellationError("authorization prompt withdrawn"));
|
|
25369
|
+
return;
|
|
25370
|
+
}
|
|
25371
|
+
let settled = false;
|
|
25372
|
+
let handle;
|
|
25373
|
+
const cleanup = () => {
|
|
25374
|
+
if (signal !== void 0) signal.removeEventListener("abort", onAbort);
|
|
25375
|
+
};
|
|
25376
|
+
const onAbort = () => {
|
|
25377
|
+
if (settled) return;
|
|
25378
|
+
settled = true;
|
|
25379
|
+
cleanup();
|
|
25380
|
+
handle?.close?.();
|
|
25381
|
+
reject(cancellationError("authorization prompt withdrawn"));
|
|
25382
|
+
};
|
|
25383
|
+
if (signal !== void 0) signal.addEventListener("abort", onAbort, { once: true });
|
|
25384
|
+
handle = app.openPicker(prompt.options.map((option) => ({
|
|
25385
|
+
value: option.id,
|
|
25386
|
+
label: option.label,
|
|
25387
|
+
...option.description !== void 0 && option.description !== "" ? { description: option.description } : {}
|
|
25388
|
+
})), (value) => {
|
|
25389
|
+
if (settled) return;
|
|
25390
|
+
settled = true;
|
|
25391
|
+
cleanup();
|
|
25392
|
+
resolve(value);
|
|
25393
|
+
}, () => {
|
|
25394
|
+
if (settled) return;
|
|
25395
|
+
settled = true;
|
|
25396
|
+
cleanup();
|
|
25397
|
+
reject(new AuthorizationDeclinedError());
|
|
25398
|
+
}, {
|
|
25399
|
+
header: prompt.message,
|
|
25400
|
+
enableSearch: true
|
|
25401
|
+
});
|
|
25402
|
+
if (settled) handle?.close?.();
|
|
25403
|
+
});
|
|
25404
|
+
}
|
|
25405
|
+
try {
|
|
25406
|
+
const text = (await app.askQuestions([{
|
|
25407
|
+
id: "answer",
|
|
25408
|
+
question: prompt.message,
|
|
25409
|
+
...prompt.placeholder !== void 0 && prompt.placeholder !== "" ? { placeholder: prompt.placeholder } : {},
|
|
25410
|
+
...prompt.kind === "secret" ? { masked: true } : {}
|
|
25411
|
+
}], prompt.signal))[0]?.custom ?? "";
|
|
25412
|
+
if (text === "") throw new AuthorizationDeclinedError();
|
|
25413
|
+
return text;
|
|
25414
|
+
} catch (error) {
|
|
25415
|
+
if (prompt.signal?.aborted === true) throw error;
|
|
25416
|
+
throw new AuthorizationDeclinedError();
|
|
25417
|
+
}
|
|
25418
|
+
}
|
|
25419
|
+
},
|
|
25420
|
+
close
|
|
25421
|
+
};
|
|
25422
|
+
}
|
|
25423
|
+
/** The user-facing text for one authorization attempt failure, mapping the
|
|
25424
|
+
* seam's stable error taxonomy to copy (plan §15). `unknownErrorText` is
|
|
25425
|
+
* the safe fallback for provider/network errors (already passed through
|
|
25426
|
+
* safeErrorMessage by the caller). */
|
|
25427
|
+
function authorizationFailureText(error, unknownErrorText) {
|
|
25428
|
+
switch (error?.code) {
|
|
25429
|
+
case "NO_FLOW": return "login method is no longer available";
|
|
25430
|
+
case "UNKNOWN_METHOD": return "selected login method is no longer available";
|
|
25431
|
+
case "ALREADY_IN_FLIGHT": return "sign-in already in progress";
|
|
25432
|
+
case "NOT_COMMITTED": return "sign-in finished without storing a credential";
|
|
25433
|
+
case "DECLINED": return "login cancelled";
|
|
25434
|
+
default: return `sign-in failed: ${unknownErrorText}`;
|
|
25435
|
+
}
|
|
25436
|
+
}
|
|
25437
|
+
//#endregion
|
|
24541
25438
|
//#region src/skill-catalog.ts
|
|
24542
25439
|
/**
|
|
24543
25440
|
* The standing-scope skill catalog adapter: the SINGLE narrow seam between
|
|
@@ -24807,7 +25704,7 @@ function sortCommands(commands) {
|
|
|
24807
25704
|
//#region src/commands.ts
|
|
24808
25705
|
/**
|
|
24809
25706
|
* The TUI-owned slash commands (/exit /settings /sessions /skill /model
|
|
24810
|
-
* /new /tasks /preset /
|
|
25707
|
+
* /new /tasks /preset /search /title /copy /export
|
|
24811
25708
|
* /fork /status /login /logout /help), extracted from the runner's
|
|
24812
25709
|
* monolithic apply() so the registration surface is testable and the runner
|
|
24813
25710
|
* closure shrinks. Every command reads the live runner state through the
|
|
@@ -24879,6 +25776,9 @@ function presetDisplayText(preset) {
|
|
|
24879
25776
|
}
|
|
24880
25777
|
/** The sentinel picker value for the "add a brand-new provider" action row. */
|
|
24881
25778
|
const ADD_PROVIDER_VALUE = "\0add-provider";
|
|
25779
|
+
/** The sentinel prefix for an authorization-target picker row (a credential
|
|
25780
|
+
* key can never start with NUL, so route values cannot collide). */
|
|
25781
|
+
const AUTH_VALUE_PREFIX = "\0auth:";
|
|
24882
25782
|
/**
|
|
24883
25783
|
* M11: the /status extension-health rows (plan §16 — /status extension
|
|
24884
25784
|
* health). Reads the extension registries' live snapshots: contribution
|
|
@@ -24970,6 +25870,7 @@ function readProviderOptions(ctx) {
|
|
|
24970
25870
|
route: "deepseek-official",
|
|
24971
25871
|
configured: true,
|
|
24972
25872
|
declared: false,
|
|
25873
|
+
namesCredential: true,
|
|
24973
25874
|
group: "configured",
|
|
24974
25875
|
settingsNs: "",
|
|
24975
25876
|
settingsPath: []
|
|
@@ -24978,26 +25879,36 @@ function readProviderOptions(ctx) {
|
|
|
24978
25879
|
route: option.label,
|
|
24979
25880
|
configured: true,
|
|
24980
25881
|
declared: false,
|
|
25882
|
+
namesCredential: true,
|
|
24981
25883
|
group: "configured",
|
|
24982
25884
|
settingsNs: "llm-pi-ai",
|
|
24983
25885
|
settingsPath: ["providers", option.label]
|
|
24984
25886
|
});
|
|
24985
25887
|
}
|
|
24986
|
-
/** Build the picker rows
|
|
24987
|
-
* available
|
|
24988
|
-
*
|
|
24989
|
-
* Platform action row pinned last.
|
|
24990
|
-
|
|
25888
|
+
/** Build the merged /login picker rows: reference targets keep their
|
|
25889
|
+
* configured/available/custom groups (the fork renders a non-interactive
|
|
25890
|
+
* header row per group), authorization targets get their own group, and the
|
|
25891
|
+
* Add New Platform action row is pinned last. Authorization row values
|
|
25892
|
+
* carry the AUTH_VALUE_PREFIX so they can never collide with a route. */
|
|
25893
|
+
function mergedPickerRows(merged) {
|
|
24991
25894
|
const rows = [];
|
|
24992
25895
|
const groupLabels = {
|
|
24993
25896
|
configured: "configured",
|
|
24994
25897
|
available: "available · catalog",
|
|
24995
|
-
custom: "custom"
|
|
25898
|
+
custom: "custom",
|
|
25899
|
+
authorization: "sign in with provider"
|
|
24996
25900
|
};
|
|
24997
|
-
for (const
|
|
24998
|
-
|
|
24999
|
-
|
|
25000
|
-
|
|
25901
|
+
for (const target of merged) if (target.kind === "reference") {
|
|
25902
|
+
const group = target.configured ? "configured" : target.declared ? "custom" : "available";
|
|
25903
|
+
rows.push({
|
|
25904
|
+
value: target.route,
|
|
25905
|
+
label: `${target.label} (${target.ref})`,
|
|
25906
|
+
group: groupLabels[group]
|
|
25907
|
+
});
|
|
25908
|
+
} else rows.push({
|
|
25909
|
+
value: AUTH_VALUE_PREFIX + target.key,
|
|
25910
|
+
label: target.inFlight ? `${target.label} — sign-in in progress` : `${target.label} — sign in`,
|
|
25911
|
+
group: groupLabels.authorization
|
|
25001
25912
|
});
|
|
25002
25913
|
rows.push({
|
|
25003
25914
|
value: ADD_PROVIDER_VALUE,
|
|
@@ -25005,6 +25916,161 @@ function providerPickerRows(options) {
|
|
|
25005
25916
|
});
|
|
25006
25917
|
return rows;
|
|
25007
25918
|
}
|
|
25919
|
+
/** Recover an authorization target from a picked row value (the marker
|
|
25920
|
+
* prefix guarantees no collision with reference route values). */
|
|
25921
|
+
function targetFromPickerValue(merged, value) {
|
|
25922
|
+
if (!value.startsWith(AUTH_VALUE_PREFIX)) return void 0;
|
|
25923
|
+
const key = value.slice(6);
|
|
25924
|
+
for (const target of merged) if (target.kind === "authorization" && target.key === key) return target;
|
|
25925
|
+
}
|
|
25926
|
+
/** One-line summary of every merged target, for the unknown-target error. */
|
|
25927
|
+
function mergedTargetsSummary(merged) {
|
|
25928
|
+
return merged.map((target) => target.kind === "reference" ? `${target.label} (${target.ref})` : `${target.label} (provider sign-in)`).join(", ");
|
|
25929
|
+
}
|
|
25930
|
+
/**
|
|
25931
|
+
* Run one authorization attempt on the seam and report it. Method picking
|
|
25932
|
+
* (single method → direct; multiple → a picker), notice rendering and
|
|
25933
|
+
* prompts all live behind the interaction built here; the seam's stable
|
|
25934
|
+
* error taxonomy maps to user-facing copy (§15). On success, a catalog
|
|
25935
|
+
* route that is not configured yet gets a minimal keyless profile so the
|
|
25936
|
+
* runtime keeps reading the credential record (§12.1 — never an apiKeyEnv,
|
|
25937
|
+
* which would switch the request path back to a reference that is not set).
|
|
25938
|
+
*/
|
|
25939
|
+
async function runAuthorizationLogin(ctx, app, runner, target, options) {
|
|
25940
|
+
const authorization = ctx.get("authorization");
|
|
25941
|
+
if (authorization === void 0) return {
|
|
25942
|
+
kind: "error",
|
|
25943
|
+
text: "authorization service unavailable"
|
|
25944
|
+
};
|
|
25945
|
+
if (target.inFlight) return {
|
|
25946
|
+
kind: "error",
|
|
25947
|
+
text: `sign-in already in progress for ${target.label}`
|
|
25948
|
+
};
|
|
25949
|
+
let method = target.methods[0]?.id;
|
|
25950
|
+
if (method === void 0) return {
|
|
25951
|
+
kind: "error",
|
|
25952
|
+
text: `no sign-in method available for ${target.label}`
|
|
25953
|
+
};
|
|
25954
|
+
if (target.methods.length > 1) {
|
|
25955
|
+
const picked = await new Promise((resolve) => {
|
|
25956
|
+
app.openPicker(target.methods.map((candidate) => ({
|
|
25957
|
+
value: candidate.id,
|
|
25958
|
+
label: candidate.label
|
|
25959
|
+
})), (value) => resolve(value), () => resolve(void 0), {
|
|
25960
|
+
header: `Sign in method · ${target.label}`,
|
|
25961
|
+
enableSearch: false
|
|
25962
|
+
});
|
|
25963
|
+
});
|
|
25964
|
+
if (picked === void 0) return {
|
|
25965
|
+
kind: "error",
|
|
25966
|
+
text: "login cancelled"
|
|
25967
|
+
};
|
|
25968
|
+
method = picked;
|
|
25969
|
+
}
|
|
25970
|
+
const { interaction, close } = createAuthorizationInteraction(app);
|
|
25971
|
+
let outcome;
|
|
25972
|
+
try {
|
|
25973
|
+
outcome = await authorization.begin({
|
|
25974
|
+
key: target.key,
|
|
25975
|
+
method,
|
|
25976
|
+
interaction,
|
|
25977
|
+
signal: runner.signal
|
|
25978
|
+
});
|
|
25979
|
+
} catch (error) {
|
|
25980
|
+
if (runner.signal.aborted) return {
|
|
25981
|
+
kind: "error",
|
|
25982
|
+
text: "login cancelled"
|
|
25983
|
+
};
|
|
25984
|
+
if (error?.code === "NOT_COMMITTED") runner.diag.error("authorization", {
|
|
25985
|
+
key: target.key,
|
|
25986
|
+
error: safeErrorMessage(error)
|
|
25987
|
+
});
|
|
25988
|
+
return {
|
|
25989
|
+
kind: "error",
|
|
25990
|
+
text: authorizationFailureText(error, safeErrorMessage(error))
|
|
25991
|
+
};
|
|
25992
|
+
} finally {
|
|
25993
|
+
close();
|
|
25994
|
+
}
|
|
25995
|
+
if (outcome.status === "cancelled") return {
|
|
25996
|
+
kind: "error",
|
|
25997
|
+
text: "login cancelled"
|
|
25998
|
+
};
|
|
25999
|
+
const profileNote = await provisionKeylessProfile(ctx, runner, target, options);
|
|
26000
|
+
return {
|
|
26001
|
+
kind: "success",
|
|
26002
|
+
text: `signed in to ${target.label}${profileNote}`
|
|
26003
|
+
};
|
|
26004
|
+
}
|
|
26005
|
+
/**
|
|
26006
|
+
* After a successful authorization, write a MINIMAL keyless profile for a
|
|
26007
|
+
* catalog route that is not configured yet (§12.1). The record alone does
|
|
26008
|
+
* not make the route selectable — llm-pi-ai registers a route only when
|
|
26009
|
+
* its settings section names it — and the profile must NOT carry
|
|
26010
|
+
* apiKeyEnv, or the request path would switch back to a reference that was
|
|
26011
|
+
* never set. Hand-declared/custom routes are left to the add wizard
|
|
26012
|
+
* (§12.2). Any failure degrades silently (the sign-in itself succeeded).
|
|
26013
|
+
* @returns a user-facing note, or '' when nothing was provisioned.
|
|
26014
|
+
*/
|
|
26015
|
+
async function provisionKeylessProfile(ctx, runner, target, options) {
|
|
26016
|
+
if (target.route === void 0) return "";
|
|
26017
|
+
const option = options.find((candidate) => candidate.route === target.route);
|
|
26018
|
+
if (option === void 0 || option.configured || option.declared || option.settingsNs === "") return "";
|
|
26019
|
+
const settings = ctx.get("settings");
|
|
26020
|
+
if (settings === void 0) return "";
|
|
26021
|
+
try {
|
|
26022
|
+
await settings.mutate(option.settingsNs, [{
|
|
26023
|
+
op: "set",
|
|
26024
|
+
path: [...option.settingsPath],
|
|
26025
|
+
value: {}
|
|
26026
|
+
}]);
|
|
26027
|
+
return " — provider profile recorded";
|
|
26028
|
+
} catch (error) {
|
|
26029
|
+
runner.diag.warn("authorization", {
|
|
26030
|
+
key: target.key,
|
|
26031
|
+
note: "profile write failed",
|
|
26032
|
+
error: safeErrorMessage(error)
|
|
26033
|
+
});
|
|
26034
|
+
return "";
|
|
26035
|
+
}
|
|
26036
|
+
}
|
|
26037
|
+
/** Value prefixes for the /logout picker rows (no collision with a ref). */
|
|
26038
|
+
const LOGOUT_REF_VALUE = "\0ref:";
|
|
26039
|
+
const LOGOUT_RECORD_VALUE = "\0record:";
|
|
26040
|
+
/** Build the /logout picker rows: every stored credential record plus every
|
|
26041
|
+
* configured reference (presence only — a secret's value never leaves the
|
|
26042
|
+
* credentials service). Records are deduplicated by key and labelled with
|
|
26043
|
+
* the authorization flow's user-facing name when one owns the key (a
|
|
26044
|
+
* record row must say what signing out actually clears). */
|
|
26045
|
+
async function logoutPickerRows(credentials, options, targets) {
|
|
26046
|
+
const rows = [];
|
|
26047
|
+
const seenRefs = /* @__PURE__ */ new Set();
|
|
26048
|
+
for (const option of options) {
|
|
26049
|
+
if (seenRefs.has(option.ref)) continue;
|
|
26050
|
+
seenRefs.add(option.ref);
|
|
26051
|
+
try {
|
|
26052
|
+
if ((await credentials.describe(option.ref)).configured) rows.push({
|
|
26053
|
+
value: LOGOUT_REF_VALUE + option.ref,
|
|
26054
|
+
label: `${option.label} (${option.ref})`,
|
|
26055
|
+
group: "API keys"
|
|
26056
|
+
});
|
|
26057
|
+
} catch {}
|
|
26058
|
+
}
|
|
26059
|
+
const records = await credentials.listRecords();
|
|
26060
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
26061
|
+
for (const record of records) {
|
|
26062
|
+
if (seenKeys.has(record.key)) continue;
|
|
26063
|
+
seenKeys.add(record.key);
|
|
26064
|
+
const owner = targets.find((target) => target.key === record.key);
|
|
26065
|
+
const label = owner !== void 0 ? `${owner.label} — stored credential${record.kind === void 0 ? "" : ` (${record.kind})`}` : `${record.key}${record.kind === void 0 ? "" : ` (${record.kind})`}`;
|
|
26066
|
+
rows.push({
|
|
26067
|
+
value: LOGOUT_RECORD_VALUE + record.key,
|
|
26068
|
+
label,
|
|
26069
|
+
group: "stored credentials"
|
|
26070
|
+
});
|
|
26071
|
+
}
|
|
26072
|
+
return rows;
|
|
26073
|
+
}
|
|
25008
26074
|
/** Run the add-provider wizard: collect route/api/baseURL/displayName/key
|
|
25009
26075
|
* through question flows, probe the endpoint for its models (falling back to
|
|
25010
26076
|
* hand entry), review, then persist the profile + credential.
|
|
@@ -25252,18 +26318,43 @@ function registerTuiCommands(runner, initial) {
|
|
|
25252
26318
|
}
|
|
25253
26319
|
};
|
|
25254
26320
|
refreshCompletions();
|
|
26321
|
+
/**
|
|
26322
|
+
* Register one TUI command plus its aliases (kimi parity: an alias is
|
|
26323
|
+
* another NAME of the same logical command). Every alias registers with
|
|
26324
|
+
* the host commands service — the shared handler by default, or its own
|
|
26325
|
+
* handler when the alias keeps a fast path (e.g. /resume's direct-resume
|
|
26326
|
+
* lookup) — so host dispatch, the completion catalog (aliases are
|
|
26327
|
+
* searchable: typing `resume` completes `/resume`) and the busy-Enter
|
|
26328
|
+
* gate all see it, while the command surface lists one logical command
|
|
26329
|
+
* and the docs mark the alias.
|
|
26330
|
+
* @param spec - the primary command; `aliases` register with the shared
|
|
26331
|
+
* handler unless `aliasHandlers` overrides one.
|
|
26332
|
+
*/
|
|
26333
|
+
const registerTuiCommand = (spec) => {
|
|
26334
|
+
commands.register({
|
|
26335
|
+
name: spec.name,
|
|
26336
|
+
description: spec.description,
|
|
26337
|
+
...spec.input === void 0 ? {} : { input: spec.input },
|
|
26338
|
+
handler: spec.handler
|
|
26339
|
+
});
|
|
26340
|
+
for (const alias of spec.aliases ?? []) {
|
|
26341
|
+
const handler = spec.aliasHandlers?.[alias] ?? spec.handler;
|
|
26342
|
+
commands.register({
|
|
26343
|
+
name: alias,
|
|
26344
|
+
description: `${spec.description} (alias of /${spec.name})`,
|
|
26345
|
+
...spec.input === void 0 ? {} : { input: spec.input },
|
|
26346
|
+
handler
|
|
26347
|
+
});
|
|
26348
|
+
}
|
|
26349
|
+
};
|
|
25255
26350
|
const exitHandler = () => {
|
|
25256
26351
|
runner.requestExit();
|
|
25257
26352
|
return { kind: "success" };
|
|
25258
26353
|
};
|
|
25259
|
-
|
|
26354
|
+
registerTuiCommand({
|
|
25260
26355
|
name: "exit",
|
|
25261
26356
|
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",
|
|
26357
|
+
aliases: ["quit"],
|
|
25267
26358
|
handler: exitHandler
|
|
25268
26359
|
});
|
|
25269
26360
|
commands.register({
|
|
@@ -25337,6 +26428,13 @@ function registerTuiCommands(runner, initial) {
|
|
|
25337
26428
|
currentValue: tuiSettings?.get().busyEnter ?? "queue",
|
|
25338
26429
|
values: ["queue", "steer"]
|
|
25339
26430
|
},
|
|
26431
|
+
{
|
|
26432
|
+
id: "local-shell-sandbox",
|
|
26433
|
+
label: "Local shell sandbox",
|
|
26434
|
+
description: "! / !! commands run outside the dsh sandbox (bypass, default) or under the sandbox policy",
|
|
26435
|
+
currentValue: tuiSettings?.get().localShellSandbox ?? "bypass",
|
|
26436
|
+
values: ["bypass", "sandbox"]
|
|
26437
|
+
},
|
|
25340
26438
|
{
|
|
25341
26439
|
id: "fullscreen",
|
|
25342
26440
|
label: "Fullscreen",
|
|
@@ -25464,6 +26562,14 @@ function registerTuiCommands(runner, initial) {
|
|
|
25464
26562
|
busyEnter: value
|
|
25465
26563
|
}), { notify: true });
|
|
25466
26564
|
}
|
|
26565
|
+
} else if (id === "local-shell-sandbox") {
|
|
26566
|
+
if (value === "bypass" || value === "sandbox") {
|
|
26567
|
+
const settings = tuiSettings;
|
|
26568
|
+
if (settings !== void 0) detach("settings local shell sandbox write", () => settings.replace({
|
|
26569
|
+
...settings.get(),
|
|
26570
|
+
localShellSandbox: value
|
|
26571
|
+
}), { notify: true });
|
|
26572
|
+
}
|
|
25467
26573
|
} else if (id === "fullscreen") {
|
|
25468
26574
|
if (value === "off" || value === "on") app.setFullscreen(value === "on");
|
|
25469
26575
|
}
|
|
@@ -25538,17 +26644,13 @@ function registerTuiCommands(runner, initial) {
|
|
|
25538
26644
|
});
|
|
25539
26645
|
return { kind: "success" };
|
|
25540
26646
|
};
|
|
25541
|
-
|
|
26647
|
+
registerTuiCommand({
|
|
25542
26648
|
name: "sessions",
|
|
25543
26649
|
description: "List, search, and switch persisted sessions",
|
|
25544
26650
|
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) => {
|
|
26651
|
+
handler: (invocation) => openSessionPicker(invocation, "sessions"),
|
|
26652
|
+
aliases: ["resume"],
|
|
26653
|
+
aliasHandlers: { resume: async (invocation) => {
|
|
25552
26654
|
const raw = invocation.rawInput.trim();
|
|
25553
26655
|
if (raw !== "") {
|
|
25554
26656
|
const currentId = runner.liveAgent?.session.id;
|
|
@@ -25571,7 +26673,7 @@ function registerTuiCommands(runner, initial) {
|
|
|
25571
26673
|
}
|
|
25572
26674
|
}
|
|
25573
26675
|
return openSessionPicker(invocation, "resume");
|
|
25574
|
-
}
|
|
26676
|
+
} }
|
|
25575
26677
|
});
|
|
25576
26678
|
/** The workspace the live session runs in; fallback to the TUI's cwd. */
|
|
25577
26679
|
const sessionCwd = (agent) => agent.session.header.cwd ?? cwd;
|
|
@@ -25957,129 +27059,13 @@ function registerTuiCommands(runner, initial) {
|
|
|
25957
27059
|
};
|
|
25958
27060
|
}
|
|
25959
27061
|
});
|
|
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({
|
|
27062
|
+
registerTuiCommand({
|
|
26058
27063
|
name: "tasks",
|
|
26059
|
-
description: "
|
|
27064
|
+
description: "Browse background jobs and subagents for this session (search filters rows)",
|
|
27065
|
+
aliases: ["subagents"],
|
|
26060
27066
|
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
|
-
});
|
|
27067
|
+
await requireAgent();
|
|
27068
|
+
runner.openTasksBrowser();
|
|
26083
27069
|
return { kind: "success" };
|
|
26084
27070
|
}
|
|
26085
27071
|
});
|
|
@@ -26271,56 +27257,6 @@ function registerTuiCommands(runner, initial) {
|
|
|
26271
27257
|
return { kind: "success" };
|
|
26272
27258
|
}
|
|
26273
27259
|
});
|
|
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
27260
|
commands.register({
|
|
26325
27261
|
name: "search",
|
|
26326
27262
|
description: "Search persisted sessions for text and switch to a hit",
|
|
@@ -26412,16 +27348,11 @@ function registerTuiCommands(runner, initial) {
|
|
|
26412
27348
|
};
|
|
26413
27349
|
}
|
|
26414
27350
|
};
|
|
26415
|
-
|
|
27351
|
+
registerTuiCommand({
|
|
26416
27352
|
name: "title",
|
|
26417
27353
|
description: "Set the session title; without an argument, regenerate it from the conversation (overwrites the current title)",
|
|
26418
27354
|
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>" },
|
|
27355
|
+
aliases: ["rename"],
|
|
26425
27356
|
handler: titleHandler
|
|
26426
27357
|
});
|
|
26427
27358
|
commands.register({
|
|
@@ -26561,7 +27492,7 @@ function registerTuiCommands(runner, initial) {
|
|
|
26561
27492
|
});
|
|
26562
27493
|
commands.register({
|
|
26563
27494
|
name: "login",
|
|
26564
|
-
description: "
|
|
27495
|
+
description: "Sign in — deepseek official or an llm-pi-ai provider route (API key or provider-native flow)",
|
|
26565
27496
|
input: { hint: "[<route|env-var>]" },
|
|
26566
27497
|
handler: async (invocation) => {
|
|
26567
27498
|
const credentials = ctx.get("credentials");
|
|
@@ -26569,39 +27500,51 @@ function registerTuiCommands(runner, initial) {
|
|
|
26569
27500
|
kind: "error",
|
|
26570
27501
|
text: "credentials service unavailable"
|
|
26571
27502
|
};
|
|
27503
|
+
const authorization = ctx.get("authorization");
|
|
26572
27504
|
const options = readProviderOptions(ctx);
|
|
27505
|
+
const targets = authorizationTargets(authorization?.list() ?? []);
|
|
27506
|
+
const merged = mergeLoginTargets(options, targets);
|
|
26573
27507
|
const arg = invocation.rawInput.trim();
|
|
26574
27508
|
let route;
|
|
26575
27509
|
let ref;
|
|
27510
|
+
let target;
|
|
26576
27511
|
if (arg !== "") {
|
|
26577
27512
|
const known = resolveCredentialArg(arg, options);
|
|
26578
27513
|
if (known !== void 0) {
|
|
26579
27514
|
const option = options.find((candidate) => candidate.ref === known);
|
|
26580
27515
|
if (option !== void 0) {
|
|
26581
|
-
|
|
26582
|
-
|
|
27516
|
+
const flow = option.route !== "deepseek-official" ? flowForRoute(targets, option.route) : void 0;
|
|
27517
|
+
if (flow !== void 0 && option.namesCredential === false) target = flow;
|
|
27518
|
+
else {
|
|
27519
|
+
route = option.route;
|
|
27520
|
+
ref = option.ref;
|
|
27521
|
+
}
|
|
26583
27522
|
} else ref = known;
|
|
26584
27523
|
} else if (ROUTE_PATTERN.test(arg)) {
|
|
26585
|
-
const
|
|
26586
|
-
if (
|
|
26587
|
-
|
|
26588
|
-
|
|
26589
|
-
|
|
26590
|
-
|
|
26591
|
-
|
|
26592
|
-
|
|
26593
|
-
|
|
26594
|
-
|
|
26595
|
-
|
|
26596
|
-
|
|
26597
|
-
|
|
27524
|
+
const flow = flowForRoute(targets, arg);
|
|
27525
|
+
if (flow !== void 0) target = flow;
|
|
27526
|
+
else {
|
|
27527
|
+
const outcome = await askAddProvider(ctx, app, runner.signal, arg);
|
|
27528
|
+
if (outcome.kind === "cancelled") return {
|
|
27529
|
+
kind: "error",
|
|
27530
|
+
text: "add provider cancelled"
|
|
27531
|
+
};
|
|
27532
|
+
if (outcome.kind === "error") return {
|
|
27533
|
+
kind: "error",
|
|
27534
|
+
text: outcome.text
|
|
27535
|
+
};
|
|
27536
|
+
return {
|
|
27537
|
+
kind: "success",
|
|
27538
|
+
text: outcome.text
|
|
27539
|
+
};
|
|
27540
|
+
}
|
|
26598
27541
|
} else return {
|
|
26599
27542
|
kind: "error",
|
|
26600
|
-
text: `unknown credential target "${arg}" — ${
|
|
27543
|
+
text: `unknown credential target "${arg}" — ${mergedTargetsSummary(merged)}`
|
|
26601
27544
|
};
|
|
26602
|
-
} else if (
|
|
27545
|
+
} else if (merged.length > 1) {
|
|
26603
27546
|
const picked = await new Promise((resolve) => {
|
|
26604
|
-
app.openPicker(
|
|
27547
|
+
app.openPicker(mergedPickerRows(merged), (value) => resolve(value), () => resolve(void 0), {
|
|
26605
27548
|
enableSearch: true,
|
|
26606
27549
|
header: "login · providers",
|
|
26607
27550
|
noMatchText: " no matching providers",
|
|
@@ -26629,8 +27572,22 @@ function registerTuiCommands(runner, initial) {
|
|
|
26629
27572
|
text: outcome.text
|
|
26630
27573
|
};
|
|
26631
27574
|
}
|
|
26632
|
-
|
|
26633
|
-
|
|
27575
|
+
const authTarget = targetFromPickerValue(merged, picked);
|
|
27576
|
+
if (authTarget !== void 0) target = authTarget;
|
|
27577
|
+
else route = picked;
|
|
27578
|
+
} else {
|
|
27579
|
+
const only = merged[0];
|
|
27580
|
+
if (only === void 0) return {
|
|
27581
|
+
kind: "error",
|
|
27582
|
+
text: "no credential targets available"
|
|
27583
|
+
};
|
|
27584
|
+
if (only.kind === "authorization") target = only;
|
|
27585
|
+
else {
|
|
27586
|
+
route = only.route;
|
|
27587
|
+
ref = only.ref;
|
|
27588
|
+
}
|
|
27589
|
+
}
|
|
27590
|
+
if (target !== void 0) return runAuthorizationLogin(ctx, app, runner, target, options);
|
|
26634
27591
|
if (route === void 0 && ref === void 0) return {
|
|
26635
27592
|
kind: "error",
|
|
26636
27593
|
text: "no credential targets available"
|
|
@@ -26641,7 +27598,8 @@ function registerTuiCommands(runner, initial) {
|
|
|
26641
27598
|
try {
|
|
26642
27599
|
const key = (await app.askQuestions([{
|
|
26643
27600
|
id: "key",
|
|
26644
|
-
question: `Paste the API key for ${label}
|
|
27601
|
+
question: `Paste the API key for ${label}:`,
|
|
27602
|
+
masked: true
|
|
26645
27603
|
}]))[0]?.custom ?? "";
|
|
26646
27604
|
if (key === "") return {
|
|
26647
27605
|
kind: "error",
|
|
@@ -26662,7 +27620,7 @@ function registerTuiCommands(runner, initial) {
|
|
|
26662
27620
|
});
|
|
26663
27621
|
commands.register({
|
|
26664
27622
|
name: "logout",
|
|
26665
|
-
description: "Clear a stored
|
|
27623
|
+
description: "Clear a stored credential — deepseek official or an llm-pi-ai provider route (API key or stored record)",
|
|
26666
27624
|
input: { hint: "[<route|env-var>]" },
|
|
26667
27625
|
handler: async (invocation) => {
|
|
26668
27626
|
const credentials = ctx.get("credentials");
|
|
@@ -26670,24 +27628,72 @@ function registerTuiCommands(runner, initial) {
|
|
|
26670
27628
|
kind: "error",
|
|
26671
27629
|
text: "credentials service unavailable"
|
|
26672
27630
|
};
|
|
27631
|
+
const authorization = ctx.get("authorization");
|
|
26673
27632
|
const options = readProviderOptions(ctx);
|
|
27633
|
+
const targets = authorizationTargets(authorization?.list() ?? []);
|
|
26674
27634
|
const arg = invocation.rawInput.trim();
|
|
26675
27635
|
if (arg !== "") {
|
|
26676
27636
|
const resolved = resolveCredentialArg(arg, options);
|
|
26677
|
-
if (resolved
|
|
27637
|
+
if (resolved !== void 0) {
|
|
27638
|
+
const option = options.find((candidate) => candidate.ref === resolved);
|
|
27639
|
+
const flow = option !== void 0 && option.route !== "deepseek-official" && !option.namesCredential ? flowForRoute(targets, option.route) : void 0;
|
|
27640
|
+
if (flow !== void 0) {
|
|
27641
|
+
await credentials.deleteRecord(flow.key);
|
|
27642
|
+
return {
|
|
27643
|
+
kind: "success",
|
|
27644
|
+
text: `${flow.label} signed out locally — stored credential cleared`
|
|
27645
|
+
};
|
|
27646
|
+
}
|
|
27647
|
+
await credentials.unset(resolved);
|
|
27648
|
+
return {
|
|
27649
|
+
kind: "success",
|
|
27650
|
+
text: `${resolved} cleared`
|
|
27651
|
+
};
|
|
27652
|
+
}
|
|
27653
|
+
const flow = flowForRoute(targets, arg.toLowerCase());
|
|
27654
|
+
if (flow !== void 0) {
|
|
27655
|
+
await credentials.deleteRecord(flow.key);
|
|
27656
|
+
return {
|
|
27657
|
+
kind: "success",
|
|
27658
|
+
text: `${flow.label} signed out locally — stored credential cleared`
|
|
27659
|
+
};
|
|
27660
|
+
}
|
|
27661
|
+
return {
|
|
26678
27662
|
kind: "error",
|
|
26679
|
-
text: `unknown credential target "${arg}" — ${
|
|
27663
|
+
text: `unknown credential target "${arg}" — ${mergedTargetsSummary(mergeLoginTargets(options, targets))}`
|
|
26680
27664
|
};
|
|
26681
|
-
|
|
27665
|
+
}
|
|
27666
|
+
const rows = await logoutPickerRows(credentials, options, targets);
|
|
27667
|
+
if (rows.length === 0) return {
|
|
27668
|
+
kind: "error",
|
|
27669
|
+
text: "nothing to sign out"
|
|
27670
|
+
};
|
|
27671
|
+
const picked = await new Promise((resolve) => {
|
|
27672
|
+
app.openPicker(rows, (value) => resolve(value), () => resolve(void 0), {
|
|
27673
|
+
enableSearch: true,
|
|
27674
|
+
header: "logout · credentials",
|
|
27675
|
+
noMatchText: " nothing to sign out",
|
|
27676
|
+
width: 76,
|
|
27677
|
+
maxHeight: 26,
|
|
27678
|
+
showHint: true
|
|
27679
|
+
});
|
|
27680
|
+
});
|
|
27681
|
+
if (picked === void 0) return {
|
|
27682
|
+
kind: "error",
|
|
27683
|
+
text: "logout cancelled"
|
|
27684
|
+
};
|
|
27685
|
+
if (picked.startsWith(LOGOUT_RECORD_VALUE)) {
|
|
27686
|
+
await credentials.deleteRecord(picked.slice(8));
|
|
26682
27687
|
return {
|
|
26683
27688
|
kind: "success",
|
|
26684
|
-
text: `${
|
|
27689
|
+
text: `${picked.slice(8)} signed out locally — stored credential cleared`
|
|
26685
27690
|
};
|
|
26686
27691
|
}
|
|
26687
|
-
|
|
27692
|
+
const targetRef = picked.slice(5);
|
|
27693
|
+
await credentials.unset(targetRef);
|
|
26688
27694
|
return {
|
|
26689
27695
|
kind: "success",
|
|
26690
|
-
text: `${
|
|
27696
|
+
text: `${targetRef} cleared`
|
|
26691
27697
|
};
|
|
26692
27698
|
}
|
|
26693
27699
|
});
|
|
@@ -26918,6 +27924,33 @@ function appendHistoryLine(file, text, lastContent) {
|
|
|
26918
27924
|
return true;
|
|
26919
27925
|
}
|
|
26920
27926
|
//#endregion
|
|
27927
|
+
//#region src/dsh-version.ts
|
|
27928
|
+
/**
|
|
27929
|
+
* The installed dsh (DeepSeek Harness CLI) version, resolved from the
|
|
27930
|
+
* launcher's real path: `process.argv[1]` is the `dsh` bin, whose realpath
|
|
27931
|
+
* walks up to the `@deepseek-ai/dsh/package.json` that owns it. The header
|
|
27932
|
+
* and the welcome card show the harness the TUI runs on, not this bundle's
|
|
27933
|
+
* own patch level. Undefined when the launcher path is unreadable.
|
|
27934
|
+
* @module @xmoon76/dsh-pi-tui/dsh-version
|
|
27935
|
+
*/
|
|
27936
|
+
/** @returns the installed dsh version string, or undefined. */
|
|
27937
|
+
function dshVersion() {
|
|
27938
|
+
const bin = process.argv[1];
|
|
27939
|
+
if (bin === void 0) return void 0;
|
|
27940
|
+
try {
|
|
27941
|
+
let dir = dirname(realpathSync(bin));
|
|
27942
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
27943
|
+
try {
|
|
27944
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
27945
|
+
if (pkg.name === "@deepseek-ai/dsh" && typeof pkg.version === "string") return pkg.version;
|
|
27946
|
+
} catch {}
|
|
27947
|
+
const parent = dirname(dir);
|
|
27948
|
+
if (parent === dir) return void 0;
|
|
27949
|
+
dir = parent;
|
|
27950
|
+
}
|
|
27951
|
+
} catch {}
|
|
27952
|
+
}
|
|
27953
|
+
//#endregion
|
|
26921
27954
|
//#region src/exit.ts
|
|
26922
27955
|
/**
|
|
26923
27956
|
* The TUI exit contract: flush the live session with a HARD timeout, record
|
|
@@ -27460,6 +28493,17 @@ function shellCommandOf(text) {
|
|
|
27460
28493
|
return text.replace(/^!+/, "").trim();
|
|
27461
28494
|
}
|
|
27462
28495
|
/**
|
|
28496
|
+
* The local-shell sandbox preference for user-typed `!`/`!!` commands:
|
|
28497
|
+
* 'sandbox' routes them through the dsh shell capability's policy,
|
|
28498
|
+
* anything else (including an absent settings document) is 'bypass' — the
|
|
28499
|
+
* pi/kimi default that runs the user's own commands outside the sandbox
|
|
28500
|
+
* (the sandbox guards the model's autonomous commands, not the user's).
|
|
28501
|
+
* @param settings - the TUI settings document's fields, when present.
|
|
28502
|
+
*/
|
|
28503
|
+
function localShellSandboxPreferenceOf(settings) {
|
|
28504
|
+
return settings?.localShellSandbox === "sandbox" ? "sandbox" : "bypass";
|
|
28505
|
+
}
|
|
28506
|
+
/**
|
|
27463
28507
|
* The model-facing submission text: the command echoed `$`-style (kimi
|
|
27464
28508
|
* ShellExecution parity) followed by the settled card result (output +
|
|
27465
28509
|
* `[exit N]` / truncation lines).
|
|
@@ -27678,82 +28722,6 @@ function formatBytes(bytes) {
|
|
|
27678
28722
|
return `${(bytes / 1048576).toFixed(1)} MiB`;
|
|
27679
28723
|
}
|
|
27680
28724
|
//#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
28725
|
//#region src/skill-catalog-refresh.ts
|
|
27758
28726
|
/** Whether two targets are the same owner (no transition needed). The
|
|
27759
28727
|
* preset identity is the caller-provided presetId compared EXACTLY —
|
|
@@ -28241,6 +29209,19 @@ const inject = [
|
|
|
28241
29209
|
TUI_STARTUP_SERVICE
|
|
28242
29210
|
];
|
|
28243
29211
|
const Config = z.object({ sessionId: z.string() });
|
|
29212
|
+
/**
|
|
29213
|
+
* Wire the credential surface refresh to the dsh 0.1.1-rc.1 credential
|
|
29214
|
+
* events. The rc.1 release split the credential update event into the
|
|
29215
|
+
* reference half and the durable-record half; both change what the footer
|
|
29216
|
+
* model row and the welcome card show (a /login /logout, an external
|
|
29217
|
+
* .credentials.yaml edit, or an authorization flow committing a record), so
|
|
29218
|
+
* they share one best-effort callback. The callback must not read secrets
|
|
29219
|
+
* or mutate providers — it only re-reads describe-level state.
|
|
29220
|
+
*/
|
|
29221
|
+
function registerCredentialSurfaceRefresh(ctx, refresh) {
|
|
29222
|
+
ctx.on("credentials/reference-updated", refresh);
|
|
29223
|
+
ctx.on("credentials/record-updated", refresh);
|
|
29224
|
+
}
|
|
28244
29225
|
/** Display window in turns; older turns collapse into a summary entry. */
|
|
28245
29226
|
const WINDOW_TURNS = 15;
|
|
28246
29227
|
/** Coalesced repaint interval for streaming events, in ms. */
|
|
@@ -28294,7 +29275,6 @@ const LOCAL_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
28294
29275
|
"model",
|
|
28295
29276
|
"new",
|
|
28296
29277
|
"preset",
|
|
28297
|
-
"queue",
|
|
28298
29278
|
"quit",
|
|
28299
29279
|
"reload",
|
|
28300
29280
|
"rename",
|
|
@@ -28431,15 +29411,30 @@ function subagentJobTranscriptId(snapshot) {
|
|
|
28431
29411
|
/** Viewer body for a subagent job with no uniquely matched child. */
|
|
28432
29412
|
function subagentJobViewHint(status, detail) {
|
|
28433
29413
|
return [
|
|
28434
|
-
`status: ${status}${status === "running" || status === "stopping" ? " — running in the background; its transcript updates live in /
|
|
29414
|
+
`status: ${status}${status === "running" || status === "stopping" ? " — running in the background; its transcript updates live in /tasks" : ` — this subagent finished${detail === void 0 ? "" : ` (${detail})`}`}`,
|
|
28435
29415
|
"",
|
|
28436
29416
|
"The job record does not carry the child session id, so this job cannot",
|
|
28437
29417
|
"be matched to its child from the task browser (a same-label foreground",
|
|
28438
|
-
"run would be indistinguishable). Open /
|
|
29418
|
+
"run would be indistinguishable). Open /tasks and pick the child by",
|
|
28439
29419
|
"its label to read the transcript."
|
|
28440
29420
|
].join("\n");
|
|
28441
29421
|
}
|
|
28442
29422
|
/**
|
|
29423
|
+
* Whether an inbox message is USER-ORIGIN input — the queue pane's steerable
|
|
29424
|
+
* `❯` rows. Everything else is injected context, not the user's own queued
|
|
29425
|
+
* input, and must never read as one: plugin notices (background-job
|
|
29426
|
+
* completions), `subagent-report` relays (a child's active report, e.g.
|
|
29427
|
+
* "Background subagent X reported:"), injected skill/agent instructions,
|
|
29428
|
+
* goal messages. The web makes the same cut (`placement: source.kind ===
|
|
29429
|
+
* 'user' ? 'steering' : 'context'`), and this deployment's queue pane has
|
|
29430
|
+
* the same rule: only user-origin rows are steerable. A sourceless row
|
|
29431
|
+
* (undefined) is treated as user input — plain rows never carry a source.
|
|
29432
|
+
* @param source - the message source projection, or undefined for a plain row.
|
|
29433
|
+
*/
|
|
29434
|
+
function isUserQueueInput(source) {
|
|
29435
|
+
return source === void 0 || source.kind === "user";
|
|
29436
|
+
}
|
|
29437
|
+
/**
|
|
28443
29438
|
* Whether a plain submitted draft is the quit word: exactly `exit` (trimmed,
|
|
28444
29439
|
* lowercase). The runner intercepts this BEFORE any session creation or
|
|
28445
29440
|
* submission (shell muscle memory); anything else — `exit!`, `Exit`, or a
|
|
@@ -28511,7 +29506,7 @@ function foldQueueRows(messages, mode, notified) {
|
|
|
28511
29506
|
id: message.id,
|
|
28512
29507
|
text: textOf(message.content),
|
|
28513
29508
|
mode,
|
|
28514
|
-
notice: source
|
|
29509
|
+
notice: !isUserQueueInput(source)
|
|
28515
29510
|
});
|
|
28516
29511
|
}
|
|
28517
29512
|
return {
|
|
@@ -28520,33 +29515,10 @@ function foldQueueRows(messages, mode, notified) {
|
|
|
28520
29515
|
};
|
|
28521
29516
|
}
|
|
28522
29517
|
/**
|
|
28523
|
-
* The installed dsh version (e.g. `0.1.0-rc.6`), resolved from the launcher's
|
|
28524
|
-
* real path: `process.argv[1]` is the `dsh` bin, whose realpath walks up to
|
|
28525
|
-
* the `@deepseek-ai/dsh/package.json` that owns it. The version the welcome
|
|
28526
|
-
* card shows is the harness the TUI runs on, not this bundle's own patch
|
|
28527
|
-
* level. Undefined when the launcher path is unreadable.
|
|
28528
|
-
* @returns the installed dsh version string, or undefined.
|
|
28529
|
-
*/
|
|
28530
|
-
function dshVersion() {
|
|
28531
|
-
const bin = process.argv[1];
|
|
28532
|
-
if (bin === void 0) return void 0;
|
|
28533
|
-
try {
|
|
28534
|
-
let dir = dirname(realpathSync(bin));
|
|
28535
|
-
for (let depth = 0; depth < 8; depth += 1) {
|
|
28536
|
-
try {
|
|
28537
|
-
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
28538
|
-
if (pkg.name === "@deepseek-ai/dsh" && typeof pkg.version === "string") return pkg.version;
|
|
28539
|
-
} catch {}
|
|
28540
|
-
const parent = dirname(dir);
|
|
28541
|
-
if (parent === dir) return void 0;
|
|
28542
|
-
dir = parent;
|
|
28543
|
-
}
|
|
28544
|
-
} catch {}
|
|
28545
|
-
}
|
|
28546
|
-
/**
|
|
28547
29518
|
* The bundle's own version, read from package.json at runtime so the welcome
|
|
28548
29519
|
* card never drifts from the shipped version. The DISPLAYED version prefers
|
|
28549
|
-
* the installed dsh version (`dshVersion`
|
|
29520
|
+
* the installed dsh version (`dshVersion` — shared with the header badge
|
|
29521
|
+
* via src/dsh-version.ts), falling back to this one.
|
|
28550
29522
|
* @returns the version string, or a fallback when the file is unreadable.
|
|
28551
29523
|
*/
|
|
28552
29524
|
function packageVersion() {
|
|
@@ -29292,9 +30264,11 @@ function apply(ctx, config) {
|
|
|
29292
30264
|
/**
|
|
29293
30265
|
* The LIVE session's workspace: each session carries its own header cwd
|
|
29294
30266
|
* (fixed at creation, e.g. a session birthed by the web in another
|
|
29295
|
-
* directory). The footer/welcome/completions
|
|
29296
|
-
* session switch
|
|
29297
|
-
*
|
|
30267
|
+
* directory). The footer/welcome/completions AND `!`/`!!` shell runs
|
|
30268
|
+
* follow THIS cwd, so a session switch moves the whole surface with the
|
|
30269
|
+
* session (pi parity: `executeBash` runs in the session cwd) and a
|
|
30270
|
+
* shell command executes where the completions suggest files; `cwd`
|
|
30271
|
+
* (the process cwd) stays for launch-relative concerns (/export paths).
|
|
29298
30272
|
*/
|
|
29299
30273
|
const sessionCwd = () => liveAgent?.session.header.cwd ?? cwd;
|
|
29300
30274
|
/** The footer model label: the live selection (with effort) when one exists. */
|
|
@@ -29466,11 +30440,13 @@ function apply(ctx, config) {
|
|
|
29466
30440
|
});
|
|
29467
30441
|
if (includeInContext && !localSignal.aborted) submitResult(result);
|
|
29468
30442
|
};
|
|
29469
|
-
const
|
|
30443
|
+
const sandboxPreference = localShellSandboxPreferenceOf(tuiSettings?.get());
|
|
30444
|
+
const shell = sandboxPreference === "sandbox" ? ctx.get("shell") : void 0;
|
|
30445
|
+
if (shell === void 0 && sandboxPreference === "sandbox") app.notify("local shell sandbox unavailable in this composition — running unsandboxed", "error");
|
|
29470
30446
|
if (shell !== void 0) {
|
|
29471
30447
|
const spec = shell.resolve({
|
|
29472
30448
|
command,
|
|
29473
|
-
workdir:
|
|
30449
|
+
workdir: sessionCwd(),
|
|
29474
30450
|
signal: localSignal
|
|
29475
30451
|
});
|
|
29476
30452
|
runOwned("local shell", () => shell.run(spec), {
|
|
@@ -29500,7 +30476,7 @@ function apply(ctx, config) {
|
|
|
29500
30476
|
return;
|
|
29501
30477
|
}
|
|
29502
30478
|
const child = spawn(command, {
|
|
29503
|
-
cwd,
|
|
30479
|
+
cwd: sessionCwd(),
|
|
29504
30480
|
stdio: [
|
|
29505
30481
|
"ignore",
|
|
29506
30482
|
"pipe",
|
|
@@ -30063,8 +31039,7 @@ function apply(ctx, config) {
|
|
|
30063
31039
|
},
|
|
30064
31040
|
onDequeue: () => {
|
|
30065
31041
|
if (liveAgent === void 0) return;
|
|
30066
|
-
const
|
|
30067
|
-
const queued = [...liveAgent.inbox.nextTurn, ...liveAgent.inbox.nextStep].filter((message) => !isNotice(message));
|
|
31042
|
+
const queued = [...liveAgent.inbox.nextTurn, ...liveAgent.inbox.nextStep].filter((message) => isUserQueueInput(message.source));
|
|
30068
31043
|
if (queued.length === 0) return;
|
|
30069
31044
|
for (const message of queued) liveAgent.inbox.remove(message.id);
|
|
30070
31045
|
const queuedText = queued.map((message) => textOf(message.content)).join("\n\n");
|
|
@@ -30072,58 +31047,7 @@ function apply(ctx, config) {
|
|
|
30072
31047
|
app.setDraft([queuedText, current].filter((part) => part.trim() !== "").join("\n\n"));
|
|
30073
31048
|
refreshQueue();
|
|
30074
31049
|
},
|
|
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
|
-
}
|
|
31050
|
+
onOpenTasks: () => openTasksBrowser()
|
|
30127
31051
|
}, {
|
|
30128
31052
|
present,
|
|
30129
31053
|
workspaceRoot: cwd,
|
|
@@ -30142,6 +31066,73 @@ function apply(ctx, config) {
|
|
|
30142
31066
|
unstableInputsRevision: () => extensionService?._unstableInputsRevision() ?? 0,
|
|
30143
31067
|
unstableFailSafeRelease: () => extensionService?._unstableEmergencyRelease()
|
|
30144
31068
|
});
|
|
31069
|
+
const openTasksBrowser = () => {
|
|
31070
|
+
if (liveAgent === void 0) return;
|
|
31071
|
+
let jobSnapshots = [];
|
|
31072
|
+
if (jobs !== void 0) try {
|
|
31073
|
+
jobSnapshots = jobs.list(liveAgent);
|
|
31074
|
+
} catch {}
|
|
31075
|
+
let rows = buildTaskRows(jobSnapshots, []);
|
|
31076
|
+
const selectRow = (value) => {
|
|
31077
|
+
const row = rows.find((candidate) => candidate.value === value);
|
|
31078
|
+
if (row === void 0) return;
|
|
31079
|
+
if (row.kind === "subagent") {
|
|
31080
|
+
runOwned("subagent view from tasks", () => enterView(row.childId, row.label), {
|
|
31081
|
+
diag,
|
|
31082
|
+
sessionId: () => liveAgent?.session.id,
|
|
31083
|
+
onError: (error) => app.notify(`could not open the subagent view: ${safeErrorMessage(error)}`, "error")
|
|
31084
|
+
});
|
|
31085
|
+
return;
|
|
31086
|
+
}
|
|
31087
|
+
openJobView(row.jobId);
|
|
31088
|
+
};
|
|
31089
|
+
const actionRow = (value, action) => {
|
|
31090
|
+
const row = rows.find((candidate) => candidate.value === value);
|
|
31091
|
+
if (row === void 0 || row.kind !== "subagent") return;
|
|
31092
|
+
const service = ctx.get("subagents");
|
|
31093
|
+
if (service === void 0 || liveAgent === void 0) {
|
|
31094
|
+
app.notify("subagent service unavailable", "error");
|
|
31095
|
+
return;
|
|
31096
|
+
}
|
|
31097
|
+
service.interrupt(row.childId, {
|
|
31098
|
+
kind: "user",
|
|
31099
|
+
parentSessionId: liveAgent.session.id
|
|
31100
|
+
});
|
|
31101
|
+
app.notify(`interrupting ${row.label}`, "info");
|
|
31102
|
+
};
|
|
31103
|
+
const taskPanelItems = (target) => target.map((row) => row.kind === "job" ? {
|
|
31104
|
+
value: row.value,
|
|
31105
|
+
label: taskRowLabel(row),
|
|
31106
|
+
status: row.status,
|
|
31107
|
+
detail: row.detail,
|
|
31108
|
+
startedAt: row.startedAt,
|
|
31109
|
+
group: rowGroup(row)
|
|
31110
|
+
} : {
|
|
31111
|
+
value: row.value,
|
|
31112
|
+
label: taskRowLabel(row),
|
|
31113
|
+
status: row.activity,
|
|
31114
|
+
detail: row.hasChildren ? "has children" : void 0,
|
|
31115
|
+
group: rowGroup(row)
|
|
31116
|
+
});
|
|
31117
|
+
const handle = app.openTaskBrowser(taskPanelItems(rows), selectRow, () => {}, {
|
|
31118
|
+
header: "tasks · subagents",
|
|
31119
|
+
enableSearch: true,
|
|
31120
|
+
onAction: actionRow
|
|
31121
|
+
});
|
|
31122
|
+
if (subagents !== void 0) {
|
|
31123
|
+
const sessionId = liveAgent.session.id;
|
|
31124
|
+
const generation = sessionGeneration;
|
|
31125
|
+
runOwned("task browser children", () => subagents.listChildren(sessionId), {
|
|
31126
|
+
diag,
|
|
31127
|
+
sessionId: () => liveAgent?.session.id,
|
|
31128
|
+
onResult: (entries) => {
|
|
31129
|
+
if (sessionGeneration !== generation || liveAgent?.session.id !== sessionId) return;
|
|
31130
|
+
rows = buildTaskRows(jobSnapshots, entries);
|
|
31131
|
+
handle.setItems(taskPanelItems(rows));
|
|
31132
|
+
}
|
|
31133
|
+
});
|
|
31134
|
+
}
|
|
31135
|
+
};
|
|
30145
31136
|
if (extensionHost !== void 0 && extensionService !== void 0) {
|
|
30146
31137
|
app.setRendererErrorSink(({ id, error, slot }) => {
|
|
30147
31138
|
const message = safeErrorMessage(error).replace(/\s+/g, " ").slice(0, 200);
|
|
@@ -30186,12 +31177,14 @@ function apply(ctx, config) {
|
|
|
30186
31177
|
theme: z.string(),
|
|
30187
31178
|
footer: z.string(),
|
|
30188
31179
|
fullscreen: z.string(),
|
|
30189
|
-
busyEnter: z.string()
|
|
31180
|
+
busyEnter: z.string(),
|
|
31181
|
+
localShellSandbox: z.string()
|
|
30190
31182
|
}), { base: {
|
|
30191
31183
|
theme: "auto",
|
|
30192
31184
|
footer: "full",
|
|
30193
31185
|
fullscreen: "on",
|
|
30194
|
-
busyEnter: "queue"
|
|
31186
|
+
busyEnter: "queue",
|
|
31187
|
+
localShellSandbox: "bypass"
|
|
30195
31188
|
} });
|
|
30196
31189
|
if (tuiSettings?.get().fullscreen === "on") app.setFullscreen(true);
|
|
30197
31190
|
const storedTheme = tuiSettings?.get().theme;
|
|
@@ -30305,7 +31298,7 @@ function apply(ctx, config) {
|
|
|
30305
31298
|
* (never the output — the job's single read cursor belongs to the agent's
|
|
30306
31299
|
* job_output; consuming it from the UI would leave the model an
|
|
30307
31300
|
* incomplete result and could swallow the completion notice); a subagent
|
|
30308
|
-
* job shows the status viewer with a /
|
|
31301
|
+
* job shows the status viewer with a /tasks hint. The job record
|
|
30309
31302
|
* carries no child session id, so label/order/time heuristics cannot
|
|
30310
31303
|
* distinguish a background child from a same-label foreground one-shot;
|
|
30311
31304
|
* the task browser therefore never opens a transcript by guess.
|
|
@@ -30338,7 +31331,7 @@ function apply(ctx, config) {
|
|
|
30338
31331
|
};
|
|
30339
31332
|
/**
|
|
30340
31333
|
* Status-only viewer for one job (never touches the read cursor). The
|
|
30341
|
-
* subagent variant appends the /
|
|
31334
|
+
* subagent variant appends the /tasks hint because a transcript
|
|
30342
31335
|
* cannot always be matched; the bash variant is pure status.
|
|
30343
31336
|
*/
|
|
30344
31337
|
const openJobStatusViewer = (jobId, title, snapshot) => {
|
|
@@ -30608,6 +31601,7 @@ function apply(ctx, config) {
|
|
|
30608
31601
|
refreshStatus,
|
|
30609
31602
|
updateWelcomeCard,
|
|
30610
31603
|
openJobView,
|
|
31604
|
+
openTasksBrowser,
|
|
30611
31605
|
enterView,
|
|
30612
31606
|
requestExit,
|
|
30613
31607
|
exit
|
|
@@ -30777,10 +31771,11 @@ function apply(ctx, config) {
|
|
|
30777
31771
|
updateWelcomeCard();
|
|
30778
31772
|
}
|
|
30779
31773
|
});
|
|
30780
|
-
|
|
31774
|
+
const refreshCredentialSurface = () => {
|
|
30781
31775
|
refreshStatus();
|
|
30782
31776
|
updateWelcomeCard();
|
|
30783
|
-
}
|
|
31777
|
+
};
|
|
31778
|
+
registerCredentialSurfaceRefresh(ctx, refreshCredentialSurface);
|
|
30784
31779
|
if (liveAgent !== void 0) {
|
|
30785
31780
|
app.setPlanMode(foldPlanMode(liveAgent.session.events));
|
|
30786
31781
|
app.setWorking(workingFromLog(liveAgent.session.events));
|
|
@@ -30848,4 +31843,4 @@ function apply(ctx, config) {
|
|
|
30848
31843
|
});
|
|
30849
31844
|
}
|
|
30850
31845
|
//#endregion
|
|
30851
|
-
export {
|
|
31846
|
+
export { dshVersion as A, resumeCommand as C, subagentJobTranscriptId as D, shouldSteerOnEnter as E, PI_TUI_EXTENSIONS_SERVICE as F, API_VERSION as I, describeKey as L, apply$1 as M, inject$1 as N, subagentJobViewHint as O, name$1 as P, resolveInitialCatalog as S, shouldConsumeAdvertisedMiss 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, forkSeed as j, subagentNoticeIsFailure 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, runningProfile as w, registerCredentialSurfaceRefresh as x, recomposeBlank as y };
|