@xmoon76/dsh-pi-tui 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/dist/index.d.mts +20 -1
- package/dist/index.mjs +2157 -443
- package/package.json +21 -19
- package/scripts/repair-core.mjs +324 -0
- package/scripts/repair-session.mjs +277 -0
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { execSync, spawn } from "node:child_process";
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
|
-
import { readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { closeSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import { homedir, tmpdir } from "node:os";
|
|
9
9
|
import * as path from "node:path";
|
|
@@ -21,7 +21,7 @@ import { foldSessionTitle } from "@deepseek-ai/dsh-session-title";
|
|
|
21
21
|
import { Chalk } from "chalk";
|
|
22
22
|
import { Marked, Tokenizer } from "marked";
|
|
23
23
|
import { spawn as spawn$1 } from "child_process";
|
|
24
|
-
import { readdirSync as readdirSync$1, statSync } from "fs";
|
|
24
|
+
import { readdirSync as readdirSync$1, statSync as statSync$1 } from "fs";
|
|
25
25
|
import { homedir as homedir$1 } from "os";
|
|
26
26
|
import { basename, dirname as dirname$1, join as join$1 } from "path";
|
|
27
27
|
import { eastAsianWidth } from "get-east-asian-width";
|
|
@@ -81,7 +81,8 @@ const TUI_TOOL_TITLES = {
|
|
|
81
81
|
workflow: "Workflow",
|
|
82
82
|
"workflow-member": "Workflow Agent",
|
|
83
83
|
error: "Error",
|
|
84
|
-
interrupted: "Interrupted"
|
|
84
|
+
interrupted: "Interrupted",
|
|
85
|
+
ask_user_question: "Question"
|
|
85
86
|
};
|
|
86
87
|
/** Summary key preference per variant (args-derived). */
|
|
87
88
|
const SUMMARY_KEYS = {
|
|
@@ -106,12 +107,6 @@ function firstLine(text) {
|
|
|
106
107
|
const newline = text.indexOf("\n");
|
|
107
108
|
return newline === -1 ? text : text.slice(0, newline);
|
|
108
109
|
}
|
|
109
|
-
/** The last line of a text (the Web's running-reasoning summary). */
|
|
110
|
-
function latestLine(text) {
|
|
111
|
-
const visible = text.trimEnd();
|
|
112
|
-
const newline = visible.lastIndexOf("\n");
|
|
113
|
-
return newline === -1 ? visible : visible.slice(newline + 1);
|
|
114
|
-
}
|
|
115
110
|
/**
|
|
116
111
|
* Strip the workspace root from a workspace-rooted absolute path (display
|
|
117
112
|
* only), exactly like the Web's relativizeToCwd: only the workspace-root
|
|
@@ -162,7 +157,8 @@ const TOOL_EMOJIS = {
|
|
|
162
157
|
workflow: "🧵",
|
|
163
158
|
"workflow-member": "🤖",
|
|
164
159
|
error: "❌",
|
|
165
|
-
interrupted: "⏹️"
|
|
160
|
+
interrupted: "⏹️",
|
|
161
|
+
ask_user_question: "❓"
|
|
166
162
|
};
|
|
167
163
|
/** Emoji per row variant, applied to every registered tool of that class. */
|
|
168
164
|
const VARIANT_EMOJIS = {
|
|
@@ -187,6 +183,61 @@ function toolEmoji(name) {
|
|
|
187
183
|
return VARIANT_EMOJIS[classifyTool(name)];
|
|
188
184
|
}
|
|
189
185
|
/**
|
|
186
|
+
* Parse the read tool's model-facing envelopes from a result: one
|
|
187
|
+
* `<path>…</path> <type>…</type> <content>…numbered lines…</content>` block
|
|
188
|
+
* per file read. A merged group card (groupConsecutiveReads) carries several
|
|
189
|
+
* consecutive envelopes, hence the plural form; the single-envelope helper
|
|
190
|
+
* below is the common-case convenience.
|
|
191
|
+
* @param result - the tool result text.
|
|
192
|
+
* @returns every parsed envelope; empty when the result is not a read envelope.
|
|
193
|
+
*/
|
|
194
|
+
function parseReadEnvelopes(result) {
|
|
195
|
+
const out = [];
|
|
196
|
+
const block = /<path>([\s\S]*?)<\/path>\s*<type>[^<]*<\/type>\s*<content>([\s\S]*?)<\/content>/g;
|
|
197
|
+
let match;
|
|
198
|
+
while ((match = block.exec(result)) !== null) {
|
|
199
|
+
const path = match[1] ?? "";
|
|
200
|
+
const body = match[2] ?? "";
|
|
201
|
+
const lines = [];
|
|
202
|
+
let totalLines;
|
|
203
|
+
for (const raw of body.split("\n")) {
|
|
204
|
+
const numbered = /^(\d+): (.*)$/.exec(raw);
|
|
205
|
+
if (numbered !== null) {
|
|
206
|
+
lines.push({
|
|
207
|
+
number: Number(numbered[1]),
|
|
208
|
+
text: numbered[2] ?? ""
|
|
209
|
+
});
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const end = /^\(End of file - total (\d+) lines\)$/.exec(raw);
|
|
213
|
+
if (end !== null) {
|
|
214
|
+
totalLines = Number(end[1]);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const showing = /^\(Showing lines \d+-\d+ of (\d+) lines/.exec(raw);
|
|
218
|
+
if (showing !== null) totalLines = Number(showing[1]);
|
|
219
|
+
}
|
|
220
|
+
out.push({
|
|
221
|
+
path,
|
|
222
|
+
lines,
|
|
223
|
+
...totalLines === void 0 ? {} : { totalLines }
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The folded preview suffix for a read card: `— {N} lines` when the envelope
|
|
230
|
+
* reports a total (or a line count), empty otherwise. A merged group card
|
|
231
|
+
* (multiple envelopes) yields nothing — its head already carries "N files".
|
|
232
|
+
*/
|
|
233
|
+
function readFoldedPreview(result) {
|
|
234
|
+
const envelopes = parseReadEnvelopes(result);
|
|
235
|
+
if (envelopes.length !== 1) return "";
|
|
236
|
+
const envelope = envelopes[0];
|
|
237
|
+
const total = envelope.totalLines ?? (envelope.lines.length > 0 ? envelope.lines.length : void 0);
|
|
238
|
+
return total === void 0 ? "" : ` — ${total} lines`;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
190
241
|
* The Web row-model header for one tool card: title = tool-owned title or the
|
|
191
242
|
* variant design title; summary = SUMMARY_KEYS-derived args summary relativized
|
|
192
243
|
* to the workspace root. Unknown variants carry the tool name alongside the
|
|
@@ -480,7 +531,7 @@ var TranscriptFolder = class {
|
|
|
480
531
|
commandNames = /* @__PURE__ */ new Map();
|
|
481
532
|
/** Workflow run cards by runId, for member/run settlement. */
|
|
482
533
|
workflowRuns = /* @__PURE__ */ new Map();
|
|
483
|
-
/** Workflow member
|
|
534
|
+
/** Workflow member rows by `${runId}/${seq}`, for agent-end settlement. */
|
|
484
535
|
workflowMembers = /* @__PURE__ */ new Map();
|
|
485
536
|
/** The turn most recently opened by turn/start. */
|
|
486
537
|
currentTurn = 0;
|
|
@@ -681,7 +732,8 @@ var TranscriptFolder = class {
|
|
|
681
732
|
name: "workflow",
|
|
682
733
|
args: event.data.name,
|
|
683
734
|
result: "",
|
|
684
|
-
status: "running"
|
|
735
|
+
status: "running",
|
|
736
|
+
members: []
|
|
685
737
|
};
|
|
686
738
|
this.workflowRuns.set(event.data.runId, card);
|
|
687
739
|
this.items.push(card);
|
|
@@ -689,25 +741,20 @@ var TranscriptFolder = class {
|
|
|
689
741
|
}
|
|
690
742
|
case "tool-workflow/agent-start": {
|
|
691
743
|
const { runId, seq, label, phase } = event.data;
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
args: label,
|
|
697
|
-
result: phase ?? "",
|
|
744
|
+
const run = this.workflowRuns.get(runId);
|
|
745
|
+
const member = {
|
|
746
|
+
label,
|
|
747
|
+
...phase === void 0 ? {} : { phase },
|
|
698
748
|
status: "running"
|
|
699
749
|
};
|
|
700
|
-
this.workflowMembers.set(`${runId}/${seq}`,
|
|
701
|
-
|
|
750
|
+
this.workflowMembers.set(`${runId}/${seq}`, member);
|
|
751
|
+
run?.members?.push(member);
|
|
702
752
|
break;
|
|
703
753
|
}
|
|
704
754
|
case "tool-workflow/agent-end": {
|
|
705
|
-
const
|
|
755
|
+
const member = this.workflowMembers.get(`${event.data.runId}/${event.data.seq}`);
|
|
706
756
|
const outcome = event.data.outcome;
|
|
707
|
-
if (
|
|
708
|
-
card.status = outcome === "completed" ? "ok" : "error";
|
|
709
|
-
card.result = outcome === "completed" ? "completed" : `outcome: ${outcome}`;
|
|
710
|
-
}
|
|
757
|
+
if (member !== void 0) member.status = outcome === "completed" ? "ok" : "error";
|
|
711
758
|
break;
|
|
712
759
|
}
|
|
713
760
|
case "tool-workflow/run-end": {
|
|
@@ -814,7 +861,8 @@ const EMPTY = {
|
|
|
814
861
|
cacheHitPct: 0,
|
|
815
862
|
inputTokens: 0,
|
|
816
863
|
outputTokens: 0,
|
|
817
|
-
cacheReadTokens: 0
|
|
864
|
+
cacheReadTokens: 0,
|
|
865
|
+
cacheWriteTokens: 0
|
|
818
866
|
};
|
|
819
867
|
/** Key identifying one step's model output (turn + step). */
|
|
820
868
|
function stepKey(turn, step) {
|
|
@@ -827,10 +875,9 @@ function stepKey(turn, step) {
|
|
|
827
875
|
*/
|
|
828
876
|
function computeStats(events) {
|
|
829
877
|
const stats = { ...EMPTY };
|
|
830
|
-
const
|
|
831
|
-
const firstDelta = /* @__PURE__ */ new Map();
|
|
832
|
-
const lastDelta = /* @__PURE__ */ new Map();
|
|
878
|
+
const perStep = /* @__PURE__ */ new Map();
|
|
833
879
|
const outputMs = [];
|
|
880
|
+
let decodeTokens = 0;
|
|
834
881
|
let firstTokenTotal = 0;
|
|
835
882
|
let firstTokenCount = 0;
|
|
836
883
|
for (const event of events) switch (event.type) {
|
|
@@ -839,50 +886,76 @@ function computeStats(events) {
|
|
|
839
886
|
break;
|
|
840
887
|
case "step/start":
|
|
841
888
|
stats.steps += 1;
|
|
842
|
-
|
|
889
|
+
perStep.set(stepKey(event.data.turn, event.data.step), { start: event.time });
|
|
843
890
|
break;
|
|
844
891
|
case "step/end": {
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
892
|
+
const timing = perStep.get(stepKey(event.data.turn, event.data.step));
|
|
893
|
+
if (timing === void 0) break;
|
|
894
|
+
settleStep(stats, timing, event.time, outputMs, (ms) => {
|
|
895
|
+
decodeTokens += ms;
|
|
896
|
+
});
|
|
897
|
+
perStep.delete(stepKey(event.data.turn, event.data.step));
|
|
851
898
|
break;
|
|
852
899
|
}
|
|
853
900
|
case "assistant/chunk": {
|
|
854
901
|
const { chunk } = event.data;
|
|
855
902
|
if (chunk.type === "text-delta") {
|
|
856
|
-
const
|
|
857
|
-
if (
|
|
858
|
-
firstDelta
|
|
859
|
-
const start =
|
|
903
|
+
const timing = perStep.get(stepKey(event.data.turn, event.data.step));
|
|
904
|
+
if (timing !== void 0 && timing.firstDelta === void 0) {
|
|
905
|
+
timing.firstDelta = event.time;
|
|
906
|
+
const start = timing.start;
|
|
860
907
|
if (start !== void 0) {
|
|
861
908
|
firstTokenTotal += event.time - start;
|
|
862
909
|
firstTokenCount += 1;
|
|
863
910
|
}
|
|
864
911
|
}
|
|
865
|
-
|
|
866
|
-
|
|
912
|
+
} else if (chunk.type === "usage") {
|
|
913
|
+
const key = stepKey(event.data.turn, event.data.step);
|
|
914
|
+
const timing = perStep.get(key);
|
|
915
|
+
if (timing !== void 0) {
|
|
916
|
+
if (timing.usage === void 0) timing.usage = chunk.usage;
|
|
917
|
+
} else addUsage(stats, chunk.usage);
|
|
918
|
+
}
|
|
867
919
|
break;
|
|
868
920
|
}
|
|
869
|
-
case "assistant/message":
|
|
870
|
-
|
|
921
|
+
case "assistant/message": {
|
|
922
|
+
const timing = perStep.get(stepKey(event.data.turn, event.data.step));
|
|
923
|
+
if (timing !== void 0) {
|
|
924
|
+
timing.completed = event.time;
|
|
925
|
+
if (event.data.usage !== void 0) timing.usage = event.data.usage;
|
|
926
|
+
} else if (event.data.usage !== void 0) addUsage(stats, event.data.usage);
|
|
871
927
|
break;
|
|
928
|
+
}
|
|
872
929
|
case "request/context": if (event.data.contextWindow !== void 0) stats.contextWindow = event.data.contextWindow;
|
|
873
930
|
}
|
|
874
931
|
if (firstTokenCount > 0) stats.firstTokenMsAvg = firstTokenTotal / firstTokenCount;
|
|
875
932
|
const streamMs = outputMs.reduce((sum, ms) => sum + ms, 0);
|
|
876
|
-
if (streamMs > 0) stats.tokensPerSec = Math.round(
|
|
877
|
-
const
|
|
878
|
-
if (
|
|
933
|
+
if (streamMs > 0) stats.tokensPerSec = Math.round(decodeTokens * 1e3 / streamMs);
|
|
934
|
+
const billedInput = stats.inputTokens + stats.cacheReadTokens + stats.cacheWriteTokens;
|
|
935
|
+
if (billedInput > 0) stats.cacheHitPct = stats.cacheReadTokens * 100 / billedInput;
|
|
879
936
|
return stats;
|
|
880
937
|
}
|
|
881
|
-
/**
|
|
938
|
+
/** Settle one step at its end boundary: wall time, decode window, usage. */
|
|
939
|
+
function settleStep(stats, timing, endTime, outputMs, addDecodeTokens) {
|
|
940
|
+
const completed = timing.completed ?? endTime;
|
|
941
|
+
const start = timing.start;
|
|
942
|
+
if (start !== void 0) stats.llmMs += Math.max(0, completed - start);
|
|
943
|
+
const usage = timing.usage;
|
|
944
|
+
if (usage !== void 0) {
|
|
945
|
+
addUsage(stats, usage);
|
|
946
|
+
const first = timing.firstDelta;
|
|
947
|
+
if (first !== void 0) {
|
|
948
|
+
outputMs.push(Math.max(0, completed - first));
|
|
949
|
+
addDecodeTokens(usage.outputTokens);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
/** Token totals from one usage record (cache fields counted separately). */
|
|
882
954
|
function addUsage(stats, usage) {
|
|
883
955
|
stats.inputTokens += usage.inputTokens;
|
|
884
956
|
stats.outputTokens += usage.outputTokens;
|
|
885
957
|
stats.cacheReadTokens += usage.cacheReadTokens ?? 0;
|
|
958
|
+
stats.cacheWriteTokens += usage.cacheWriteTokens ?? 0;
|
|
886
959
|
}
|
|
887
960
|
/**
|
|
888
961
|
* Incremental stats folding: apply appended events and read `snapshot()`
|
|
@@ -892,10 +965,9 @@ function addUsage(stats, usage) {
|
|
|
892
965
|
*/
|
|
893
966
|
var StatsFolder = class {
|
|
894
967
|
stats = { ...EMPTY };
|
|
895
|
-
|
|
896
|
-
firstDelta = /* @__PURE__ */ new Map();
|
|
897
|
-
lastDelta = /* @__PURE__ */ new Map();
|
|
968
|
+
perStep = /* @__PURE__ */ new Map();
|
|
898
969
|
outputMs = [];
|
|
970
|
+
decodeTokens = 0;
|
|
899
971
|
firstTokenTotal = 0;
|
|
900
972
|
firstTokenCount = 0;
|
|
901
973
|
/**
|
|
@@ -910,9 +982,9 @@ var StatsFolder = class {
|
|
|
910
982
|
const derived = { ...this.stats };
|
|
911
983
|
if (this.firstTokenCount > 0) derived.firstTokenMsAvg = this.firstTokenTotal / this.firstTokenCount;
|
|
912
984
|
const streamMs = this.outputMs.reduce((sum, ms) => sum + ms, 0);
|
|
913
|
-
if (streamMs > 0) derived.tokensPerSec = Math.round(
|
|
914
|
-
const
|
|
915
|
-
if (
|
|
985
|
+
if (streamMs > 0) derived.tokensPerSec = Math.round(this.decodeTokens * 1e3 / streamMs);
|
|
986
|
+
const billedInput = derived.inputTokens + derived.cacheReadTokens + derived.cacheWriteTokens;
|
|
987
|
+
if (billedInput > 0) derived.cacheHitPct = derived.cacheReadTokens * 100 / billedInput;
|
|
916
988
|
return derived;
|
|
917
989
|
}
|
|
918
990
|
applyEvent(event) {
|
|
@@ -922,36 +994,49 @@ var StatsFolder = class {
|
|
|
922
994
|
break;
|
|
923
995
|
case "step/start":
|
|
924
996
|
this.stats.steps += 1;
|
|
925
|
-
this.
|
|
997
|
+
this.perStep.set(stepKey(event.data.turn, event.data.step), { start: event.time });
|
|
926
998
|
break;
|
|
927
999
|
case "step/end": {
|
|
928
1000
|
const key = stepKey(event.data.turn, event.data.step);
|
|
929
|
-
const
|
|
930
|
-
if (
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1001
|
+
const timing = this.perStep.get(key);
|
|
1002
|
+
if (timing === void 0) break;
|
|
1003
|
+
settleStep(this.stats, timing, event.time, this.outputMs, (tokens) => {
|
|
1004
|
+
this.decodeTokens += tokens;
|
|
1005
|
+
});
|
|
1006
|
+
this.perStep.delete(key);
|
|
934
1007
|
break;
|
|
935
1008
|
}
|
|
936
1009
|
case "assistant/chunk": {
|
|
937
1010
|
const { chunk } = event.data;
|
|
938
1011
|
if (chunk.type === "text-delta") {
|
|
939
1012
|
const key = stepKey(event.data.turn, event.data.step);
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1013
|
+
const timing = this.perStep.get(key);
|
|
1014
|
+
if (timing !== void 0 && timing.firstDelta === void 0) {
|
|
1015
|
+
timing.firstDelta = event.time;
|
|
1016
|
+
const start = timing.start;
|
|
943
1017
|
if (start !== void 0) {
|
|
944
1018
|
this.firstTokenTotal += event.time - start;
|
|
945
1019
|
this.firstTokenCount += 1;
|
|
946
1020
|
}
|
|
947
1021
|
}
|
|
948
|
-
|
|
949
|
-
|
|
1022
|
+
} else if (chunk.type === "usage") {
|
|
1023
|
+
const key = stepKey(event.data.turn, event.data.step);
|
|
1024
|
+
const timing = this.perStep.get(key);
|
|
1025
|
+
if (timing !== void 0) {
|
|
1026
|
+
if (timing.usage === void 0) timing.usage = chunk.usage;
|
|
1027
|
+
} else addUsage(this.stats, chunk.usage);
|
|
1028
|
+
}
|
|
950
1029
|
break;
|
|
951
1030
|
}
|
|
952
|
-
case "assistant/message":
|
|
953
|
-
|
|
1031
|
+
case "assistant/message": {
|
|
1032
|
+
const key = stepKey(event.data.turn, event.data.step);
|
|
1033
|
+
const timing = this.perStep.get(key);
|
|
1034
|
+
if (timing !== void 0) {
|
|
1035
|
+
timing.completed = event.time;
|
|
1036
|
+
if (event.data.usage !== void 0) timing.usage = event.data.usage;
|
|
1037
|
+
} else if (event.data.usage !== void 0) addUsage(this.stats, event.data.usage);
|
|
954
1038
|
break;
|
|
1039
|
+
}
|
|
955
1040
|
case "request/context": if (event.data.contextWindow !== void 0) this.stats.contextWindow = event.data.contextWindow;
|
|
956
1041
|
}
|
|
957
1042
|
}
|
|
@@ -987,7 +1072,8 @@ function formatStats(stats) {
|
|
|
987
1072
|
`↑${formatTokens(stats.inputTokens)}`,
|
|
988
1073
|
`↓${formatTokens(stats.outputTokens)}`,
|
|
989
1074
|
stats.cacheReadTokens > 0 ? `R${formatTokens(stats.cacheReadTokens)}` : "",
|
|
990
|
-
stats.
|
|
1075
|
+
stats.cacheWriteTokens > 0 ? `W${formatTokens(stats.cacheWriteTokens)}` : "",
|
|
1076
|
+
stats.cacheReadTokens > 0 || stats.cacheWriteTokens > 0 ? `CH${stats.cacheHitPct.toFixed(1)}%` : ""
|
|
991
1077
|
].filter((part) => part !== "");
|
|
992
1078
|
const ownParts = [
|
|
993
1079
|
`LLM ${formatSeconds(stats.llmMs)}`,
|
|
@@ -1099,7 +1185,11 @@ const color = {
|
|
|
1099
1185
|
warning: (text) => hex("warning")(text),
|
|
1100
1186
|
error: (text) => hex("error")(text),
|
|
1101
1187
|
roleUser: (text) => hex("roleUser")(text),
|
|
1102
|
-
shellMode: (text) => hex("shellMode")(text)
|
|
1188
|
+
shellMode: (text) => hex("shellMode")(text),
|
|
1189
|
+
/** Plain italics (kimi thinking parity). */
|
|
1190
|
+
italic: (text) => chalk.italic(text),
|
|
1191
|
+
/** Dim + italic for reasoning: intermediate thinking never reads like output. */
|
|
1192
|
+
textDimItalic: (text) => chalk.italic.hex(currentPalette.textDim)(text)
|
|
1103
1193
|
};
|
|
1104
1194
|
/** SelectList palette from the semantic tokens. */
|
|
1105
1195
|
const selectListTheme = {
|
|
@@ -1551,7 +1641,7 @@ var CombinedAutocompleteProvider = class {
|
|
|
1551
1641
|
if (displayBase.startsWith("~/") || displayBase.startsWith("/")) {
|
|
1552
1642
|
const baseDir = displayBase.startsWith("~/") ? this.expandHomePath(displayBase) : displayBase;
|
|
1553
1643
|
try {
|
|
1554
|
-
if (!statSync(baseDir).isDirectory()) return null;
|
|
1644
|
+
if (!statSync$1(baseDir).isDirectory()) return null;
|
|
1555
1645
|
} catch {
|
|
1556
1646
|
return null;
|
|
1557
1647
|
}
|
|
@@ -1602,7 +1692,7 @@ var CombinedAutocompleteProvider = class {
|
|
|
1602
1692
|
let isDirectory = entry.isDirectory();
|
|
1603
1693
|
if (!isDirectory && entry.isSymbolicLink()) try {
|
|
1604
1694
|
const fullPath = join$1(searchDir, entry.name);
|
|
1605
|
-
isDirectory = statSync(fullPath).isDirectory();
|
|
1695
|
+
isDirectory = statSync$1(fullPath).isDirectory();
|
|
1606
1696
|
} catch {}
|
|
1607
1697
|
let relativePath;
|
|
1608
1698
|
const name = entry.name;
|
|
@@ -1685,7 +1775,7 @@ var CombinedAutocompleteProvider = class {
|
|
|
1685
1775
|
const baseDir = join$1(root, scoped.displayBase);
|
|
1686
1776
|
let isDir = false;
|
|
1687
1777
|
try {
|
|
1688
|
-
isDir = statSync(baseDir).isDirectory();
|
|
1778
|
+
isDir = statSync$1(baseDir).isDirectory();
|
|
1689
1779
|
} catch {
|
|
1690
1780
|
isDir = false;
|
|
1691
1781
|
}
|
|
@@ -8338,6 +8428,73 @@ var StrictStrikethroughTokenizer = class extends Tokenizer {
|
|
|
8338
8428
|
};
|
|
8339
8429
|
}
|
|
8340
8430
|
};
|
|
8431
|
+
const FULLWIDTH_LEFT_PAREN = 65288;
|
|
8432
|
+
const FULLWIDTH_RIGHT_PAREN = 65289;
|
|
8433
|
+
const CJK_URL_TERMINATOR_REGEX = /[\u3000-\u303f\uff01-\uff07\uff0a-\uff0f\uff1a-\uff20\uff3b-\uff40\uff5b-\uff65\u2013\u2014\u2018\u2019\u201c\u201d\u2026]/;
|
|
8434
|
+
/**
|
|
8435
|
+
* Index at which to cut an autolink match, or -1 to keep it whole. Non-paren
|
|
8436
|
+
* CJK punctuation terminates the URL outside of full-width parens; full-width
|
|
8437
|
+
* parens only terminate it when unbalanced. Punctuation inside a balanced
|
|
8438
|
+
* parenthetical (e.g. the ,in (北京,1949年)) stays part of the URL —
|
|
8439
|
+
* prose parentheticals contain spaces and never survive marked's match this
|
|
8440
|
+
* far, so a balanced group is almost always deliberate URL content.
|
|
8441
|
+
*/
|
|
8442
|
+
function findCjkUrlBoundary(match) {
|
|
8443
|
+
let parenDepth = 0;
|
|
8444
|
+
let unmatchedOpen = -1;
|
|
8445
|
+
for (let i = 0; i < match.length; i++) {
|
|
8446
|
+
const code = match.charCodeAt(i);
|
|
8447
|
+
if (code === FULLWIDTH_LEFT_PAREN) {
|
|
8448
|
+
parenDepth++;
|
|
8449
|
+
if (unmatchedOpen === -1) unmatchedOpen = i;
|
|
8450
|
+
} else if (code === FULLWIDTH_RIGHT_PAREN) {
|
|
8451
|
+
if (parenDepth === 0) return i;
|
|
8452
|
+
parenDepth--;
|
|
8453
|
+
if (parenDepth === 0) unmatchedOpen = -1;
|
|
8454
|
+
} else if (parenDepth === 0 && CJK_URL_TERMINATOR_REGEX.test(match[i])) return i;
|
|
8455
|
+
}
|
|
8456
|
+
return parenDepth > 0 ? unmatchedOpen : -1;
|
|
8457
|
+
}
|
|
8458
|
+
var CjkBoundaryUrlTokenizer = class extends StrictStrikethroughTokenizer {
|
|
8459
|
+
url(src) {
|
|
8460
|
+
const cap = this.rules.inline.url.exec(src);
|
|
8461
|
+
if (!cap) return;
|
|
8462
|
+
if (cap[2] === "@") {
|
|
8463
|
+
const text = cap[0];
|
|
8464
|
+
return {
|
|
8465
|
+
type: "link",
|
|
8466
|
+
raw: text,
|
|
8467
|
+
text,
|
|
8468
|
+
href: `mailto:${text}`,
|
|
8469
|
+
tokens: [{
|
|
8470
|
+
type: "text",
|
|
8471
|
+
raw: text,
|
|
8472
|
+
text
|
|
8473
|
+
}]
|
|
8474
|
+
};
|
|
8475
|
+
}
|
|
8476
|
+
const boundary = findCjkUrlBoundary(cap[0]);
|
|
8477
|
+
if (boundary !== -1) cap[0] = cap[0].slice(0, boundary);
|
|
8478
|
+
if (!cap[0]) return;
|
|
8479
|
+
let previous;
|
|
8480
|
+
do {
|
|
8481
|
+
previous = cap[0];
|
|
8482
|
+
cap[0] = this.rules.inline._backpedal.exec(cap[0])?.[0] ?? "";
|
|
8483
|
+
} while (previous !== cap[0]);
|
|
8484
|
+
const text = cap[0];
|
|
8485
|
+
return {
|
|
8486
|
+
type: "link",
|
|
8487
|
+
raw: text,
|
|
8488
|
+
text,
|
|
8489
|
+
href: cap[1] === "www." ? `http://${text}` : text,
|
|
8490
|
+
tokens: [{
|
|
8491
|
+
type: "text",
|
|
8492
|
+
raw: text,
|
|
8493
|
+
text
|
|
8494
|
+
}]
|
|
8495
|
+
};
|
|
8496
|
+
}
|
|
8497
|
+
};
|
|
8341
8498
|
function isEscaped(source, index) {
|
|
8342
8499
|
let backslashes = 0;
|
|
8343
8500
|
for (let position = index - 1; position >= 0 && source[position] === "\\"; position--) backslashes++;
|
|
@@ -8453,7 +8610,7 @@ function trimPartialClosingFences(tokens) {
|
|
|
8453
8610
|
token.text = token.text.slice(0, -lastLine.length).replace(/\n$/, "");
|
|
8454
8611
|
}
|
|
8455
8612
|
const markdownParser = new Marked();
|
|
8456
|
-
markdownParser.setOptions({ tokenizer: new
|
|
8613
|
+
markdownParser.setOptions({ tokenizer: new CjkBoundaryUrlTokenizer() });
|
|
8457
8614
|
markdownParser.use({ extensions: [...LATEX_MARKDOWN_EXTENSIONS] });
|
|
8458
8615
|
var Markdown = class {
|
|
8459
8616
|
text;
|
|
@@ -11837,22 +11994,204 @@ function renderDiffLines(text) {
|
|
|
11837
11994
|
return text.split("\n").map(renderDiffLine);
|
|
11838
11995
|
}
|
|
11839
11996
|
/**
|
|
11997
|
+
* Above this combined input size the O(n·m) LCS table is skipped and the
|
|
11998
|
+
* diff degrades to a naive all-delete/all-add listing (still correct, just
|
|
11999
|
+
* without alignment).
|
|
12000
|
+
*/
|
|
12001
|
+
const DIFF_LCS_MAX_LINES = 2e3;
|
|
12002
|
+
/**
|
|
12003
|
+
* Compute a line-level diff by DP longest-common-subsequence (kimi
|
|
12004
|
+
* `computeDiffLines` parity): context rows for identical lines, add/delete
|
|
12005
|
+
* rows for the differing runs, each carrying its source-side line number.
|
|
12006
|
+
* @param oldText - the before side.
|
|
12007
|
+
* @param newText - the after side.
|
|
12008
|
+
* @param oldStart - first line number of the old side (default 1).
|
|
12009
|
+
* @param newStart - first line number of the new side (default 1).
|
|
12010
|
+
* @returns the diff rows in document order.
|
|
12011
|
+
*/
|
|
12012
|
+
function computeDiffLines(oldText, newText, oldStart = 1, newStart = 1) {
|
|
12013
|
+
const oldLines = oldText.split("\n");
|
|
12014
|
+
const newLines = newText.split("\n");
|
|
12015
|
+
const m = oldLines.length;
|
|
12016
|
+
const n = newLines.length;
|
|
12017
|
+
if (m + n > DIFF_LCS_MAX_LINES) {
|
|
12018
|
+
const out = [];
|
|
12019
|
+
for (let i = 0; i < m; i++) out.push({
|
|
12020
|
+
kind: "delete",
|
|
12021
|
+
lineNum: oldStart + i,
|
|
12022
|
+
code: oldLines[i]
|
|
12023
|
+
});
|
|
12024
|
+
for (let j = 0; j < n; j++) out.push({
|
|
12025
|
+
kind: "add",
|
|
12026
|
+
lineNum: newStart + j,
|
|
12027
|
+
code: newLines[j]
|
|
12028
|
+
});
|
|
12029
|
+
return out;
|
|
12030
|
+
}
|
|
12031
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
12032
|
+
for (let i = 1; i <= m; i++) for (let j = 1; j <= n; j++) dp[i][j] = oldLines[i - 1] === newLines[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
12033
|
+
const reversed = [];
|
|
12034
|
+
let i = m;
|
|
12035
|
+
let j = n;
|
|
12036
|
+
while (i > 0 || j > 0) if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
12037
|
+
reversed.push({
|
|
12038
|
+
kind: "context",
|
|
12039
|
+
lineNum: newStart + j - 1,
|
|
12040
|
+
code: newLines[j - 1]
|
|
12041
|
+
});
|
|
12042
|
+
i--;
|
|
12043
|
+
j--;
|
|
12044
|
+
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
12045
|
+
reversed.push({
|
|
12046
|
+
kind: "add",
|
|
12047
|
+
lineNum: newStart + j - 1,
|
|
12048
|
+
code: newLines[j - 1]
|
|
12049
|
+
});
|
|
12050
|
+
j--;
|
|
12051
|
+
} else {
|
|
12052
|
+
reversed.push({
|
|
12053
|
+
kind: "delete",
|
|
12054
|
+
lineNum: oldStart + i - 1,
|
|
12055
|
+
code: oldLines[i - 1]
|
|
12056
|
+
});
|
|
12057
|
+
i--;
|
|
12058
|
+
}
|
|
12059
|
+
return reversed.reverse();
|
|
12060
|
+
}
|
|
12061
|
+
/**
|
|
12062
|
+
* Group change rows into clusters with `contextLines` of context on each
|
|
12063
|
+
* side, merging clusters whose unchanged gap is ≤ 2·contextLines.
|
|
12064
|
+
* @returns the clusters plus add/delete/change counts over all diff rows.
|
|
12065
|
+
*/
|
|
12066
|
+
function buildDiffClusters(diffLines, contextLines) {
|
|
12067
|
+
const changeIndices = [];
|
|
12068
|
+
let added = 0;
|
|
12069
|
+
let removed = 0;
|
|
12070
|
+
for (const [i, line] of diffLines.entries()) if (line.kind === "add") {
|
|
12071
|
+
added++;
|
|
12072
|
+
changeIndices.push(i);
|
|
12073
|
+
} else if (line.kind === "delete") {
|
|
12074
|
+
removed++;
|
|
12075
|
+
changeIndices.push(i);
|
|
12076
|
+
}
|
|
12077
|
+
if (changeIndices.length === 0) return {
|
|
12078
|
+
clusters: [],
|
|
12079
|
+
changedCount: 0,
|
|
12080
|
+
addedCount: added,
|
|
12081
|
+
removedCount: removed
|
|
12082
|
+
};
|
|
12083
|
+
const clusters = [];
|
|
12084
|
+
const mergeGap = 2 * contextLines;
|
|
12085
|
+
let groupStart = changeIndices[0];
|
|
12086
|
+
let groupEnd = changeIndices[0];
|
|
12087
|
+
for (let k = 1; k < changeIndices.length; k++) {
|
|
12088
|
+
const idx = changeIndices[k];
|
|
12089
|
+
if (idx - groupEnd <= mergeGap) groupEnd = idx;
|
|
12090
|
+
else {
|
|
12091
|
+
clusters.push({
|
|
12092
|
+
start: Math.max(0, groupStart - contextLines),
|
|
12093
|
+
end: Math.min(diffLines.length - 1, groupEnd + contextLines)
|
|
12094
|
+
});
|
|
12095
|
+
groupStart = idx;
|
|
12096
|
+
groupEnd = idx;
|
|
12097
|
+
}
|
|
12098
|
+
}
|
|
12099
|
+
clusters.push({
|
|
12100
|
+
start: Math.max(0, groupStart - contextLines),
|
|
12101
|
+
end: Math.min(diffLines.length - 1, groupEnd + contextLines)
|
|
12102
|
+
});
|
|
12103
|
+
return {
|
|
12104
|
+
clusters,
|
|
12105
|
+
changedCount: changeIndices.length,
|
|
12106
|
+
addedCount: added,
|
|
12107
|
+
removedCount: removed
|
|
12108
|
+
};
|
|
12109
|
+
}
|
|
12110
|
+
/** One diff body row: dim line-number gutter plus the colored/plain code. */
|
|
12111
|
+
function formatDiffRow(line) {
|
|
12112
|
+
const gutter = color.textDim(`${String(line.lineNum).padStart(4)} `);
|
|
12113
|
+
if (line.kind === "add") return gutter + color.success(`+ ${line.code}`);
|
|
12114
|
+
if (line.kind === "delete") return gutter + color.error(`- ${line.code}`);
|
|
12115
|
+
return gutter + ` ${line.code}`;
|
|
12116
|
+
}
|
|
12117
|
+
/**
|
|
11840
12118
|
* Render a result-side diff view (a `card: 'diff'` presentResult intent) as
|
|
11841
|
-
* colored lines: one
|
|
11842
|
-
*
|
|
11843
|
-
*
|
|
12119
|
+
* colored lines: one `+N -M path` header per hunk (kimi parity; counts in
|
|
12120
|
+
* add/remove colors, path workspace-relative), then the LCS-aligned body
|
|
12121
|
+
* with context clustering — unchanged runs between clusters elide to a
|
|
12122
|
+
* `… N unchanged lines …` separator, and `maxLines` caps the body at a
|
|
12123
|
+
* cluster boundary with a `… N more changes hidden (hint)` footer. A hunk
|
|
12124
|
+
* with `oldText: null` (create) shows only new lines; an empty newText
|
|
12125
|
+
* (pure deletion) shows only old lines.
|
|
11844
12126
|
* @param diffs - the diff view's hunks.
|
|
11845
12127
|
* @param cwd - workspace root for path relativization; optional.
|
|
12128
|
+
* @param options - context/cap/hint tuning.
|
|
11846
12129
|
* @returns the colored render lines.
|
|
11847
12130
|
*/
|
|
11848
|
-
function renderDiffView(diffs, cwd) {
|
|
11849
|
-
const
|
|
12131
|
+
function renderDiffView(diffs, cwd, options = {}) {
|
|
12132
|
+
const contextLines = options.contextLines ?? 3;
|
|
12133
|
+
const cap = options.maxLines !== void 0 && options.maxLines >= 0 ? options.maxLines : Number.POSITIVE_INFINITY;
|
|
12134
|
+
const out = [];
|
|
11850
12135
|
for (const hunk of diffs) {
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
12136
|
+
const oldSide = hunk.oldText === null || hunk.oldText === "" ? [] : hunk.oldText.split("\n");
|
|
12137
|
+
const newSide = hunk.newText === "" ? [] : hunk.newText.split("\n");
|
|
12138
|
+
const diffLines = oldSide.length === 0 ? newSide.map((code, index) => ({
|
|
12139
|
+
kind: "add",
|
|
12140
|
+
lineNum: index + 1,
|
|
12141
|
+
code
|
|
12142
|
+
})) : newSide.length === 0 ? oldSide.map((code, index) => ({
|
|
12143
|
+
kind: "delete",
|
|
12144
|
+
lineNum: index + 1,
|
|
12145
|
+
code
|
|
12146
|
+
})) : computeDiffLines(hunk.oldText, hunk.newText);
|
|
12147
|
+
const { clusters, changedCount, addedCount, removedCount } = buildDiffClusters(diffLines, contextLines);
|
|
12148
|
+
let header = "";
|
|
12149
|
+
if (addedCount > 0) header += color.success(`+${addedCount} `);
|
|
12150
|
+
if (removedCount > 0) header += color.error(`-${removedCount} `);
|
|
12151
|
+
header += relativizeToCwd(hunk.path, cwd);
|
|
12152
|
+
out.push(header);
|
|
12153
|
+
if (clusters.length === 0) continue;
|
|
12154
|
+
let body = 0;
|
|
12155
|
+
let prevEnd = -1;
|
|
12156
|
+
let truncated = false;
|
|
12157
|
+
let shownChanges = 0;
|
|
12158
|
+
outer: for (const cluster of clusters) {
|
|
12159
|
+
if (body >= cap) {
|
|
12160
|
+
truncated = true;
|
|
12161
|
+
break;
|
|
12162
|
+
}
|
|
12163
|
+
if (prevEnd >= 0) {
|
|
12164
|
+
const gap = cluster.start - prevEnd - 1;
|
|
12165
|
+
if (gap > 0) {
|
|
12166
|
+
if (body + 1 > cap) {
|
|
12167
|
+
truncated = true;
|
|
12168
|
+
break;
|
|
12169
|
+
}
|
|
12170
|
+
out.push(color.textMuted(` … ${gap} unchanged line${gap > 1 ? "s" : ""} …`));
|
|
12171
|
+
body++;
|
|
12172
|
+
}
|
|
12173
|
+
}
|
|
12174
|
+
for (let i = cluster.start; i <= cluster.end; i++) {
|
|
12175
|
+
if (body >= cap) {
|
|
12176
|
+
truncated = true;
|
|
12177
|
+
break outer;
|
|
12178
|
+
}
|
|
12179
|
+
const line = diffLines[i];
|
|
12180
|
+
out.push(formatDiffRow(line));
|
|
12181
|
+
body++;
|
|
12182
|
+
if (line.kind !== "context") shownChanges++;
|
|
12183
|
+
prevEnd = i;
|
|
12184
|
+
}
|
|
12185
|
+
}
|
|
12186
|
+
if (truncated) {
|
|
12187
|
+
const hidden = changedCount - shownChanges;
|
|
12188
|
+
if (hidden > 0) {
|
|
12189
|
+
const hint = options.expandHint ?? "click to expand";
|
|
12190
|
+
out.push(color.textMuted(` … ${hidden} more change${hidden > 1 ? "s" : ""} hidden (${hint})`));
|
|
12191
|
+
}
|
|
12192
|
+
}
|
|
11854
12193
|
}
|
|
11855
|
-
return
|
|
12194
|
+
return out;
|
|
11856
12195
|
}
|
|
11857
12196
|
//#endregion
|
|
11858
12197
|
//#region src/search.ts
|
|
@@ -11906,6 +12245,378 @@ var TranscriptSearchComponent = class {
|
|
|
11906
12245
|
}
|
|
11907
12246
|
};
|
|
11908
12247
|
//#endregion
|
|
12248
|
+
//#region src/question.ts
|
|
12249
|
+
/**
|
|
12250
|
+
* The user-questions dialog flow (ask_user_question v2): a focusable overlay
|
|
12251
|
+
* component that walks one question at a time, collects drafts per question,
|
|
12252
|
+
* and submits the whole batch at the end — the Web QuestionComposer
|
|
12253
|
+
* semantics (per-question paging, batch submit, skip) with pi/kimi keyboard
|
|
12254
|
+
* ergonomics (↑↓ highlight, digits, real Input for free text, review page).
|
|
12255
|
+
*
|
|
12256
|
+
* Pure component: the app layer owns the promise/abort plumbing and routes
|
|
12257
|
+
* input here while the flow is active.
|
|
12258
|
+
* @module @xmoon76/dsh-pi-tui/question
|
|
12259
|
+
*/
|
|
12260
|
+
/** The "type your own answer" row shown below options (pi isOther parity). */
|
|
12261
|
+
const OTHER_ROW = "\0other";
|
|
12262
|
+
/** The current question's tab label: `Q{n}` or Submit. */
|
|
12263
|
+
function tabLabel(index, total) {
|
|
12264
|
+
return index === total ? "Submit" : `Q${index + 1}`;
|
|
12265
|
+
}
|
|
12266
|
+
/** Split a conventional recommendation suffix off the DISPLAY label (the
|
|
12267
|
+
* answer value keeps the full label — Web parseRecommendedLabel parity). */
|
|
12268
|
+
function parseRecommended(label) {
|
|
12269
|
+
const suffix = /\s*(?:\((?:recommended|推荐)\)|((?:recommended|推荐)))\s*$/i;
|
|
12270
|
+
return suffix.test(label) ? {
|
|
12271
|
+
label: label.replace(suffix, ""),
|
|
12272
|
+
recommended: true
|
|
12273
|
+
} : {
|
|
12274
|
+
label,
|
|
12275
|
+
recommended: false
|
|
12276
|
+
};
|
|
12277
|
+
}
|
|
12278
|
+
/**
|
|
12279
|
+
* The interactive question flow. Renders one question at a time with a tab
|
|
12280
|
+
* strip (answered marks), a navigable option list (↑↓/digits/Enter), a real
|
|
12281
|
+
* Input for free text, and a final review page (Submit/Cancel). Esc cancels
|
|
12282
|
+
* the whole flow; the app layer resolves the batch on done().
|
|
12283
|
+
*/
|
|
12284
|
+
var QuestionFlow = class {
|
|
12285
|
+
questions;
|
|
12286
|
+
onDone;
|
|
12287
|
+
onCancel;
|
|
12288
|
+
drafts;
|
|
12289
|
+
/** Current question index; `questions.length` is the review page. */
|
|
12290
|
+
tab = 0;
|
|
12291
|
+
/** Highlighted option index within the current question. */
|
|
12292
|
+
cursor = 0;
|
|
12293
|
+
/** Free-text editing mode (the "Type something." row or an optionless question). */
|
|
12294
|
+
editingOther = false;
|
|
12295
|
+
otherInput = new Input();
|
|
12296
|
+
/** Review-page action highlight (0 = Submit, 1 = Cancel). */
|
|
12297
|
+
submitIdx = 0;
|
|
12298
|
+
focused = false;
|
|
12299
|
+
constructor(questions, onDone, onCancel) {
|
|
12300
|
+
this.questions = questions;
|
|
12301
|
+
this.onDone = onDone;
|
|
12302
|
+
this.onCancel = onCancel;
|
|
12303
|
+
this.drafts = questions.map(() => ({
|
|
12304
|
+
selected: /* @__PURE__ */ new Set(),
|
|
12305
|
+
custom: "",
|
|
12306
|
+
skipped: false
|
|
12307
|
+
}));
|
|
12308
|
+
this.otherInput.onSubmit = (value) => this.commitOther(value);
|
|
12309
|
+
this.otherInput.onEscape = () => this.exitOther();
|
|
12310
|
+
this.syncEditMode();
|
|
12311
|
+
}
|
|
12312
|
+
/** The rows of the current question (options plus the free-text row). */
|
|
12313
|
+
rows() {
|
|
12314
|
+
const question = this.questions[this.tab];
|
|
12315
|
+
if (question === void 0) return [];
|
|
12316
|
+
const rows = (question.options ?? []).map((option, index) => {
|
|
12317
|
+
const { label, recommended } = parseRecommended(option.label);
|
|
12318
|
+
const approve = question.intent?.approve;
|
|
12319
|
+
return {
|
|
12320
|
+
key: String(index),
|
|
12321
|
+
label,
|
|
12322
|
+
recommended: recommended || approve !== void 0 && option.label === approve,
|
|
12323
|
+
...option.description === void 0 ? {} : { description: option.description }
|
|
12324
|
+
};
|
|
12325
|
+
});
|
|
12326
|
+
if ((question.options?.length ?? 0) > 0) rows.push({
|
|
12327
|
+
key: OTHER_ROW,
|
|
12328
|
+
label: "Type something.",
|
|
12329
|
+
recommended: false
|
|
12330
|
+
});
|
|
12331
|
+
return rows;
|
|
12332
|
+
}
|
|
12333
|
+
/** The current question's draft. */
|
|
12334
|
+
draft() {
|
|
12335
|
+
return this.drafts[this.tab];
|
|
12336
|
+
}
|
|
12337
|
+
/** Whether the current question has an answer (selected, custom, or skipped). */
|
|
12338
|
+
answered() {
|
|
12339
|
+
const draft = this.draft();
|
|
12340
|
+
if (draft === void 0) return false;
|
|
12341
|
+
return draft.selected.size > 0 || draft.custom !== "" || draft.skipped;
|
|
12342
|
+
}
|
|
12343
|
+
/** The free-text row of the current question (present when options exist). */
|
|
12344
|
+
otherRowIndex() {
|
|
12345
|
+
return this.rows().findIndex((row) => row.key === OTHER_ROW);
|
|
12346
|
+
}
|
|
12347
|
+
/** Enter on the highlighted row, or in text mode: commit and advance. */
|
|
12348
|
+
confirm() {
|
|
12349
|
+
const draft = this.draft();
|
|
12350
|
+
if (draft === void 0) return;
|
|
12351
|
+
const row = this.rows()[this.cursor];
|
|
12352
|
+
if (row !== void 0 && row.key !== OTHER_ROW) {
|
|
12353
|
+
const option = this.questions[this.tab]?.options?.[Number(row.key)];
|
|
12354
|
+
if (option !== void 0) {
|
|
12355
|
+
const label = option.label;
|
|
12356
|
+
if (this.questions[this.tab]?.multiSelect === true) {
|
|
12357
|
+
if (draft.selected.has(label)) draft.selected.delete(label);
|
|
12358
|
+
else draft.selected.add(label);
|
|
12359
|
+
draft.skipped = false;
|
|
12360
|
+
return;
|
|
12361
|
+
}
|
|
12362
|
+
draft.selected.clear();
|
|
12363
|
+
draft.selected.add(label);
|
|
12364
|
+
draft.skipped = false;
|
|
12365
|
+
this.advance();
|
|
12366
|
+
}
|
|
12367
|
+
return;
|
|
12368
|
+
}
|
|
12369
|
+
if (row?.key === OTHER_ROW) this.enterOther();
|
|
12370
|
+
}
|
|
12371
|
+
/** Move to the next question (or the review page on the last one). */
|
|
12372
|
+
advance() {
|
|
12373
|
+
this.tab += 1;
|
|
12374
|
+
this.cursor = 0;
|
|
12375
|
+
this.submitIdx = 0;
|
|
12376
|
+
this.syncEditMode();
|
|
12377
|
+
}
|
|
12378
|
+
/** Optionless questions edit text directly; options start in list mode. */
|
|
12379
|
+
syncEditMode() {
|
|
12380
|
+
const question = this.questions[this.tab];
|
|
12381
|
+
const optionless = question !== void 0 && (question.options?.length ?? 0) === 0;
|
|
12382
|
+
this.editingOther = optionless;
|
|
12383
|
+
if (optionless) {
|
|
12384
|
+
const draft = this.draft();
|
|
12385
|
+
this.otherInput.setValue(draft?.custom ?? "");
|
|
12386
|
+
} else {
|
|
12387
|
+
const recommended = this.rows().findIndex((row) => row.recommended);
|
|
12388
|
+
this.cursor = recommended >= 0 ? recommended : 0;
|
|
12389
|
+
}
|
|
12390
|
+
this.otherInput.focused = this.focused && this.editingOther;
|
|
12391
|
+
}
|
|
12392
|
+
/** Skip the current question (empty answer) and move on. */
|
|
12393
|
+
skip() {
|
|
12394
|
+
const draft = this.draft();
|
|
12395
|
+
if (draft === void 0) return;
|
|
12396
|
+
draft.selected.clear();
|
|
12397
|
+
draft.custom = "";
|
|
12398
|
+
draft.skipped = true;
|
|
12399
|
+
this.advance();
|
|
12400
|
+
}
|
|
12401
|
+
/** Enter the free-text editing mode for the current question. */
|
|
12402
|
+
enterOther() {
|
|
12403
|
+
this.editingOther = true;
|
|
12404
|
+
const draft = this.draft();
|
|
12405
|
+
this.otherInput.setValue(draft?.custom ?? "");
|
|
12406
|
+
this.otherInput.focused = this.focused;
|
|
12407
|
+
}
|
|
12408
|
+
/** Leave text mode back to the option list. */
|
|
12409
|
+
exitOther() {
|
|
12410
|
+
this.editingOther = false;
|
|
12411
|
+
this.otherInput.focused = false;
|
|
12412
|
+
}
|
|
12413
|
+
/** Commit the typed text into the draft and advance. */
|
|
12414
|
+
commitOther(value) {
|
|
12415
|
+
const draft = this.draft();
|
|
12416
|
+
if (draft === void 0) return;
|
|
12417
|
+
const text = value.trim();
|
|
12418
|
+
draft.custom = text;
|
|
12419
|
+
if (text === "") draft.skipped = true;
|
|
12420
|
+
else {
|
|
12421
|
+
if (this.questions[this.tab]?.multiSelect !== true) draft.selected.clear();
|
|
12422
|
+
draft.skipped = false;
|
|
12423
|
+
}
|
|
12424
|
+
this.exitOther();
|
|
12425
|
+
this.advance();
|
|
12426
|
+
}
|
|
12427
|
+
/** Submit the whole batch from the review page. */
|
|
12428
|
+
submit() {
|
|
12429
|
+
const answers = this.questions.map((question, index) => {
|
|
12430
|
+
const draft = this.drafts[index];
|
|
12431
|
+
if (draft.skipped) return {
|
|
12432
|
+
id: question.id,
|
|
12433
|
+
selected: []
|
|
12434
|
+
};
|
|
12435
|
+
const custom = draft.custom.trim();
|
|
12436
|
+
if (custom !== "" && question.multiSelect !== true) return {
|
|
12437
|
+
id: question.id,
|
|
12438
|
+
selected: [],
|
|
12439
|
+
custom
|
|
12440
|
+
};
|
|
12441
|
+
return {
|
|
12442
|
+
id: question.id,
|
|
12443
|
+
selected: [...draft.selected],
|
|
12444
|
+
...custom === "" ? {} : { custom }
|
|
12445
|
+
};
|
|
12446
|
+
});
|
|
12447
|
+
this.onDone(answers);
|
|
12448
|
+
}
|
|
12449
|
+
handleInput(data) {
|
|
12450
|
+
if (data === "\0") return;
|
|
12451
|
+
if (this.editingOther) {
|
|
12452
|
+
if (data === "\r" || data === "\n") this.commitOther(this.otherInput.getValue());
|
|
12453
|
+
else if (data === "\x1B") this.exitOther();
|
|
12454
|
+
else if (data === "\x1B[D") {
|
|
12455
|
+
if (this.tab > 0) {
|
|
12456
|
+
this.tab -= 1;
|
|
12457
|
+
this.cursor = 0;
|
|
12458
|
+
this.submitIdx = 0;
|
|
12459
|
+
this.syncEditMode();
|
|
12460
|
+
}
|
|
12461
|
+
} else if (data === "\x1B[C") {
|
|
12462
|
+
if (this.tab < this.questions.length - 1) {
|
|
12463
|
+
this.tab += 1;
|
|
12464
|
+
this.cursor = 0;
|
|
12465
|
+
this.submitIdx = 0;
|
|
12466
|
+
this.syncEditMode();
|
|
12467
|
+
} else {
|
|
12468
|
+
this.tab = this.questions.length;
|
|
12469
|
+
this.submitIdx = 0;
|
|
12470
|
+
this.syncEditMode();
|
|
12471
|
+
}
|
|
12472
|
+
} else this.otherInput.handleInput(data);
|
|
12473
|
+
return;
|
|
12474
|
+
}
|
|
12475
|
+
const rows = this.rows();
|
|
12476
|
+
if (this.tab >= this.questions.length) {
|
|
12477
|
+
if (data === "\r" || data === "\n") {
|
|
12478
|
+
if (this.submitIdx === 0) this.submit();
|
|
12479
|
+
else this.onCancel();
|
|
12480
|
+
} else if (data === "\x1B") this.onCancel();
|
|
12481
|
+
else if (data === "\x1B[D" || data === "h") this.submitIdx = 0;
|
|
12482
|
+
else if (data === "\x1B[C" || data === "l") this.submitIdx = 1;
|
|
12483
|
+
else if (data === "b") {
|
|
12484
|
+
this.tab = Math.max(0, this.questions.length - 1);
|
|
12485
|
+
this.cursor = 0;
|
|
12486
|
+
this.submitIdx = 0;
|
|
12487
|
+
this.syncEditMode();
|
|
12488
|
+
}
|
|
12489
|
+
return;
|
|
12490
|
+
}
|
|
12491
|
+
const digit = /^[1-9]$/.exec(data);
|
|
12492
|
+
if (digit !== null) {
|
|
12493
|
+
const row = rows[Number(digit[0]) - 1];
|
|
12494
|
+
if (row !== void 0) {
|
|
12495
|
+
this.cursor = rows.indexOf(row);
|
|
12496
|
+
this.confirm();
|
|
12497
|
+
}
|
|
12498
|
+
return;
|
|
12499
|
+
}
|
|
12500
|
+
if (data === "\x1B[A" || data === "k") {
|
|
12501
|
+
if (rows.length > 0) this.cursor = (this.cursor - 1 + rows.length) % rows.length;
|
|
12502
|
+
return;
|
|
12503
|
+
}
|
|
12504
|
+
if (data === "\x1B[B" || data === "j") {
|
|
12505
|
+
if (rows.length > 0) this.cursor = (this.cursor + 1) % rows.length;
|
|
12506
|
+
return;
|
|
12507
|
+
}
|
|
12508
|
+
if (data === "\r" || data === "\n") {
|
|
12509
|
+
this.confirm();
|
|
12510
|
+
return;
|
|
12511
|
+
}
|
|
12512
|
+
if (data === "\x1B[D" || data === "h") {
|
|
12513
|
+
if (this.tab > 0) {
|
|
12514
|
+
this.tab -= 1;
|
|
12515
|
+
this.cursor = 0;
|
|
12516
|
+
this.submitIdx = 0;
|
|
12517
|
+
this.syncEditMode();
|
|
12518
|
+
}
|
|
12519
|
+
return;
|
|
12520
|
+
}
|
|
12521
|
+
if (data === "\x1B[C" || data === "l") {
|
|
12522
|
+
if (this.tab < this.questions.length - 1) {
|
|
12523
|
+
this.tab += 1;
|
|
12524
|
+
this.cursor = 0;
|
|
12525
|
+
this.submitIdx = 0;
|
|
12526
|
+
} else {
|
|
12527
|
+
this.tab = this.questions.length;
|
|
12528
|
+
this.cursor = 0;
|
|
12529
|
+
this.submitIdx = 0;
|
|
12530
|
+
this.syncEditMode();
|
|
12531
|
+
}
|
|
12532
|
+
return;
|
|
12533
|
+
}
|
|
12534
|
+
if (data === "s") {
|
|
12535
|
+
this.skip();
|
|
12536
|
+
return;
|
|
12537
|
+
}
|
|
12538
|
+
if (data === "\x1B") this.onCancel();
|
|
12539
|
+
}
|
|
12540
|
+
invalidate() {
|
|
12541
|
+
this.otherInput.invalidate();
|
|
12542
|
+
}
|
|
12543
|
+
render(width) {
|
|
12544
|
+
const safeWidth = Math.max(1, width);
|
|
12545
|
+
const lines = [];
|
|
12546
|
+
const tabs = this.questions.map((_, index) => {
|
|
12547
|
+
const draft = this.drafts[index];
|
|
12548
|
+
const answered = draft !== void 0 && (draft.selected.size > 0 || draft.custom !== "" || draft.skipped);
|
|
12549
|
+
const label = tabLabel(index, this.questions.length);
|
|
12550
|
+
const mark = answered ? color.success("✓") : color.textDim("○");
|
|
12551
|
+
return this.tab === index ? color.textStrong(` ${mark} ${label} `) : color.textDim(` ${mark} ${label} `);
|
|
12552
|
+
});
|
|
12553
|
+
const submitAnswered = this.drafts.every((draft) => draft.selected.size > 0 || draft.custom !== "" || draft.skipped);
|
|
12554
|
+
const submitTab = this.tab === this.questions.length;
|
|
12555
|
+
tabs.push(submitTab ? color.textStrong(` ${submitAnswered ? color.success("✓") : color.textDim("○")} Submit `) : color.textDim(` ${submitAnswered ? color.success("✓") : color.textDim("○")} Submit `));
|
|
12556
|
+
lines.push(tabs.join(""));
|
|
12557
|
+
if (this.tab >= this.questions.length) {
|
|
12558
|
+
lines.push("");
|
|
12559
|
+
lines.push(color.textStrong("Review your answer before submit"));
|
|
12560
|
+
this.questions.forEach((question, index) => {
|
|
12561
|
+
const draft = this.drafts[index];
|
|
12562
|
+
if (draft === void 0) return;
|
|
12563
|
+
const value = draft.skipped ? "(skipped)" : draft.custom !== "" && question.multiSelect !== true ? draft.custom : [...draft.selected].join(", ") + (draft.custom !== "" ? ` + ${draft.custom}` : "");
|
|
12564
|
+
lines.push(`${color.textDim(`Q${index + 1}`)} ${question.question}`);
|
|
12565
|
+
lines.push(` ${value === "" ? color.textDim("(no answer)") : value}`);
|
|
12566
|
+
});
|
|
12567
|
+
lines.push("");
|
|
12568
|
+
const submit = this.submitIdx === 0 ? color.textStrong("Submit") : color.textDim("Submit");
|
|
12569
|
+
const cancel = this.submitIdx === 1 ? color.textStrong("Cancel") : color.textDim("Cancel");
|
|
12570
|
+
lines.push(`${submit} ${cancel}`);
|
|
12571
|
+
lines.push(color.textDim("← → choose · b back · enter confirm · esc cancel"));
|
|
12572
|
+
return lines;
|
|
12573
|
+
}
|
|
12574
|
+
const question = this.questions[this.tab];
|
|
12575
|
+
const draft = this.draft();
|
|
12576
|
+
if (question === void 0 || draft === void 0) return lines;
|
|
12577
|
+
if (question.header !== void 0 && question.header !== "") lines.push(color.textDim(question.header));
|
|
12578
|
+
lines.push(color.textStrong(question.question));
|
|
12579
|
+
if (question.detail !== void 0 && question.detail !== "") for (const line of question.detail.split("\n")) lines.push(color.textDim(truncateToWidth(line, safeWidth, "…")));
|
|
12580
|
+
const rows = this.rows();
|
|
12581
|
+
const multi = question.multiSelect === true;
|
|
12582
|
+
this.editingOther && rows.some((row) => row.key === OTHER_ROW);
|
|
12583
|
+
if (rows.length > 0 && !(rows.length === 1 && rows[0]?.key === OTHER_ROW)) rows.forEach((row, index) => {
|
|
12584
|
+
if (row.key === OTHER_ROW && this.editingOther) {
|
|
12585
|
+
const prefix = color.primary("→");
|
|
12586
|
+
const inputLine = this.otherInput.render(Math.max(1, safeWidth - visibleWidth(prefix) - 2))[0] ?? "";
|
|
12587
|
+
const stripped = inputLine.startsWith("> ") ? inputLine.slice(2) : inputLine;
|
|
12588
|
+
lines.push(`${prefix} ${color.textDim(`[${index + 1}]`)} ${stripped}`);
|
|
12589
|
+
return;
|
|
12590
|
+
}
|
|
12591
|
+
const selected = row.key === OTHER_ROW ? draft.custom !== "" : draft.selected.has(question.options?.[Number(row.key)]?.label ?? "");
|
|
12592
|
+
const marker = multi || row.key === OTHER_ROW ? selected ? color.success("[✓]") : color.textDim("[ ]") : selected ? color.success(`[${Number(row.key) + 1}]`) : color.textDim(`[${Number(row.key) + 1}]`);
|
|
12593
|
+
const pointer = index === this.cursor ? color.primary("→") : " ";
|
|
12594
|
+
const badge = row.recommended ? ` ${color.primary("[recommended]")}` : "";
|
|
12595
|
+
const desc = row.description === void 0 ? "" : color.textDim(` — ${row.description}`);
|
|
12596
|
+
const label = index === this.cursor ? color.textStrong(row.label) : row.label;
|
|
12597
|
+
lines.push(`${pointer} ${marker} ${label}${badge}${desc}`);
|
|
12598
|
+
});
|
|
12599
|
+
else if (this.editingOther || (question.options?.length ?? 0) === 0) {
|
|
12600
|
+
const inputLine = this.otherInput.render(Math.max(1, safeWidth - 2))[0] ?? "";
|
|
12601
|
+
const stripped = inputLine.startsWith("> ") ? inputLine.slice(2) : inputLine;
|
|
12602
|
+
lines.push(` ${stripped}`);
|
|
12603
|
+
lines.push(color.textDim("type your answer, enter to confirm"));
|
|
12604
|
+
}
|
|
12605
|
+
if (draft.skipped) lines.push(color.textDim("(skipped)"));
|
|
12606
|
+
lines.push("");
|
|
12607
|
+
const hintParts = [
|
|
12608
|
+
"↑↓ navigate",
|
|
12609
|
+
"1-9 choose",
|
|
12610
|
+
multi ? "enter toggle" : "enter confirm",
|
|
12611
|
+
"←→ switch",
|
|
12612
|
+
"s skip",
|
|
12613
|
+
"esc cancel"
|
|
12614
|
+
];
|
|
12615
|
+
lines.push(color.textDim(hintParts.join(" · ")));
|
|
12616
|
+
return lines;
|
|
12617
|
+
}
|
|
12618
|
+
};
|
|
12619
|
+
//#endregion
|
|
11909
12620
|
//#region src/working.ts
|
|
11910
12621
|
/**
|
|
11911
12622
|
* The animated busy indicator shown on the row directly above the editor
|
|
@@ -11960,7 +12671,7 @@ var WorkingIndicator = class extends Text {
|
|
|
11960
12671
|
}
|
|
11961
12672
|
updateDisplay() {
|
|
11962
12673
|
const frame = this.frames[this.currentFrame] ?? "";
|
|
11963
|
-
const indicator = frame === "" ? "" : frame + "
|
|
12674
|
+
const indicator = frame === "" ? "" : frame + " ";
|
|
11964
12675
|
this.setText(indicator + color.textDim(this.message));
|
|
11965
12676
|
this.ui.requestRender();
|
|
11966
12677
|
}
|
|
@@ -11996,12 +12707,13 @@ var Frame = class {
|
|
|
11996
12707
|
render(width) {
|
|
11997
12708
|
const inner = Math.max(1, Math.floor(width) - 4);
|
|
11998
12709
|
const lines = this.child.render(inner).map((line) => truncateToWidth(line, inner, "…"));
|
|
11999
|
-
const
|
|
12710
|
+
const contentWidth = Math.min(inner, Math.max(1, ...lines.map((line) => visibleWidth(line))));
|
|
12711
|
+
const frameWidth = contentWidth + 4;
|
|
12000
12712
|
const b = color.border;
|
|
12001
12713
|
const out = [b(`╭${"─".repeat(frameWidth - 2)}╮`)];
|
|
12002
12714
|
for (const line of lines) {
|
|
12003
12715
|
const vis = visibleWidth(line);
|
|
12004
|
-
const pad = Math.max(0,
|
|
12716
|
+
const pad = Math.max(0, contentWidth - vis);
|
|
12005
12717
|
out.push(`${b("│")} ${line}${" ".repeat(pad)} ${b("│")}`);
|
|
12006
12718
|
}
|
|
12007
12719
|
out.push(b(`╰${"─".repeat(frameWidth - 2)}╯`));
|
|
@@ -12013,11 +12725,22 @@ var Frame = class {
|
|
|
12013
12725
|
* border below it (a fixed-width rule looked misaligned next to the frame). */
|
|
12014
12726
|
var WelcomeCard = class {
|
|
12015
12727
|
facts;
|
|
12728
|
+
idle = false;
|
|
12016
12729
|
lastWidth = -1;
|
|
12017
12730
|
cached = [];
|
|
12018
12731
|
/** Replace the facts; the next render rebuilds the card. */
|
|
12019
12732
|
setFacts(facts) {
|
|
12020
12733
|
this.facts = facts;
|
|
12734
|
+
this.idle = false;
|
|
12735
|
+
this.cached = [];
|
|
12736
|
+
}
|
|
12737
|
+
/**
|
|
12738
|
+
* The pre-session state (deferred session creation): the card invites the
|
|
12739
|
+
* first message instead of naming a session that does not exist yet.
|
|
12740
|
+
*/
|
|
12741
|
+
setIdle(idle) {
|
|
12742
|
+
if (this.idle === idle) return;
|
|
12743
|
+
this.idle = idle;
|
|
12021
12744
|
this.cached = [];
|
|
12022
12745
|
}
|
|
12023
12746
|
invalidate() {
|
|
@@ -12025,29 +12748,47 @@ var WelcomeCard = class {
|
|
|
12025
12748
|
}
|
|
12026
12749
|
render(width) {
|
|
12027
12750
|
const facts = this.facts;
|
|
12751
|
+
const b = color.border;
|
|
12752
|
+
const inner = Math.max(1, width - 4);
|
|
12753
|
+
const renderRow = (line) => wrapTextWithAnsi(line, inner).map((wrapped) => {
|
|
12754
|
+
const vis = visibleWidth(wrapped);
|
|
12755
|
+
return `${b("│")} ${wrapped}${" ".repeat(Math.max(0, inner - vis))} ${b("│")}`;
|
|
12756
|
+
});
|
|
12757
|
+
if (this.idle) {
|
|
12758
|
+
if (this.lastWidth === width && this.cached.length > 0) return this.cached;
|
|
12759
|
+
this.lastWidth = width;
|
|
12760
|
+
this.cached = [
|
|
12761
|
+
b(`╭${"─".repeat(Math.max(0, width - 2))}╮`),
|
|
12762
|
+
...renderRow(color.textMuted("🐋 dsh-pi-tui — type a message to start a session")),
|
|
12763
|
+
b(`╰${"─".repeat(Math.max(0, width - 2))}╯`)
|
|
12764
|
+
];
|
|
12765
|
+
return this.cached;
|
|
12766
|
+
}
|
|
12028
12767
|
if (facts === void 0) return [];
|
|
12029
12768
|
if (this.lastWidth === width && this.cached.length > 0) return this.cached;
|
|
12030
12769
|
this.lastWidth = width;
|
|
12031
|
-
const line1 = `🐋
|
|
12032
|
-
const line2 = [color.
|
|
12033
|
-
const line3 = [color.textMuted(facts.cwd), `v${facts.version}`].filter((part) => part !== "").join(" · ");
|
|
12034
|
-
const inner = Math.max(1, width - 4);
|
|
12035
|
-
const b = color.border;
|
|
12770
|
+
const line1 = `🐋 session ${color.textMuted(facts.sessionId)}`;
|
|
12771
|
+
const line2 = [color.textMuted(facts.model), facts.preset === void 0 ? "" : `preset ${color.textMuted(facts.preset)}`].filter((part) => part !== "").join(" · ");
|
|
12772
|
+
const line3 = [color.textMuted(facts.cwd), color.textMuted(`v${facts.version}`)].filter((part) => part !== "").join(" · ");
|
|
12036
12773
|
this.cached = [
|
|
12037
12774
|
b(`╭${"─".repeat(Math.max(0, width - 2))}╮`),
|
|
12038
12775
|
...[
|
|
12039
12776
|
line1,
|
|
12040
12777
|
line2,
|
|
12041
12778
|
line3
|
|
12042
|
-
].flatMap(
|
|
12043
|
-
const vis = visibleWidth(wrapped);
|
|
12044
|
-
return `${b("│")} ${wrapped}${" ".repeat(Math.max(0, inner - vis))} ${b("│")}`;
|
|
12045
|
-
})),
|
|
12779
|
+
].flatMap(renderRow),
|
|
12046
12780
|
b(`╰${"─".repeat(Math.max(0, width - 2))}╯`)
|
|
12047
12781
|
];
|
|
12048
12782
|
return this.cached;
|
|
12049
12783
|
}
|
|
12050
12784
|
};
|
|
12785
|
+
/** One blank row between consecutive transcript blocks (kimi/pi Spacer(1) parity). */
|
|
12786
|
+
var Spacer = class {
|
|
12787
|
+
invalidate() {}
|
|
12788
|
+
render() {
|
|
12789
|
+
return [""];
|
|
12790
|
+
}
|
|
12791
|
+
};
|
|
12051
12792
|
/** Pi-style context progress bar: `[███░░░░░░░░░] 25%`. */
|
|
12052
12793
|
function contextBar(used, window) {
|
|
12053
12794
|
const ratio = Math.min(1, Math.max(0, used / window));
|
|
@@ -12094,6 +12835,22 @@ var TuiApp = class TuiApp {
|
|
|
12094
12835
|
todoPanelVisible = false;
|
|
12095
12836
|
/** The todo panel Text; empty when hidden. */
|
|
12096
12837
|
todoPanel;
|
|
12838
|
+
/**
|
|
12839
|
+
* The persistent dock strip directly above the todo panel: one line each
|
|
12840
|
+
* for the current permission, the live goal, the todo summary, and running
|
|
12841
|
+
* background tasks (kimi chrome parity). Empty lines drop out entirely.
|
|
12842
|
+
*/
|
|
12843
|
+
dock;
|
|
12844
|
+
/** Active background tasks for the dock line (label + status). */
|
|
12845
|
+
dockTasks = [];
|
|
12846
|
+
/**
|
|
12847
|
+
* The queued-input pane below the todo panel (kimi QueuePane parity):
|
|
12848
|
+
* a border rule plus one `❯ text` row per pending message and a dim hint.
|
|
12849
|
+
* Renders nothing while the queue is empty.
|
|
12850
|
+
*/
|
|
12851
|
+
queuePane;
|
|
12852
|
+
/** The pending inbox messages (next-turn followups and next-step steers). */
|
|
12853
|
+
queueItems = [];
|
|
12097
12854
|
/** Whether the Ctrl+O expansion master switch is on. */
|
|
12098
12855
|
isToolOutputExpanded() {
|
|
12099
12856
|
return this.toolOutputExpanded;
|
|
@@ -12111,6 +12868,9 @@ var TuiApp = class TuiApp {
|
|
|
12111
12868
|
searchComponent;
|
|
12112
12869
|
/** Overlay handles currently mounted on the active screen, for mode switches. */
|
|
12113
12870
|
overlayHandles = /* @__PURE__ */ new Set();
|
|
12871
|
+
/** Capturing overlays hidden beneath a newer one (modal stacking), keyed by
|
|
12872
|
+
* the newer overlay's handle; restored when it hides. */
|
|
12873
|
+
overlayDependents = /* @__PURE__ */ new Map();
|
|
12114
12874
|
/** Footer state. */
|
|
12115
12875
|
status = {
|
|
12116
12876
|
model: "",
|
|
@@ -12121,7 +12881,7 @@ var TuiApp = class TuiApp {
|
|
|
12121
12881
|
statsLine: ""
|
|
12122
12882
|
};
|
|
12123
12883
|
/** Header text (todo summary), kept for theme-swap repaints. */
|
|
12124
|
-
headerText = "🐋
|
|
12884
|
+
headerText = "🐋 dsh-pi-tui";
|
|
12125
12885
|
/** Footer text, kept for theme-swap repaints. */
|
|
12126
12886
|
footerText = "";
|
|
12127
12887
|
/** Plan-mode badge state; appended to the header and footer when active. */
|
|
@@ -12135,6 +12895,9 @@ var TuiApp = class TuiApp {
|
|
|
12135
12895
|
/** Transient error line shown under the transcript; cleared by the next
|
|
12136
12896
|
* repaint or after {@link TuiApp.NOTIFY_DURATION_MS}, whichever comes first. */
|
|
12137
12897
|
notifyText = "";
|
|
12898
|
+
/** Styling of the current notify line: info (default) is dim with a ℹ,
|
|
12899
|
+
* errors are red with a ✗. */
|
|
12900
|
+
notifyKind = "info";
|
|
12138
12901
|
/** The pending auto-clear for {@link notifyText}, while one is armed. */
|
|
12139
12902
|
notifyTimer;
|
|
12140
12903
|
/** How long a notify line stays before it auto-clears, in ms. */
|
|
@@ -12175,21 +12938,48 @@ var TuiApp = class TuiApp {
|
|
|
12175
12938
|
this.editorBorder = this.editor.borderColor;
|
|
12176
12939
|
this.editor.onSubmit = (text) => {
|
|
12177
12940
|
this.rememberInput(text);
|
|
12941
|
+
this.clearNotify();
|
|
12178
12942
|
this.events.onSubmit(text);
|
|
12179
12943
|
};
|
|
12180
|
-
this.header = new Text("🐋
|
|
12944
|
+
this.header = new Text("🐋 dsh-pi-tui", 0, 0);
|
|
12181
12945
|
this.messagesView = new Container();
|
|
12946
|
+
this.dock = new Text("", 0, 0);
|
|
12182
12947
|
this.todoPanel = new Text("", 0, 0);
|
|
12948
|
+
this.queuePane = new Text("", 0, 0);
|
|
12183
12949
|
this.working = new WorkingIndicator(this.tui, options.workingIntervalMs === void 0 ? {} : { intervalMs: options.workingIntervalMs });
|
|
12184
12950
|
this.footer = new Text("", 0, 0);
|
|
12185
12951
|
this.tui.addChild(this.header);
|
|
12186
12952
|
this.tui.addChild(this.messagesView);
|
|
12953
|
+
this.tui.addChild(this.dock);
|
|
12187
12954
|
this.tui.addChild(this.todoPanel);
|
|
12955
|
+
this.tui.addChild(this.queuePane);
|
|
12188
12956
|
this.tui.addChild(this.working);
|
|
12189
12957
|
this.tui.addChild(this.editor);
|
|
12190
12958
|
this.tui.addChild(this.footer);
|
|
12191
12959
|
this.tui.setFocus(this.editor);
|
|
12192
|
-
this.tui.addInputListener((data) => this.
|
|
12960
|
+
this.tui.addInputListener((data) => this.routeInput(data));
|
|
12961
|
+
}
|
|
12962
|
+
/**
|
|
12963
|
+
* App-level input routing plus the autocomplete repaint. The editor's
|
|
12964
|
+
* slash-command autocomplete resolves on a promise microtask AFTER the
|
|
12965
|
+
* keystroke's own paint, and the editor's own render requests go to the
|
|
12966
|
+
* MAIN screen — which is STOPPED while the alt screen renders (fullscreen),
|
|
12967
|
+
* so its renderRequested flag is stuck true and every request is dropped:
|
|
12968
|
+
* the fresh suggestion list would never paint (the visible frame always
|
|
12969
|
+
* shows the previous query's results — typing /res keeps showing /reload
|
|
12970
|
+
* first). Schedule a forced frame on the ACTIVE screen once the provider's
|
|
12971
|
+
* continuation has applied the fresh list. Deliberately a dsh-side fix:
|
|
12972
|
+
* the vendored fork stays pristine (see AGENTS.md decision 8).
|
|
12973
|
+
* @param data - the raw input sequence.
|
|
12974
|
+
*/
|
|
12975
|
+
routeInput(data) {
|
|
12976
|
+
const result = this.handleInput(data);
|
|
12977
|
+
queueMicrotask(() => {
|
|
12978
|
+
queueMicrotask(() => {
|
|
12979
|
+
if (this.editor.isShowingAutocomplete()) this.requestRender(true);
|
|
12980
|
+
});
|
|
12981
|
+
});
|
|
12982
|
+
return result;
|
|
12193
12983
|
}
|
|
12194
12984
|
/** Enter raw mode and start rendering. */
|
|
12195
12985
|
start() {
|
|
@@ -12231,6 +13021,16 @@ var TuiApp = class TuiApp {
|
|
|
12231
13021
|
this.startTranscriptSearch();
|
|
12232
13022
|
return { consume: true };
|
|
12233
13023
|
}
|
|
13024
|
+
if (matchesKey(data, "shift+tab")) {
|
|
13025
|
+
if (this.overlayHost.hasOverlayEntries) return void 0;
|
|
13026
|
+
this.events.onCyclePermission?.();
|
|
13027
|
+
return { consume: true };
|
|
13028
|
+
}
|
|
13029
|
+
if (matchesKey(data, "alt+up")) {
|
|
13030
|
+
if (this.overlayHost.hasOverlayEntries) return void 0;
|
|
13031
|
+
this.events.onDequeue?.();
|
|
13032
|
+
return { consume: true };
|
|
13033
|
+
}
|
|
12234
13034
|
if (matchesKey(data, "escape")) {
|
|
12235
13035
|
if (this.overlayHost.hasOverlayEntries) return void 0;
|
|
12236
13036
|
if (this.events.onSingleEscape?.() === true) return { consume: true };
|
|
@@ -12257,7 +13057,6 @@ var TuiApp = class TuiApp {
|
|
|
12257
13057
|
if (matchesKey(data, "ctrl+s")) {
|
|
12258
13058
|
if (this.overlayHost.hasOverlayEntries) return { consume: true };
|
|
12259
13059
|
const draft = this.editor.getText();
|
|
12260
|
-
if (draft.trim() === "") return { consume: true };
|
|
12261
13060
|
this.editor.setText("");
|
|
12262
13061
|
this.events.onSteer?.(draft);
|
|
12263
13062
|
return { consume: true };
|
|
@@ -12283,6 +13082,11 @@ var TuiApp = class TuiApp {
|
|
|
12283
13082
|
/**
|
|
12284
13083
|
* Show an overlay on the active screen and track its handle, so a
|
|
12285
13084
|
* fullscreen toggle can hide every mounted overlay on the old screen.
|
|
13085
|
+
* Capturing overlays stack modally: every other capturing overlay is
|
|
13086
|
+
* hidden beneath the new one and restored when it hides — the fork's
|
|
13087
|
+
* compositor interleaves stacked boxes line by line (two different-width
|
|
13088
|
+
* dialogs would garble into one frame), and modal hiding is done here so
|
|
13089
|
+
* the vendored fork stays pristine.
|
|
12286
13090
|
* @param component - the overlay content.
|
|
12287
13091
|
* @param options - overlay sizing/positioning.
|
|
12288
13092
|
* @returns the handle; hide() also forgets the handle.
|
|
@@ -12290,9 +13094,19 @@ var TuiApp = class TuiApp {
|
|
|
12290
13094
|
showOverlayOnHost(component, options) {
|
|
12291
13095
|
const handle = this.overlayHost.showOverlay(component, options);
|
|
12292
13096
|
this.overlayHandles.add(handle);
|
|
13097
|
+
if (options?.nonCapturing !== true) {
|
|
13098
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
13099
|
+
for (const other of this.overlayHandles) if (other !== handle && !other.isHidden()) {
|
|
13100
|
+
other.setHidden(true);
|
|
13101
|
+
hidden.add(other);
|
|
13102
|
+
}
|
|
13103
|
+
if (hidden.size > 0) this.overlayDependents.set(handle, hidden);
|
|
13104
|
+
}
|
|
12293
13105
|
return {
|
|
12294
13106
|
...handle,
|
|
12295
13107
|
hide: () => {
|
|
13108
|
+
this.overlayDependents.get(handle)?.forEach((other) => other.setHidden(false));
|
|
13109
|
+
this.overlayDependents.delete(handle);
|
|
12296
13110
|
this.overlayHandles.delete(handle);
|
|
12297
13111
|
handle.hide();
|
|
12298
13112
|
}
|
|
@@ -12416,10 +13230,18 @@ var TuiApp = class TuiApp {
|
|
|
12416
13230
|
component: this.fullscreenScroll,
|
|
12417
13231
|
grow: 1
|
|
12418
13232
|
},
|
|
13233
|
+
{
|
|
13234
|
+
component: this.dock,
|
|
13235
|
+
shrink: 0
|
|
13236
|
+
},
|
|
12419
13237
|
{
|
|
12420
13238
|
component: this.todoPanel,
|
|
12421
13239
|
shrink: 0
|
|
12422
13240
|
},
|
|
13241
|
+
{
|
|
13242
|
+
component: this.queuePane,
|
|
13243
|
+
shrink: 0
|
|
13244
|
+
},
|
|
12423
13245
|
{
|
|
12424
13246
|
component: this.working,
|
|
12425
13247
|
shrink: 0
|
|
@@ -12434,7 +13256,7 @@ var TuiApp = class TuiApp {
|
|
|
12434
13256
|
}
|
|
12435
13257
|
]);
|
|
12436
13258
|
alt.setLayoutRoot(root);
|
|
12437
|
-
alt.addInputListener((data) => this.
|
|
13259
|
+
alt.addInputListener((data) => this.routeInput(data));
|
|
12438
13260
|
this.tui.stop();
|
|
12439
13261
|
alt.start();
|
|
12440
13262
|
alt.setFocus(this.editor);
|
|
@@ -12496,7 +13318,6 @@ var TuiApp = class TuiApp {
|
|
|
12496
13318
|
*/
|
|
12497
13319
|
setTranscript(messages) {
|
|
12498
13320
|
this.messages = messages;
|
|
12499
|
-
this.clearNotify();
|
|
12500
13321
|
this.rebuildMessages();
|
|
12501
13322
|
}
|
|
12502
13323
|
/** Rebuild the message component tree from the current transcript state. */
|
|
@@ -12506,24 +13327,21 @@ var TuiApp = class TuiApp {
|
|
|
12506
13327
|
const boundary = this.expandBoundary();
|
|
12507
13328
|
const width = this.terminal.columns;
|
|
12508
13329
|
const rows = [];
|
|
12509
|
-
|
|
12510
|
-
|
|
12511
|
-
const component = this.renderMessage(message, boundary);
|
|
12512
|
-
this.messagesView.addChild(component);
|
|
12513
|
-
rows.push({
|
|
12514
|
-
message,
|
|
12515
|
-
height: component.render(width).length
|
|
12516
|
-
});
|
|
12517
|
-
}
|
|
12518
|
-
for (const message of this.localMessages) {
|
|
13330
|
+
const blocks = [...this.messages.filter((message) => !(message.kind === "thinking" && this.hideThinking)), ...this.localMessages];
|
|
13331
|
+
blocks.forEach((message, index) => {
|
|
12519
13332
|
const component = this.renderMessage(message, boundary);
|
|
12520
13333
|
this.messagesView.addChild(component);
|
|
13334
|
+
const height = component.render(width).length + (index < blocks.length - 1 ? 1 : 0);
|
|
12521
13335
|
rows.push({
|
|
12522
13336
|
message,
|
|
12523
|
-
height
|
|
13337
|
+
height
|
|
12524
13338
|
});
|
|
13339
|
+
if (index < blocks.length - 1) this.messagesView.addChild(new Spacer());
|
|
13340
|
+
});
|
|
13341
|
+
if (this.notifyText !== "") {
|
|
13342
|
+
const line = this.notifyKind === "info" ? color.textDim(`ℹ ${this.notifyText}`) : color.error(`✗ ${this.notifyText}`);
|
|
13343
|
+
this.messagesView.addChild(new Text(line, 0, 0));
|
|
12525
13344
|
}
|
|
12526
|
-
if (this.notifyText !== "") this.messagesView.addChild(new Text(color.error(`✗ ${this.notifyText}`), 0, 0));
|
|
12527
13345
|
this.messageRows = rows;
|
|
12528
13346
|
this.renderTodoPanel();
|
|
12529
13347
|
this.requestRender();
|
|
@@ -12589,12 +13407,16 @@ var TuiApp = class TuiApp {
|
|
|
12589
13407
|
this.requestRender();
|
|
12590
13408
|
}
|
|
12591
13409
|
/**
|
|
12592
|
-
* Show a transient
|
|
13410
|
+
* Show a transient line under the transcript. Cleared by the next
|
|
12593
13411
|
* `setTranscript` repaint or after {@link TuiApp.NOTIFY_DURATION_MS},
|
|
12594
13412
|
* whichever comes first, so a one-off notice never lingers forever.
|
|
13413
|
+
* @param text - the notice text.
|
|
13414
|
+
* @param kind - `'info'` renders dim with a ℹ (default, the common case);
|
|
13415
|
+
* `'error'` renders red with a ✗ — pass it explicitly for failures.
|
|
12595
13416
|
*/
|
|
12596
|
-
notify(text) {
|
|
13417
|
+
notify(text, kind = "info") {
|
|
12597
13418
|
this.notifyText = text;
|
|
13419
|
+
this.notifyKind = kind;
|
|
12598
13420
|
if (this.notifyTimer !== void 0) clearTimeout(this.notifyTimer);
|
|
12599
13421
|
this.notifyTimer = setTimeout(() => {
|
|
12600
13422
|
this.notifyTimer = void 0;
|
|
@@ -12603,13 +13425,25 @@ var TuiApp = class TuiApp {
|
|
|
12603
13425
|
}, this.notifyDurationMs);
|
|
12604
13426
|
this.rebuildMessages();
|
|
12605
13427
|
}
|
|
12606
|
-
/** Clear the transient notify line and its pending auto-clear timer.
|
|
13428
|
+
/** Clear the transient notify line and its pending auto-clear timer.
|
|
13429
|
+
* Called on fresh user input and session switches; repaints alone never
|
|
13430
|
+
* clear it (an active session would flash every notice away). Rebuilds
|
|
13431
|
+
* the transcript when a notice was actually showing. */
|
|
12607
13432
|
clearNotify() {
|
|
13433
|
+
if (this.notifyText === "" && this.notifyTimer === void 0) return;
|
|
12608
13434
|
this.notifyText = "";
|
|
12609
13435
|
if (this.notifyTimer !== void 0) {
|
|
12610
13436
|
clearTimeout(this.notifyTimer);
|
|
12611
13437
|
this.notifyTimer = void 0;
|
|
12612
13438
|
}
|
|
13439
|
+
this.rebuildMessages();
|
|
13440
|
+
}
|
|
13441
|
+
/**
|
|
13442
|
+
* Replace the editor draft. The runner restores a submission that the
|
|
13443
|
+
* divergence guard blocked, so the user's text survives for a retry.
|
|
13444
|
+
*/
|
|
13445
|
+
setEditorText(text) {
|
|
13446
|
+
this.editor.setText(text);
|
|
12613
13447
|
}
|
|
12614
13448
|
/**
|
|
12615
13449
|
* Set the session head rendered above the transcript: the session identity,
|
|
@@ -12621,6 +13455,14 @@ var TuiApp = class TuiApp {
|
|
|
12621
13455
|
this.welcomeCard.setFacts(facts);
|
|
12622
13456
|
this.rebuildMessages();
|
|
12623
13457
|
}
|
|
13458
|
+
/**
|
|
13459
|
+
* Toggle the pre-session welcome state (deferred session creation): the
|
|
13460
|
+
* card invites the first message; the first real facts replace it.
|
|
13461
|
+
*/
|
|
13462
|
+
setWelcomeIdle(idle) {
|
|
13463
|
+
this.welcomeCard.setIdle(idle);
|
|
13464
|
+
this.rebuildMessages();
|
|
13465
|
+
}
|
|
12624
13466
|
/** The turn threshold at or above which collapsible entries expand. */
|
|
12625
13467
|
expandBoundary() {
|
|
12626
13468
|
if (!this.toolOutputExpanded || false) return Number.POSITIVE_INFINITY;
|
|
@@ -12632,29 +13474,49 @@ var TuiApp = class TuiApp {
|
|
|
12632
13474
|
}
|
|
12633
13475
|
/** Render one transcript message as a pi-tui component. */
|
|
12634
13476
|
renderMessage(message, boundary) {
|
|
12635
|
-
if (message.kind === "user")
|
|
13477
|
+
if (message.kind === "user") {
|
|
13478
|
+
const prefix = `${color.roleUser("❯")} `;
|
|
13479
|
+
const indent = " ".repeat(visibleWidth(prefix));
|
|
13480
|
+
return new Text(new Text(message.text, 0, 0).render(Math.max(1, this.terminal.columns - visibleWidth(prefix))).map((line, index) => (index === 0 ? prefix : indent) + line).join("\n"), 0, 0);
|
|
13481
|
+
}
|
|
12636
13482
|
if (message.kind === "assistant") {
|
|
12637
|
-
const
|
|
12638
|
-
|
|
12639
|
-
|
|
12640
|
-
|
|
13483
|
+
const prefix = `${color.primary("🐋")} `;
|
|
13484
|
+
const indent = " ".repeat(visibleWidth(prefix));
|
|
13485
|
+
return new Text(new Markdown(message.text, 0, 0, markdownTheme).render(Math.max(1, this.terminal.columns - visibleWidth(prefix))).map((line, index) => (index === 0 ? prefix : indent) + line).join("\n"), 0, 0);
|
|
13486
|
+
}
|
|
13487
|
+
if (message.kind === "thinking") {
|
|
13488
|
+
if (message.turn >= boundary || this.expandedOverride.get(message) === true) return new Text(color.textDimItalic(`🐳 ${message.text}`), 0, 0);
|
|
13489
|
+
const lines = message.text.split("\n").filter((line) => line !== "");
|
|
13490
|
+
const shown = message.running === true ? lines.slice(-2) : lines.slice(0, 2);
|
|
13491
|
+
const prefix = "🐳 ";
|
|
13492
|
+
const prefixWidth = visibleWidth(prefix);
|
|
13493
|
+
const contentWidth = Math.max(1, this.terminal.columns - prefixWidth);
|
|
13494
|
+
const rows = Array.from({ length: 2 }, (_, index) => {
|
|
13495
|
+
const text = shown[index] ?? "";
|
|
13496
|
+
const body = text === "" ? "" : truncateToWidth(text, contentWidth, "…");
|
|
13497
|
+
const pad = index === 0 ? prefix : " ".repeat(prefixWidth);
|
|
13498
|
+
return color.textDimItalic(pad + body);
|
|
13499
|
+
});
|
|
13500
|
+
const remaining = lines.length - 2;
|
|
13501
|
+
const hint = remaining > 0 ? `... (${remaining} more lines, ctrl+o to expand)` : "(ctrl+o to expand)";
|
|
13502
|
+
rows.push(color.textDim(`${" ".repeat(prefixWidth)}${truncateToWidth(hint, contentWidth, "…")}`));
|
|
13503
|
+
return new Text(rows.join("\n"), 0, 0);
|
|
12641
13504
|
}
|
|
12642
|
-
if (message.kind === "thinking") return new Text(message.turn >= boundary || this.expandedOverride.get(message) === true ? color.textDim(`🐳 ${message.text}`) : color.textDim(`🐳 ${message.running === true ? preview(latestLine(message.text), 1) : preview(firstLine(message.text), 1)} (ctrl+o to expand)`), 0, 0);
|
|
12643
13505
|
if (message.kind === "system") {
|
|
12644
13506
|
const expanded = message.turn >= boundary || this.expandedOverride.get(message) === true;
|
|
12645
13507
|
if (message.label !== void 0) {
|
|
12646
13508
|
const row = new Container();
|
|
12647
13509
|
const emoji = message.emoji ?? "📎";
|
|
12648
13510
|
if (expanded) {
|
|
12649
|
-
row.addChild(new Text(color.textMuted(`${emoji}
|
|
13511
|
+
row.addChild(new Text(color.textMuted(`${emoji} Context injection ${message.label}`), 0, 0));
|
|
12650
13512
|
row.addChild(new Text(color.textDim(message.text), 0, 0));
|
|
12651
13513
|
} else {
|
|
12652
13514
|
const summary = message.summary === void 0 ? "" : ` — ${message.summary}`;
|
|
12653
|
-
row.addChild(new Text(color.textMuted(`${emoji}
|
|
13515
|
+
row.addChild(new Text(truncateToWidth(color.textMuted(`${emoji} Context injection ${message.label}${summary} (ctrl+o to expand)`), this.terminal.columns, "…"), 0, 0));
|
|
12654
13516
|
}
|
|
12655
13517
|
return row;
|
|
12656
13518
|
}
|
|
12657
|
-
return new Text(expanded ? `${color.textMuted("§")} ${message.text}` : color.textMuted(`§ ${preview(message.text, 2)} (ctrl+o to expand)`), 0, 0);
|
|
13519
|
+
return new Text(expanded ? `${color.textMuted("§")} ${color.textDim(message.text)}` : color.textMuted(`§ ${truncateToWidth(preview(message.text, 2), Math.max(1, this.terminal.columns - 22), "…")} (ctrl+o to expand)`), 0, 0);
|
|
12658
13520
|
}
|
|
12659
13521
|
if (message.kind === "summary") return new Text(color.textDim(message.text), 0, 0);
|
|
12660
13522
|
const card = new Container();
|
|
@@ -12662,13 +13524,13 @@ var TuiApp = class TuiApp {
|
|
|
12662
13524
|
const summary = header.summary === "" ? "" : ` ${header.summary}`;
|
|
12663
13525
|
const emoji = toolEmoji(message.name);
|
|
12664
13526
|
const pill = message.status === "ok" ? color.success("[ok]") : message.status === "error" ? color.error("[error]") : color.textDim("[running]");
|
|
12665
|
-
const head = `${emoji}
|
|
13527
|
+
const head = `${color.textDim(`${emoji} ${header.title}${summary}`)} ${pill}`;
|
|
12666
13528
|
if (message.turn >= boundary || this.expandedOverride.get(message) === true) {
|
|
12667
13529
|
card.addChild(new Text(head, 0, 0));
|
|
12668
|
-
this.renderToolBody(card, message);
|
|
13530
|
+
this.renderToolBody(card, message, this.expandedOverride.get(message) === true);
|
|
12669
13531
|
} else {
|
|
12670
|
-
const resultPreview = message.result === "" ? "" : ` — ${preview(message.result, 3)}`;
|
|
12671
|
-
card.addChild(new Text(`${head}${resultPreview}`, 0, 0));
|
|
13532
|
+
const resultPreview = message.name === "read" ? readFoldedPreview(message.result) : message.result === "" ? "" : ` — ${preview(message.result, 3)}`;
|
|
13533
|
+
card.addChild(new Text(truncateToWidth(`${head}${resultPreview}`, this.terminal.columns, "…"), 0, 0));
|
|
12672
13534
|
}
|
|
12673
13535
|
return card;
|
|
12674
13536
|
}
|
|
@@ -12677,18 +13539,47 @@ var TuiApp = class TuiApp {
|
|
|
12677
13539
|
* the body follows the tool's own render intent (presentResult): a read
|
|
12678
13540
|
* card shows numbered lines plus the relativized path and total line
|
|
12679
13541
|
* count, a search card groups matches by file and marks truncation, a
|
|
12680
|
-
* terminal card shows the output and exit status, and a diff card
|
|
12681
|
-
* the
|
|
12682
|
-
*
|
|
13542
|
+
* terminal card shows the output and exit status, and a diff card shows
|
|
13543
|
+
* the LCS-aligned, clustered diff (capped in the default view, full when
|
|
13544
|
+
* the card was explicitly expanded). Without a view the raw result text
|
|
13545
|
+
* renders, diff-colored when it looks like one; a diff-card call whose
|
|
13546
|
+
* result carried no view reuses the call-time diff so the block never
|
|
13547
|
+
* collapses (kimi parity).
|
|
12683
13548
|
* @param card - the card container to fill.
|
|
12684
13549
|
* @param message - the tool message.
|
|
13550
|
+
* @param explicitlyExpanded - whether the user expanded this card by hand.
|
|
12685
13551
|
*/
|
|
12686
|
-
renderToolBody(card, message) {
|
|
13552
|
+
renderToolBody(card, message, explicitlyExpanded) {
|
|
13553
|
+
if (message.name === "workflow" && message.members !== void 0) {
|
|
13554
|
+
const groups = /* @__PURE__ */ new Map();
|
|
13555
|
+
for (const member of message.members) {
|
|
13556
|
+
const key = member.phase ?? "";
|
|
13557
|
+
const list = groups.get(key);
|
|
13558
|
+
if (list === void 0) groups.set(key, [member]);
|
|
13559
|
+
else list.push(member);
|
|
13560
|
+
}
|
|
13561
|
+
for (const [phase, members] of groups) {
|
|
13562
|
+
if (phase !== "") card.addChild(new Text(color.textMuted(` ${phase}`), 0, 0));
|
|
13563
|
+
for (const member of members) {
|
|
13564
|
+
const mark = member.status === "ok" ? color.success("•") : member.status === "error" ? color.error("✗") : color.primary("●");
|
|
13565
|
+
const statusText = member.status === "ok" ? "completed" : member.status === "error" ? "failed" : "running";
|
|
13566
|
+
card.addChild(new Text(` ${mark} ${member.label} — ${color.textDim(statusText)}`, 0, 0));
|
|
13567
|
+
}
|
|
13568
|
+
}
|
|
13569
|
+
return;
|
|
13570
|
+
}
|
|
12687
13571
|
if (message.status === "running") {
|
|
12688
13572
|
const callView = this.present?.call(message.name, message.args);
|
|
12689
|
-
if (callView !== void 0
|
|
12690
|
-
|
|
12691
|
-
|
|
13573
|
+
if (callView !== void 0) {
|
|
13574
|
+
if (callView.card === "diff" && callView.diffs.length > 0) {
|
|
13575
|
+
this.renderDiffBody(card, callView.diffs, explicitlyExpanded);
|
|
13576
|
+
return;
|
|
13577
|
+
}
|
|
13578
|
+
if (callView.card === "generic" && callView.rawInput !== void 0) {
|
|
13579
|
+
const raw = typeof callView.rawInput === "string" ? callView.rawInput : JSON.stringify(callView.rawInput, null, 2);
|
|
13580
|
+
card.addChild(new Text(color.textDim(raw), 0, 0));
|
|
13581
|
+
return;
|
|
13582
|
+
}
|
|
12692
13583
|
}
|
|
12693
13584
|
return;
|
|
12694
13585
|
}
|
|
@@ -12723,16 +13614,52 @@ var TuiApp = class TuiApp {
|
|
|
12723
13614
|
return;
|
|
12724
13615
|
}
|
|
12725
13616
|
case "diff":
|
|
12726
|
-
|
|
13617
|
+
this.renderDiffBody(card, resultView.diffs, explicitlyExpanded);
|
|
12727
13618
|
return;
|
|
12728
13619
|
}
|
|
12729
|
-
if (
|
|
12730
|
-
|
|
13620
|
+
if (resultView === void 0) {
|
|
13621
|
+
const callView = this.present?.call(message.name, message.args);
|
|
13622
|
+
if (callView !== void 0 && callView.card === "diff" && callView.diffs.length > 0) {
|
|
13623
|
+
this.renderDiffBody(card, callView.diffs, explicitlyExpanded);
|
|
13624
|
+
return;
|
|
13625
|
+
}
|
|
13626
|
+
}
|
|
13627
|
+
if (message.name === "read") {
|
|
13628
|
+
const envelopes = parseReadEnvelopes(message.result);
|
|
13629
|
+
if (envelopes.length > 0) {
|
|
13630
|
+
if (envelopes.length === 1) {
|
|
13631
|
+
const envelope = envelopes[0];
|
|
13632
|
+
for (const line of envelope.lines) card.addChild(new Text(color.textDim(` ${line.number} │ ${line.text}`), 0, 0));
|
|
13633
|
+
if (envelope.path !== "") card.addChild(new Text(color.textMuted(` path: ${relativizeToCwd(envelope.path, this.workspaceRoot)}`), 0, 0));
|
|
13634
|
+
if (envelope.totalLines !== void 0) card.addChild(new Text(color.textMuted(` total lines: ${envelope.totalLines}`), 0, 0));
|
|
13635
|
+
} else envelopes.forEach((envelope, index) => {
|
|
13636
|
+
const branch = index === envelopes.length - 1 ? "└─" : "├─";
|
|
13637
|
+
const count = envelope.totalLines ?? (envelope.lines.length > 0 ? envelope.lines.length : void 0);
|
|
13638
|
+
card.addChild(new Text(color.textDim(` ${branch} ${relativizeToCwd(envelope.path, this.workspaceRoot)}${count === void 0 ? "" : ` · ${count} lines`}`), 0, 0));
|
|
13639
|
+
});
|
|
13640
|
+
return;
|
|
13641
|
+
}
|
|
13642
|
+
}
|
|
13643
|
+
if (isDiffResult(message.name, message.result)) for (const line of renderDiffLines(message.result)) card.addChild(new Text(line, 0, 0));
|
|
13644
|
+
else card.addChild(new Text(color.textDim(message.result), 0, 0));
|
|
13645
|
+
}
|
|
13646
|
+
/**
|
|
13647
|
+
* Fill a tool card with a diff body: LCS-aligned clustered lines, capped
|
|
13648
|
+
* at {@link DIFF_PREVIEW_LINES} in the default recent-turn view and full
|
|
13649
|
+
* when the card was explicitly expanded (kimi parity).
|
|
13650
|
+
* @param card - the card container to fill.
|
|
13651
|
+
* @param diffs - the diff hunks.
|
|
13652
|
+
* @param explicitlyExpanded - explicit expansion disables the cap.
|
|
13653
|
+
*/
|
|
13654
|
+
renderDiffBody(card, diffs, explicitlyExpanded) {
|
|
13655
|
+
for (const line of renderDiffView(diffs, this.workspaceRoot, { maxLines: explicitlyExpanded ? void 0 : 10 })) card.addChild(new Text(line, 0, 0));
|
|
12731
13656
|
}
|
|
12732
13657
|
/** Request a render on the active screen. Public so in-place submenu
|
|
12733
|
-
* components (async content swaps) can trigger the next frame.
|
|
12734
|
-
|
|
12735
|
-
|
|
13658
|
+
* components (async content swaps) can trigger the next frame. `force`
|
|
13659
|
+
* bypasses the render throttle (used to repaint the autocomplete list on
|
|
13660
|
+
* the keystroke's own frame). */
|
|
13661
|
+
requestRender(force = false) {
|
|
13662
|
+
(this.fullscreen ?? this.tui).requestRender(force);
|
|
12736
13663
|
}
|
|
12737
13664
|
/**
|
|
12738
13665
|
* Reflect the todo list in the header line: active (non-completed) count
|
|
@@ -12750,6 +13677,7 @@ var TuiApp = class TuiApp {
|
|
|
12750
13677
|
this.todoText = ` · ${active.length} active · ${label}`;
|
|
12751
13678
|
}
|
|
12752
13679
|
this.renderHeader();
|
|
13680
|
+
this.renderDock();
|
|
12753
13681
|
if (this.todoPanelVisible) this.renderTodoPanel();
|
|
12754
13682
|
}
|
|
12755
13683
|
/** Toggle the todo panel between the transcript and the editor. */
|
|
@@ -12802,7 +13730,7 @@ var TuiApp = class TuiApp {
|
|
|
12802
13730
|
renderHeader() {
|
|
12803
13731
|
const badge = this.planMode ? ` ${color.warning("[plan]")}` : "";
|
|
12804
13732
|
const title = this.sessionTitleText === "" ? "" : ` · ${color.textMuted(this.sessionTitleText)}`;
|
|
12805
|
-
this.headerText = `🐋
|
|
13733
|
+
this.headerText = `🐋 dsh-pi-tui${title}${this.todoText}${badge}`;
|
|
12806
13734
|
this.header.setText(this.headerText);
|
|
12807
13735
|
this.requestRender();
|
|
12808
13736
|
}
|
|
@@ -12818,6 +13746,87 @@ var TuiApp = class TuiApp {
|
|
|
12818
13746
|
...status
|
|
12819
13747
|
};
|
|
12820
13748
|
this.renderFooter();
|
|
13749
|
+
this.renderDock();
|
|
13750
|
+
}
|
|
13751
|
+
/**
|
|
13752
|
+
* Replace the active background-task list for the dock line.
|
|
13753
|
+
* @param tasks - active jobs (label + lifecycle status), empty to hide.
|
|
13754
|
+
*/
|
|
13755
|
+
setTasks(tasks) {
|
|
13756
|
+
this.dockTasks = tasks;
|
|
13757
|
+
this.renderDock();
|
|
13758
|
+
}
|
|
13759
|
+
/**
|
|
13760
|
+
* Replace the pending inbox rows for the queue pane (kimi QueuePane
|
|
13761
|
+
* parity): a border rule, one `❯ text` row per message, and a dim hint.
|
|
13762
|
+
* An empty queue renders nothing at all.
|
|
13763
|
+
* @param items - pending followups/steers, in delivery order.
|
|
13764
|
+
*/
|
|
13765
|
+
setQueueItems(items) {
|
|
13766
|
+
this.queueItems = items;
|
|
13767
|
+
this.renderQueuePane();
|
|
13768
|
+
}
|
|
13769
|
+
/** Rebuild the queue pane text from the current inbox rows. */
|
|
13770
|
+
renderQueuePane() {
|
|
13771
|
+
const items = this.queueItems;
|
|
13772
|
+
if (items.length === 0) {
|
|
13773
|
+
this.queuePane.setText("");
|
|
13774
|
+
this.requestRender();
|
|
13775
|
+
return;
|
|
13776
|
+
}
|
|
13777
|
+
const width = Math.max(1, this.terminal.columns);
|
|
13778
|
+
const lines = [color.border("─".repeat(width))];
|
|
13779
|
+
for (const item of items) {
|
|
13780
|
+
const prefix = `${color.accent("❯")} `;
|
|
13781
|
+
const truncated = truncateToWidth(item.text.replace(/\s+/g, " ").trim(), Math.max(1, width - visibleWidth(prefix)), "…");
|
|
13782
|
+
lines.push(prefix + truncated);
|
|
13783
|
+
}
|
|
13784
|
+
lines.push(color.textDim(truncateToWidth(` ctrl+s to steer all · alt+↑ to edit all · /queue for per-item actions`, Math.max(1, width - 2), "…")));
|
|
13785
|
+
this.queuePane.setText(lines.join("\n"));
|
|
13786
|
+
this.requestRender();
|
|
13787
|
+
}
|
|
13788
|
+
/**
|
|
13789
|
+
* Replace the editor draft wholesale (the Alt+↑ dequeue path pulls every
|
|
13790
|
+
* queued message back into the editor for editing).
|
|
13791
|
+
*/
|
|
13792
|
+
setDraft(text) {
|
|
13793
|
+
this.editor.setText(text);
|
|
13794
|
+
this.requestRender();
|
|
13795
|
+
}
|
|
13796
|
+
/** The editor's current draft text (the Alt+↑ dequeue merge reads it). */
|
|
13797
|
+
getDraft() {
|
|
13798
|
+
return this.editor.getText();
|
|
13799
|
+
}
|
|
13800
|
+
/**
|
|
13801
|
+
* Rebuild the persistent dock strip above the todo panel: goal, todo
|
|
13802
|
+
* summary, and background tasks — one truncated line each, only while
|
|
13803
|
+
* non-empty (kimi chrome parity). The dock is the "at a glance" surface
|
|
13804
|
+
* under the transcript; the full todo list stays on Ctrl+T. The
|
|
13805
|
+
* permission preset badges in the footer only, so the mode has exactly
|
|
13806
|
+
* one home (no dock/footer duplication).
|
|
13807
|
+
*/
|
|
13808
|
+
renderDock() {
|
|
13809
|
+
const lines = [];
|
|
13810
|
+
if (this.status.goal !== void 0 && this.status.goal !== "") lines.push(color.text(`⚑ ${this.status.goal}`));
|
|
13811
|
+
if (this.todoItems.length > 0) {
|
|
13812
|
+
const active = this.todoItems.filter((todo) => todo.status !== "completed");
|
|
13813
|
+
const done = this.todoItems.length - active.length;
|
|
13814
|
+
const first = active[0];
|
|
13815
|
+
const label = first === void 0 ? "" : first.content.length > 40 ? `${first.content.slice(0, 40)}…` : first.content;
|
|
13816
|
+
const summary = [
|
|
13817
|
+
done > 0 ? `${done} done` : "",
|
|
13818
|
+
active.length > 0 ? `${active.length} active` : "",
|
|
13819
|
+
label
|
|
13820
|
+
].filter((part) => part !== "").join(" · ");
|
|
13821
|
+
lines.push(color.textDim(`☑ ${summary}`));
|
|
13822
|
+
}
|
|
13823
|
+
if (this.dockTasks.length > 0) {
|
|
13824
|
+
const first = this.dockTasks[0];
|
|
13825
|
+
const label = first.label.length > 40 ? `${first.label.slice(0, 40)}…` : first.label;
|
|
13826
|
+
lines.push(color.textDim(`⏳ ${this.dockTasks.length} task${this.dockTasks.length === 1 ? "" : "s"} · ${label}`));
|
|
13827
|
+
}
|
|
13828
|
+
this.dock.setText(lines.join("\n"));
|
|
13829
|
+
this.requestRender();
|
|
12821
13830
|
}
|
|
12822
13831
|
/** Footer density presets: full keeps the stats line, compact drops it. */
|
|
12823
13832
|
footerPreset = "full";
|
|
@@ -12834,6 +13843,7 @@ var TuiApp = class TuiApp {
|
|
|
12834
13843
|
renderFooter() {
|
|
12835
13844
|
const context = this.status.contextTokens !== void 0 && this.status.contextWindow !== void 0 && this.status.contextWindow > 0 ? contextBar(this.status.contextTokens, this.status.contextWindow) : "";
|
|
12836
13845
|
const line1 = [
|
|
13846
|
+
this.status.permission === "danger-full-access" ? color.warning("[yolo]") : this.status.permission === "read-only" ? color.textMuted("[read-only]") : this.status.permission === "workspace-write" ? color.text("[workspace-write]") : this.status.permission === "custom" ? color.warning("[custom]") : "",
|
|
12837
13847
|
this.planMode ? color.warning("[plan]") : "",
|
|
12838
13848
|
this.status.goal === void 0 || this.status.goal === "" ? "" : color.primary(this.status.goal),
|
|
12839
13849
|
this.status.model === "" ? "" : `[${this.status.model}]`,
|
|
@@ -13045,11 +14055,15 @@ var TuiApp = class TuiApp {
|
|
|
13045
14055
|
return;
|
|
13046
14056
|
}
|
|
13047
14057
|
const state = {
|
|
13048
|
-
questions
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
|
|
13052
|
-
|
|
14058
|
+
flow: new QuestionFlow(questions.map((question) => ({
|
|
14059
|
+
id: question.id,
|
|
14060
|
+
question: question.question,
|
|
14061
|
+
...question.header !== void 0 ? { header: question.header } : {},
|
|
14062
|
+
...question.detail !== void 0 ? { detail: question.detail } : {},
|
|
14063
|
+
...question.options !== void 0 ? { options: question.options } : {},
|
|
14064
|
+
...question.multiSelect !== void 0 ? { multiSelect: question.multiSelect } : {},
|
|
14065
|
+
...question.intent !== void 0 ? { intent: question.intent } : {}
|
|
14066
|
+
})), (answers) => this.settleQuestions(state, answers), () => this.settleQuestions(state, void 0)),
|
|
13053
14067
|
resolve,
|
|
13054
14068
|
reject,
|
|
13055
14069
|
signal
|
|
@@ -13059,123 +14073,36 @@ var TuiApp = class TuiApp {
|
|
|
13059
14073
|
return;
|
|
13060
14074
|
}
|
|
13061
14075
|
if (signal !== void 0) {
|
|
13062
|
-
const onAbort = () => this.settleQuestions(state,
|
|
14076
|
+
const onAbort = () => this.settleQuestions(state, void 0);
|
|
13063
14077
|
state.onAbort = onAbort;
|
|
13064
14078
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
13065
14079
|
}
|
|
13066
14080
|
this.activeQuestions = state;
|
|
13067
|
-
this.
|
|
13068
|
-
|
|
13069
|
-
|
|
13070
|
-
|
|
13071
|
-
renderQuestion(state) {
|
|
13072
|
-
const question = state.questions[state.index];
|
|
13073
|
-
if (question === void 0) {
|
|
13074
|
-
this.settleQuestions(state, "done");
|
|
13075
|
-
return;
|
|
13076
|
-
}
|
|
13077
|
-
const dialog = new Box(1, 1);
|
|
13078
|
-
if (question.header !== void 0 && question.header !== "") dialog.addChild(new Text(color.textDim(question.header)));
|
|
13079
|
-
dialog.addChild(new Text(question.question));
|
|
13080
|
-
const options = question.options ?? [];
|
|
13081
|
-
if (options.length > 0) options.forEach((option, index) => {
|
|
13082
|
-
const checked = state.selected.get(question.id)?.has(option.label) === true ? color.success("✓") : " ";
|
|
13083
|
-
dialog.addChild(new Text(`${checked} ${index + 1}) ${option.label}${option.description === void 0 ? "" : ` — ${option.description}`}`));
|
|
13084
|
-
});
|
|
13085
|
-
else if (question.multiSelect !== true) {
|
|
13086
|
-
dialog.addChild(new Text(`> ${state.customText}`));
|
|
13087
|
-
dialog.addChild(new Text("(type an answer, enter to confirm)"));
|
|
13088
|
-
}
|
|
13089
|
-
dialog.addChild(new Text(""));
|
|
13090
|
-
const verb = question.multiSelect === true ? "toggle" : "select";
|
|
13091
|
-
dialog.addChild(new Text(`[1-9] ${verb} [enter] confirm [esc] cancel (${state.index + 1}/${state.questions.length})`));
|
|
13092
|
-
state.handle?.hide();
|
|
13093
|
-
state.handle = this.showOverlayOnHost(new Frame(dialog), {
|
|
13094
|
-
width: 72,
|
|
13095
|
-
maxHeight: 24
|
|
14081
|
+
state.handle = this.showOverlayOnHost(new Frame(state.flow), {
|
|
14082
|
+
width: 76,
|
|
14083
|
+
maxHeight: 26
|
|
14084
|
+
});
|
|
13096
14085
|
});
|
|
13097
14086
|
}
|
|
13098
|
-
/** Route a key while a question is showing; every key is consumed. */
|
|
14087
|
+
/** Route a key while a question flow is showing; every key is consumed. */
|
|
13099
14088
|
handleQuestionKey(data) {
|
|
13100
14089
|
const state = this.activeQuestions;
|
|
13101
14090
|
if (state === void 0) return void 0;
|
|
13102
|
-
|
|
13103
|
-
|
|
13104
|
-
this.settleQuestions(state, "done");
|
|
13105
|
-
return { consume: true };
|
|
13106
|
-
}
|
|
13107
|
-
const options = question.options ?? [];
|
|
13108
|
-
const digit = /^[1-9]$/.exec(data);
|
|
13109
|
-
if (digit !== null) {
|
|
13110
|
-
const option = options[Number(digit[0]) - 1];
|
|
13111
|
-
if (option !== void 0) {
|
|
13112
|
-
const selected = state.selected.get(question.id) ?? /* @__PURE__ */ new Set();
|
|
13113
|
-
if (question.multiSelect === true) {
|
|
13114
|
-
if (selected.has(option.label)) selected.delete(option.label);
|
|
13115
|
-
else selected.add(option.label);
|
|
13116
|
-
} else {
|
|
13117
|
-
selected.clear();
|
|
13118
|
-
selected.add(option.label);
|
|
13119
|
-
}
|
|
13120
|
-
state.selected.set(question.id, selected);
|
|
13121
|
-
this.renderQuestion(state);
|
|
13122
|
-
}
|
|
13123
|
-
return { consume: true };
|
|
13124
|
-
}
|
|
13125
|
-
if (matchesKey(data, "enter")) {
|
|
13126
|
-
if (options.length > 0) {
|
|
13127
|
-
state.index += 1;
|
|
13128
|
-
this.renderQuestion(state);
|
|
13129
|
-
} else if (question.multiSelect !== true) {
|
|
13130
|
-
state.custom.set(question.id, state.customText);
|
|
13131
|
-
state.customText = "";
|
|
13132
|
-
state.index += 1;
|
|
13133
|
-
this.renderQuestion(state);
|
|
13134
|
-
}
|
|
13135
|
-
return { consume: true };
|
|
13136
|
-
}
|
|
13137
|
-
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
13138
|
-
this.settleQuestions(state, "cancelled");
|
|
13139
|
-
return { consume: true };
|
|
13140
|
-
}
|
|
13141
|
-
if (options.length === 0 && question.multiSelect !== true) {
|
|
13142
|
-
if (data === "" || data === "\b") {
|
|
13143
|
-
state.customText = [...state.customText].slice(0, -1).join("");
|
|
13144
|
-
this.renderQuestion(state);
|
|
13145
|
-
} else {
|
|
13146
|
-
let appended = false;
|
|
13147
|
-
for (const char of data) if (char.charCodeAt(0) >= 32) {
|
|
13148
|
-
state.customText += char;
|
|
13149
|
-
appended = true;
|
|
13150
|
-
}
|
|
13151
|
-
if (appended) this.renderQuestion(state);
|
|
13152
|
-
}
|
|
13153
|
-
}
|
|
14091
|
+
state.flow.handleInput(data);
|
|
14092
|
+
this.requestRender();
|
|
13154
14093
|
return { consume: true };
|
|
13155
14094
|
}
|
|
13156
|
-
/** Resolve the question flow with its answers, or reject on cancel. */
|
|
13157
|
-
settleQuestions(state,
|
|
14095
|
+
/** Resolve the question flow with its answers, or reject on cancel/abort. */
|
|
14096
|
+
settleQuestions(state, answers) {
|
|
13158
14097
|
if (this.activeQuestions !== state) return;
|
|
13159
14098
|
this.activeQuestions = void 0;
|
|
13160
14099
|
state.handle?.hide();
|
|
13161
14100
|
if (state.onAbort !== void 0 && state.signal !== void 0) state.signal.removeEventListener("abort", state.onAbort);
|
|
13162
14101
|
this.overlayHost.setFocus(this.editor);
|
|
13163
|
-
if (
|
|
14102
|
+
if (answers === void 0) {
|
|
13164
14103
|
state.reject(/* @__PURE__ */ new Error("question flow cancelled"));
|
|
13165
14104
|
return;
|
|
13166
14105
|
}
|
|
13167
|
-
const answers = state.questions.map((question) => {
|
|
13168
|
-
const selected = [...state.selected.get(question.id) ?? []];
|
|
13169
|
-
const custom = (question.options ?? []).length === 0 && question.multiSelect !== true ? state.custom.get(question.id) : void 0;
|
|
13170
|
-
return custom === void 0 ? {
|
|
13171
|
-
id: question.id,
|
|
13172
|
-
selected
|
|
13173
|
-
} : {
|
|
13174
|
-
id: question.id,
|
|
13175
|
-
selected,
|
|
13176
|
-
custom
|
|
13177
|
-
};
|
|
13178
|
-
});
|
|
13179
14106
|
state.resolve(answers);
|
|
13180
14107
|
}
|
|
13181
14108
|
};
|
|
@@ -13455,15 +14382,30 @@ function metaOf(cwd, presetId) {
|
|
|
13455
14382
|
/**
|
|
13456
14383
|
* Register the TUI-owned slash commands on the commands service. The
|
|
13457
14384
|
* completion list is refreshed after every registration so TUI-owned
|
|
13458
|
-
* commands appear in the editor's tab list.
|
|
14385
|
+
* commands appear in the editor's tab list. Registration is sessionless:
|
|
14386
|
+
* the commands service's global layer needs no agent, so the whole surface
|
|
14387
|
+
* is available before the first session exists (deferred start).
|
|
13459
14388
|
* @param runner - the live runner surface.
|
|
14389
|
+
* @returns a hook that rebuilds the per-skill slash commands (a no-op until
|
|
14390
|
+
* the first session exists, since the skill catalog scope is the agent).
|
|
13460
14391
|
*/
|
|
13461
14392
|
function registerTuiCommands(runner) {
|
|
13462
14393
|
const { ctx, app } = runner;
|
|
13463
14394
|
const cwd = runner.cwd;
|
|
13464
14395
|
const signal = runner.signal;
|
|
13465
14396
|
const commands = ctx.get("commands");
|
|
13466
|
-
if (commands === void 0)
|
|
14397
|
+
if (commands === void 0) throw new Error("commands service unavailable");
|
|
14398
|
+
/**
|
|
14399
|
+
* Resolve the live agent for a session-backed command, creating the first
|
|
14400
|
+
* session lazily when the deferred start has not run yet. Throws when the
|
|
14401
|
+
* creation failed — the executor surfaces the error to the user.
|
|
14402
|
+
*/
|
|
14403
|
+
const requireAgent = async () => {
|
|
14404
|
+
await runner.ensureSession();
|
|
14405
|
+
const agent = runner.liveAgent;
|
|
14406
|
+
if (agent === void 0) throw new Error("session could not be created");
|
|
14407
|
+
return agent;
|
|
14408
|
+
};
|
|
13467
14409
|
const refreshCompletions = () => {
|
|
13468
14410
|
app.setCommandCompletions(commands.list(runner.liveAgent).map((command) => ({
|
|
13469
14411
|
name: command.name,
|
|
@@ -13478,7 +14420,8 @@ function registerTuiCommands(runner) {
|
|
|
13478
14420
|
handler: () => {
|
|
13479
14421
|
const liveAgent = runner.liveAgent;
|
|
13480
14422
|
app.stop();
|
|
13481
|
-
|
|
14423
|
+
if (liveAgent === void 0) runner.exit(0);
|
|
14424
|
+
else runner.sessions.flush(liveAgent.session).then(() => runner.exit(0));
|
|
13482
14425
|
return { kind: "success" };
|
|
13483
14426
|
}
|
|
13484
14427
|
});
|
|
@@ -13490,14 +14433,27 @@ function registerTuiCommands(runner) {
|
|
|
13490
14433
|
const tuiSettings = runner.tuiSettings;
|
|
13491
14434
|
const theme = tuiSettings?.get().theme ?? "auto";
|
|
13492
14435
|
const themeValue = theme.startsWith("custom:") ? theme.slice(7) : theme;
|
|
14436
|
+
const settings = ctx.get("settings");
|
|
14437
|
+
const permissionNames = [...ctx.get("permissionPresets")?.names ?? []];
|
|
14438
|
+
let defaultPermission;
|
|
14439
|
+
if (settings !== void 0) try {
|
|
14440
|
+
defaultPermission = settings.get(settingsNamespace("permission"))?.defaultPreset;
|
|
14441
|
+
} catch {}
|
|
13493
14442
|
app.openSettings([
|
|
13494
|
-
{
|
|
14443
|
+
...liveAgent === void 0 ? [] : [{
|
|
13495
14444
|
id: "approval",
|
|
13496
|
-
label: "Approval policy",
|
|
14445
|
+
label: "Approval policy (this session)",
|
|
13497
14446
|
description: "How tool approvals are handled in this session",
|
|
13498
14447
|
currentValue: effectiveApprovalPolicy(liveAgent.session.events) ?? "ask",
|
|
13499
14448
|
values: ["ask", "never"]
|
|
13500
|
-
},
|
|
14449
|
+
}],
|
|
14450
|
+
...permissionNames.length > 0 ? [{
|
|
14451
|
+
id: "default-permission",
|
|
14452
|
+
label: "Default permission",
|
|
14453
|
+
description: "Preset new sessions start with (persisted; Shift+Tab cycles this session)",
|
|
14454
|
+
currentValue: defaultPermission ?? permissionNames[0] ?? "",
|
|
14455
|
+
values: permissionNames
|
|
14456
|
+
}] : [],
|
|
13501
14457
|
{
|
|
13502
14458
|
id: "theme",
|
|
13503
14459
|
label: "Theme",
|
|
@@ -13543,24 +14499,26 @@ function registerTuiCommands(runner) {
|
|
|
13543
14499
|
label: color.border("─".repeat(34)),
|
|
13544
14500
|
currentValue: ""
|
|
13545
14501
|
},
|
|
13546
|
-
|
|
13547
|
-
|
|
13548
|
-
|
|
13549
|
-
|
|
13550
|
-
|
|
13551
|
-
|
|
13552
|
-
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
|
|
13556
|
-
|
|
13557
|
-
|
|
13558
|
-
|
|
13559
|
-
|
|
13560
|
-
|
|
13561
|
-
|
|
13562
|
-
|
|
13563
|
-
|
|
14502
|
+
...liveAgent === void 0 ? [] : [
|
|
14503
|
+
{
|
|
14504
|
+
id: "session",
|
|
14505
|
+
label: color.textDim("Session"),
|
|
14506
|
+
description: color.textDim(liveAgent.session.id),
|
|
14507
|
+
currentValue: color.textDim(displaySessionId(liveAgent.session.id))
|
|
14508
|
+
},
|
|
14509
|
+
{
|
|
14510
|
+
id: "model",
|
|
14511
|
+
label: color.textDim("Model"),
|
|
14512
|
+
description: color.textDim("Provider and model routing this session"),
|
|
14513
|
+
currentValue: color.textDim(`${liveAgent.options.provider}/${liveAgent.options.model}`)
|
|
14514
|
+
},
|
|
14515
|
+
{
|
|
14516
|
+
id: "preset",
|
|
14517
|
+
label: color.textDim("Agent preset"),
|
|
14518
|
+
description: color.textDim("Composition this session runs on (see /preset)"),
|
|
14519
|
+
currentValue: color.textDim(runner.currentPreset() ?? "none")
|
|
14520
|
+
}
|
|
14521
|
+
],
|
|
13564
14522
|
{
|
|
13565
14523
|
id: "cwd",
|
|
13566
14524
|
label: color.textDim("Working directory"),
|
|
@@ -13569,7 +14527,17 @@ function registerTuiCommands(runner) {
|
|
|
13569
14527
|
}
|
|
13570
14528
|
], (id, value) => {
|
|
13571
14529
|
if (id === "approval") {
|
|
13572
|
-
if (value === "ask" || value === "never")
|
|
14530
|
+
if ((value === "ask" || value === "never") && liveAgent !== void 0) {
|
|
14531
|
+
ctx.get("approval")?.setPolicy(liveAgent, value);
|
|
14532
|
+
runner.refreshStatus();
|
|
14533
|
+
}
|
|
14534
|
+
} else if (id === "default-permission") {
|
|
14535
|
+
const settings = ctx.get("settings");
|
|
14536
|
+
if (settings !== void 0 && permissionNames.includes(value)) settings.mutate(settingsNamespace("permission"), [{
|
|
14537
|
+
op: "set",
|
|
14538
|
+
path: ["defaultPreset"],
|
|
14539
|
+
value
|
|
14540
|
+
}]);
|
|
13573
14541
|
} else if (id === "theme") {
|
|
13574
14542
|
if (value === "auto" || value === "dark" || value === "light" || customThemeNames().includes(value)) {
|
|
13575
14543
|
if (value === "auto") app.autoDetectTheme();
|
|
@@ -13578,7 +14546,7 @@ function registerTuiCommands(runner) {
|
|
|
13578
14546
|
const palette = loadCustomTheme(value);
|
|
13579
14547
|
if (palette !== void 0) app.applyPalette(palette);
|
|
13580
14548
|
else {
|
|
13581
|
-
app.notify(`theme ${value} not found
|
|
14549
|
+
app.notify(`theme ${value} not found`, "error");
|
|
13582
14550
|
return;
|
|
13583
14551
|
}
|
|
13584
14552
|
}
|
|
@@ -13606,7 +14574,7 @@ function registerTuiCommands(runner) {
|
|
|
13606
14574
|
}
|
|
13607
14575
|
});
|
|
13608
14576
|
const openSessionPicker = async (invocation, header) => {
|
|
13609
|
-
const
|
|
14577
|
+
const currentId = runner.liveAgent?.session.id;
|
|
13610
14578
|
const persistence = ctx.get("sessionPersistence");
|
|
13611
14579
|
if (persistence === void 0) return {
|
|
13612
14580
|
kind: "error",
|
|
@@ -13615,17 +14583,17 @@ function registerTuiCommands(runner) {
|
|
|
13615
14583
|
const query = ctx.get("sessionQuery");
|
|
13616
14584
|
let rows;
|
|
13617
14585
|
if (query !== void 0) rows = (await query.listSessions()).map((record) => headerToPickerRow(record.header, record.live));
|
|
13618
|
-
else rows = (await persistence.list()).map((header) => headerToPickerRow(header, header.id ===
|
|
14586
|
+
else rows = (await persistence.list()).map((header) => headerToPickerRow(header, header.id === currentId));
|
|
13619
14587
|
rows.sort((a, b) => b.createdAt - a.createdAt);
|
|
13620
14588
|
if (rows.length === 0) return {
|
|
13621
14589
|
kind: "error",
|
|
13622
14590
|
text: "no persisted sessions"
|
|
13623
14591
|
};
|
|
13624
14592
|
const shown = rows.slice(0, 200);
|
|
13625
|
-
const picker = app.openPicker(shown.map((row) => sessionPickerItem(row,
|
|
13626
|
-
if (id ===
|
|
14593
|
+
const picker = app.openPicker(shown.map((row) => sessionPickerItem(row, currentId ?? "")), (id) => {
|
|
14594
|
+
if (id === currentId) return;
|
|
13627
14595
|
runner.switchSession(id).then((error) => {
|
|
13628
|
-
if (error !== void 0) app.notify(error);
|
|
14596
|
+
if (error !== void 0) app.notify(error, "error");
|
|
13629
14597
|
});
|
|
13630
14598
|
}, () => {}, {
|
|
13631
14599
|
enableSearch: true,
|
|
@@ -13641,7 +14609,7 @@ function registerTuiCommands(runner) {
|
|
|
13641
14609
|
picker.setItems(shown.map((row) => sessionPickerItem({
|
|
13642
14610
|
...row,
|
|
13643
14611
|
title: titles.get(row.id)
|
|
13644
|
-
}, liveAgent
|
|
14612
|
+
}, runner.liveAgent?.session.id ?? "")));
|
|
13645
14613
|
}).catch(() => {});
|
|
13646
14614
|
return { kind: "success" };
|
|
13647
14615
|
};
|
|
@@ -13658,22 +14626,22 @@ function registerTuiCommands(runner) {
|
|
|
13658
14626
|
handler: async (invocation) => {
|
|
13659
14627
|
const raw = invocation.rawInput.trim();
|
|
13660
14628
|
if (raw !== "") {
|
|
13661
|
-
const
|
|
14629
|
+
const currentId = runner.liveAgent?.session.id;
|
|
13662
14630
|
const persistence = ctx.get("sessionPersistence");
|
|
13663
14631
|
if (persistence !== void 0) {
|
|
13664
14632
|
const query = ctx.get("sessionQuery");
|
|
13665
14633
|
let rows;
|
|
13666
14634
|
if (query !== void 0) rows = (await query.listSessions()).map((record) => headerToPickerRow(record.header, record.live));
|
|
13667
|
-
else rows = (await persistence.list()).map((header) => headerToPickerRow(header, header.id ===
|
|
14635
|
+
else rows = (await persistence.list()).map((header) => headerToPickerRow(header, header.id === currentId));
|
|
13668
14636
|
rows.sort((a, b) => b.createdAt - a.createdAt);
|
|
13669
14637
|
const match = findSessionMatch(rows, raw);
|
|
13670
14638
|
if (match !== void 0) {
|
|
13671
|
-
if (match.id ===
|
|
14639
|
+
if (match.id === currentId) return {
|
|
13672
14640
|
kind: "error",
|
|
13673
14641
|
text: "already on this session"
|
|
13674
14642
|
};
|
|
13675
14643
|
runner.switchSession(match.id).then((error) => {
|
|
13676
|
-
if (error !== void 0) app.notify(error);
|
|
14644
|
+
if (error !== void 0) app.notify(error, "error");
|
|
13677
14645
|
});
|
|
13678
14646
|
return { kind: "success" };
|
|
13679
14647
|
}
|
|
@@ -13682,28 +14650,26 @@ function registerTuiCommands(runner) {
|
|
|
13682
14650
|
return openSessionPicker(invocation, "resume");
|
|
13683
14651
|
}
|
|
13684
14652
|
});
|
|
13685
|
-
const skillService = () => {
|
|
13686
|
-
|
|
13687
|
-
return ctx.get("agentPresets")?.serviceFor(liveAgent, "skills") ?? ctx.get("skills");
|
|
14653
|
+
const skillService = (agent) => {
|
|
14654
|
+
return ctx.get("agentPresets")?.serviceFor(agent, "skills") ?? ctx.get("skills");
|
|
13688
14655
|
};
|
|
13689
14656
|
/** The workspace the live session runs in; fallback to the TUI's cwd. */
|
|
13690
|
-
const sessionCwd = () =>
|
|
13691
|
-
const loadSkill = async (name) => {
|
|
13692
|
-
const
|
|
13693
|
-
const skills = skillService();
|
|
14657
|
+
const sessionCwd = (agent) => agent.session.header.cwd ?? cwd;
|
|
14658
|
+
const loadSkill = async (agent, name) => {
|
|
14659
|
+
const skills = skillService(agent);
|
|
13694
14660
|
if (skills === void 0) return {
|
|
13695
14661
|
kind: "error",
|
|
13696
14662
|
text: "skill service unavailable"
|
|
13697
14663
|
};
|
|
13698
14664
|
const skill = await skills.get(name, {
|
|
13699
|
-
cwd: sessionCwd(),
|
|
13700
|
-
scope:
|
|
14665
|
+
cwd: sessionCwd(agent),
|
|
14666
|
+
scope: agent
|
|
13701
14667
|
});
|
|
13702
14668
|
if (skill === void 0) return {
|
|
13703
14669
|
kind: "error",
|
|
13704
14670
|
text: "unknown skill \"" + name + "\""
|
|
13705
14671
|
};
|
|
13706
|
-
|
|
14672
|
+
agent.inject(createUserMessage({
|
|
13707
14673
|
content: [{
|
|
13708
14674
|
type: "text",
|
|
13709
14675
|
text: "Skill loaded by the user: **" + skill.name + "**\n\n" + (skill.content ?? skill.description)
|
|
@@ -13723,11 +14689,12 @@ function registerTuiCommands(runner) {
|
|
|
13723
14689
|
for (const dispose of skillDisposers.values()) dispose();
|
|
13724
14690
|
skillDisposers.clear();
|
|
13725
14691
|
const liveAgent = runner.liveAgent;
|
|
13726
|
-
|
|
14692
|
+
if (liveAgent === void 0) return 0;
|
|
14693
|
+
const skills = skillService(liveAgent);
|
|
13727
14694
|
if (skills === void 0) return 0;
|
|
13728
14695
|
const taken = new Set(commands.list(liveAgent).map((command) => command.name));
|
|
13729
14696
|
const catalog = await skills.list({
|
|
13730
|
-
cwd: sessionCwd(),
|
|
14697
|
+
cwd: sessionCwd(liveAgent),
|
|
13731
14698
|
scope: liveAgent
|
|
13732
14699
|
});
|
|
13733
14700
|
for (const skill of catalog) {
|
|
@@ -13736,7 +14703,10 @@ function registerTuiCommands(runner) {
|
|
|
13736
14703
|
const dispose = commands.register({
|
|
13737
14704
|
name: skill.name,
|
|
13738
14705
|
description: "[skill] " + skill.description,
|
|
13739
|
-
handler: async () =>
|
|
14706
|
+
handler: async () => {
|
|
14707
|
+
const agent = await requireAgent();
|
|
14708
|
+
return loadSkill(agent, skill.name);
|
|
14709
|
+
}
|
|
13740
14710
|
});
|
|
13741
14711
|
skillDisposers.set(skill.name, dispose);
|
|
13742
14712
|
} catch {}
|
|
@@ -13750,16 +14720,16 @@ function registerTuiCommands(runner) {
|
|
|
13750
14720
|
description: "Load a skill into the session context",
|
|
13751
14721
|
input: { hint: "<name>" },
|
|
13752
14722
|
handler: async (invocation) => {
|
|
13753
|
-
const liveAgent =
|
|
13754
|
-
const skills = skillService();
|
|
14723
|
+
const liveAgent = await requireAgent();
|
|
14724
|
+
const skills = skillService(liveAgent);
|
|
13755
14725
|
if (skills === void 0) return {
|
|
13756
14726
|
kind: "error",
|
|
13757
14727
|
text: "skill service unavailable"
|
|
13758
14728
|
};
|
|
13759
14729
|
const name = invocation.rawInput.trim();
|
|
13760
|
-
if (name !== "") return loadSkill(name);
|
|
14730
|
+
if (name !== "") return loadSkill(liveAgent, name);
|
|
13761
14731
|
const catalog = await skills.list({
|
|
13762
|
-
cwd: sessionCwd(),
|
|
14732
|
+
cwd: sessionCwd(liveAgent),
|
|
13763
14733
|
scope: liveAgent
|
|
13764
14734
|
});
|
|
13765
14735
|
if (catalog.length === 0) return {
|
|
@@ -13773,7 +14743,7 @@ function registerTuiCommands(runner) {
|
|
|
13773
14743
|
currentValue: "",
|
|
13774
14744
|
values: ["✓"]
|
|
13775
14745
|
})), (id) => {
|
|
13776
|
-
loadSkill(id).then((result) => {
|
|
14746
|
+
loadSkill(liveAgent, id).then((result) => {
|
|
13777
14747
|
if (result.kind === "error") app.notify(result.text);
|
|
13778
14748
|
});
|
|
13779
14749
|
}, () => {});
|
|
@@ -13800,7 +14770,7 @@ function registerTuiCommands(runner) {
|
|
|
13800
14770
|
app.setFooterPreset(doc.footer === "compact" ? "compact" : "full");
|
|
13801
14771
|
app.setFullscreen(doc.fullscreen === "on");
|
|
13802
14772
|
}
|
|
13803
|
-
app.notify(`reloaded — ${skillCount} skills \u00b7 settings reapplied
|
|
14773
|
+
app.notify(`reloaded — ${skillCount} skills \u00b7 settings reapplied`, "info");
|
|
13804
14774
|
return { kind: "success" };
|
|
13805
14775
|
}
|
|
13806
14776
|
});
|
|
@@ -13849,24 +14819,117 @@ function registerTuiCommands(runner) {
|
|
|
13849
14819
|
sessionId: SessionId(`session-${randomUUID()}`),
|
|
13850
14820
|
meta: metaOf(cwd, presetId),
|
|
13851
14821
|
agentOptions: {
|
|
13852
|
-
provider: liveAgent
|
|
13853
|
-
model: liveAgent
|
|
14822
|
+
provider: liveAgent?.options.provider ?? runner.selected.current?.provider,
|
|
14823
|
+
model: liveAgent?.options.model ?? runner.selected.current?.model
|
|
13854
14824
|
},
|
|
13855
14825
|
setup: composition.setup
|
|
13856
14826
|
});
|
|
13857
14827
|
const error = await runner.swapTo(next);
|
|
13858
|
-
if (error !== void 0) app.notify(error);
|
|
14828
|
+
if (error !== void 0) app.notify(error, "error");
|
|
13859
14829
|
return {
|
|
13860
14830
|
kind: "success",
|
|
13861
14831
|
text: "started a fresh session"
|
|
13862
14832
|
};
|
|
13863
14833
|
}
|
|
13864
14834
|
});
|
|
14835
|
+
commands.register({
|
|
14836
|
+
name: "queue",
|
|
14837
|
+
description: "Manage queued input: edit, delete, steer, or insert",
|
|
14838
|
+
handler: async () => {
|
|
14839
|
+
const liveAgent = await requireAgent();
|
|
14840
|
+
const inbox = liveAgent.inbox;
|
|
14841
|
+
const queued = [...inbox.nextTurn.map((message, index) => ({
|
|
14842
|
+
message,
|
|
14843
|
+
slot: `next ${index + 1}`,
|
|
14844
|
+
list: "next-turn"
|
|
14845
|
+
})), ...inbox.nextStep.map((message, index) => ({
|
|
14846
|
+
message,
|
|
14847
|
+
slot: `steer ${index + 1}`,
|
|
14848
|
+
list: "next-step"
|
|
14849
|
+
}))];
|
|
14850
|
+
if (queued.length === 0) return {
|
|
14851
|
+
kind: "success",
|
|
14852
|
+
text: "no queued input"
|
|
14853
|
+
};
|
|
14854
|
+
app.openSettings(queued.map(({ message, slot }) => ({
|
|
14855
|
+
id: message.id,
|
|
14856
|
+
label: `[${slot}] ${textOf(message.content).replace(/\s+/g, " ").trim().slice(0, 40)}`,
|
|
14857
|
+
description: message.id,
|
|
14858
|
+
currentValue: "",
|
|
14859
|
+
submenu: (value, done) => new SettingsList([
|
|
14860
|
+
{
|
|
14861
|
+
id: "edit",
|
|
14862
|
+
label: "Edit",
|
|
14863
|
+
description: "Rewrite this queued message",
|
|
14864
|
+
currentValue: "",
|
|
14865
|
+
values: ["✓"]
|
|
14866
|
+
},
|
|
14867
|
+
{
|
|
14868
|
+
id: "delete",
|
|
14869
|
+
label: "Delete",
|
|
14870
|
+
description: "Remove it from the queue",
|
|
14871
|
+
currentValue: "",
|
|
14872
|
+
values: ["✓"]
|
|
14873
|
+
},
|
|
14874
|
+
{
|
|
14875
|
+
id: "steer",
|
|
14876
|
+
label: "Steer",
|
|
14877
|
+
description: "Send it now: into the running turn, or start one when idle",
|
|
14878
|
+
currentValue: "",
|
|
14879
|
+
values: ["✓"]
|
|
14880
|
+
},
|
|
14881
|
+
{
|
|
14882
|
+
id: "insert",
|
|
14883
|
+
label: "Insert before",
|
|
14884
|
+
description: "Queue a new message ahead of this one",
|
|
14885
|
+
currentValue: "",
|
|
14886
|
+
values: ["✓"]
|
|
14887
|
+
}
|
|
14888
|
+
], 6, settingsListTheme, (action) => done(action), () => done(), {})
|
|
14889
|
+
})), (id, action) => {
|
|
14890
|
+
const target = queued.find((item) => item.message.id === id);
|
|
14891
|
+
if (target === void 0) return;
|
|
14892
|
+
const message = target.message;
|
|
14893
|
+
if (action === "delete") {
|
|
14894
|
+
inbox.remove(message.id);
|
|
14895
|
+
app.notify("queued message deleted", "info");
|
|
14896
|
+
} else if (action === "steer") {
|
|
14897
|
+
inbox.remove(message.id);
|
|
14898
|
+
if (liveAgent.status === "running") liveAgent.steer(message);
|
|
14899
|
+
else liveAgent.followup(message);
|
|
14900
|
+
app.notify("steering queued message", "info");
|
|
14901
|
+
} else if (action === "edit" || action === "insert") app.askQuestions([{
|
|
14902
|
+
id: "q",
|
|
14903
|
+
question: action === "edit" ? "Edit queued message:" : "New message to insert:"
|
|
14904
|
+
}]).then((answers) => {
|
|
14905
|
+
const text = answers[0]?.custom?.trim() ?? "";
|
|
14906
|
+
if (text === "") return;
|
|
14907
|
+
const next = createUserMessage({
|
|
14908
|
+
content: [{
|
|
14909
|
+
type: "text",
|
|
14910
|
+
text
|
|
14911
|
+
}],
|
|
14912
|
+
source: { kind: "user" }
|
|
14913
|
+
});
|
|
14914
|
+
if (action === "edit") {
|
|
14915
|
+
inbox.replace(message.id, next);
|
|
14916
|
+
app.notify("queued message updated", "info");
|
|
14917
|
+
} else {
|
|
14918
|
+
const position = (target.list === "next-turn" ? inbox.nextTurn : inbox.nextStep).findIndex((item) => item.id === message.id);
|
|
14919
|
+
if (position < 0) return;
|
|
14920
|
+
inbox.splice(target.list, position, 0, [next]);
|
|
14921
|
+
app.notify("message inserted before the selected one", "info");
|
|
14922
|
+
}
|
|
14923
|
+
}).catch(() => {});
|
|
14924
|
+
}, () => {});
|
|
14925
|
+
return { kind: "success" };
|
|
14926
|
+
}
|
|
14927
|
+
});
|
|
13865
14928
|
commands.register({
|
|
13866
14929
|
name: "tasks",
|
|
13867
14930
|
description: "List background jobs for this session",
|
|
13868
|
-
handler: () => {
|
|
13869
|
-
const liveAgent =
|
|
14931
|
+
handler: async () => {
|
|
14932
|
+
const liveAgent = await requireAgent();
|
|
13870
14933
|
const jobs = ctx.get("jobs");
|
|
13871
14934
|
if (jobs === void 0) return {
|
|
13872
14935
|
kind: "error",
|
|
@@ -13886,12 +14949,32 @@ function registerTuiCommands(runner) {
|
|
|
13886
14949
|
return { kind: "success" };
|
|
13887
14950
|
}
|
|
13888
14951
|
});
|
|
14952
|
+
commands.register({
|
|
14953
|
+
name: "yolo",
|
|
14954
|
+
description: "Switch to danger-full-access (alias of /permission danger-full-access)",
|
|
14955
|
+
handler: async () => {
|
|
14956
|
+
const liveAgent = await requireAgent();
|
|
14957
|
+
const commands = ctx.get("commands");
|
|
14958
|
+
if (commands === void 0) return {
|
|
14959
|
+
kind: "error",
|
|
14960
|
+
text: "commands service unavailable"
|
|
14961
|
+
};
|
|
14962
|
+
if (await commands.execute(liveAgent, "/permission danger-full-access", signal) === void 0) return {
|
|
14963
|
+
kind: "error",
|
|
14964
|
+
text: "/permission unavailable (permission presets not composed)"
|
|
14965
|
+
};
|
|
14966
|
+
return {
|
|
14967
|
+
kind: "success",
|
|
14968
|
+
text: "danger-full-access — approvals off"
|
|
14969
|
+
};
|
|
14970
|
+
}
|
|
14971
|
+
});
|
|
13889
14972
|
commands.register({
|
|
13890
14973
|
name: "preset",
|
|
13891
14974
|
description: "Show or switch the session agent preset",
|
|
13892
14975
|
input: { hint: "[status|<id>|default [<id>]]" },
|
|
13893
14976
|
handler: async (invocation) => {
|
|
13894
|
-
const liveAgent =
|
|
14977
|
+
const liveAgent = await requireAgent();
|
|
13895
14978
|
const presets = ctx.get("agentPresets");
|
|
13896
14979
|
if (presets === void 0) return {
|
|
13897
14980
|
kind: "error",
|
|
@@ -13967,7 +15050,7 @@ function registerTuiCommands(runner) {
|
|
|
13967
15050
|
name: "subagents",
|
|
13968
15051
|
description: "List child agents; view a transcript or interrupt one",
|
|
13969
15052
|
handler: async () => {
|
|
13970
|
-
const liveAgent =
|
|
15053
|
+
const liveAgent = await requireAgent();
|
|
13971
15054
|
const subagents = ctx.get("subagents");
|
|
13972
15055
|
if (subagents === void 0) return {
|
|
13973
15056
|
kind: "error",
|
|
@@ -14006,7 +15089,7 @@ function registerTuiCommands(runner) {
|
|
|
14006
15089
|
kind: "user",
|
|
14007
15090
|
parentSessionId: liveAgent.session.id
|
|
14008
15091
|
});
|
|
14009
|
-
app.notify(`interrupting ${labelOf(child)}
|
|
15092
|
+
app.notify(`interrupting ${labelOf(child)}`, "info");
|
|
14010
15093
|
}
|
|
14011
15094
|
}, () => {});
|
|
14012
15095
|
return { kind: "success" };
|
|
@@ -14017,7 +15100,7 @@ function registerTuiCommands(runner) {
|
|
|
14017
15100
|
description: "Search persisted sessions for text and switch to a hit",
|
|
14018
15101
|
input: { hint: "<query>" },
|
|
14019
15102
|
handler: async (invocation) => {
|
|
14020
|
-
const
|
|
15103
|
+
const currentId = runner.liveAgent?.session.id;
|
|
14021
15104
|
const persistence = ctx.get("sessionPersistence");
|
|
14022
15105
|
if (persistence === void 0) return {
|
|
14023
15106
|
kind: "error",
|
|
@@ -14060,9 +15143,9 @@ function registerTuiCommands(runner) {
|
|
|
14060
15143
|
label: hit.id.length > 26 ? `${hit.id.slice(0, 26)}…` : hit.id,
|
|
14061
15144
|
description: `${Math.max(0, Math.floor((now - hit.createdAt) / 6e4))}m ago · …${hit.snippet}…`
|
|
14062
15145
|
})), (id) => {
|
|
14063
|
-
if (id ===
|
|
15146
|
+
if (id === currentId) return;
|
|
14064
15147
|
runner.switchSession(id).then((error) => {
|
|
14065
|
-
if (error !== void 0) app.notify(error);
|
|
15148
|
+
if (error !== void 0) app.notify(error, "error");
|
|
14066
15149
|
});
|
|
14067
15150
|
}, () => {});
|
|
14068
15151
|
return { kind: "success" };
|
|
@@ -14072,8 +15155,8 @@ function registerTuiCommands(runner) {
|
|
|
14072
15155
|
name: "title",
|
|
14073
15156
|
description: "Set or show the session title",
|
|
14074
15157
|
input: { hint: "<title>" },
|
|
14075
|
-
handler: (invocation) => {
|
|
14076
|
-
const liveAgent =
|
|
15158
|
+
handler: async (invocation) => {
|
|
15159
|
+
const liveAgent = await requireAgent();
|
|
14077
15160
|
const titles = ctx.get("sessionTitle");
|
|
14078
15161
|
if (titles === void 0) return {
|
|
14079
15162
|
kind: "error",
|
|
@@ -14097,8 +15180,8 @@ function registerTuiCommands(runner) {
|
|
|
14097
15180
|
commands.register({
|
|
14098
15181
|
name: "copy",
|
|
14099
15182
|
description: "Copy the last assistant message (OSC 52 clipboard)",
|
|
14100
|
-
handler: () => {
|
|
14101
|
-
const last =
|
|
15183
|
+
handler: async () => {
|
|
15184
|
+
const last = (await requireAgent()).session.events.findLast((event) => event.type === "assistant/message");
|
|
14102
15185
|
if (last === void 0) return {
|
|
14103
15186
|
kind: "error",
|
|
14104
15187
|
text: "no assistant message yet"
|
|
@@ -14124,7 +15207,7 @@ function registerTuiCommands(runner) {
|
|
|
14124
15207
|
description: "Export this session log (JSONL by default, `md` for a readable transcript)",
|
|
14125
15208
|
input: { hint: "[md|<path>]" },
|
|
14126
15209
|
handler: async (invocation) => {
|
|
14127
|
-
const liveAgent =
|
|
15210
|
+
const liveAgent = await requireAgent();
|
|
14128
15211
|
const persistence = ctx.get("sessionPersistence");
|
|
14129
15212
|
if (persistence === void 0) return {
|
|
14130
15213
|
kind: "error",
|
|
@@ -14165,7 +15248,7 @@ function registerTuiCommands(runner) {
|
|
|
14165
15248
|
description: "Fork this session at the last completed turn",
|
|
14166
15249
|
handler: async () => {
|
|
14167
15250
|
const liveAgent = runner.liveAgent;
|
|
14168
|
-
const seed = forkSeed(liveAgent.session.events);
|
|
15251
|
+
const seed = liveAgent === void 0 ? void 0 : forkSeed(liveAgent.session.events);
|
|
14169
15252
|
if (seed === void 0) return {
|
|
14170
15253
|
kind: "error",
|
|
14171
15254
|
text: "no completed turn to fork from"
|
|
@@ -14187,7 +15270,7 @@ function registerTuiCommands(runner) {
|
|
|
14187
15270
|
seed
|
|
14188
15271
|
});
|
|
14189
15272
|
const error = await runner.swapTo(next);
|
|
14190
|
-
if (error !== void 0) app.notify(error);
|
|
15273
|
+
if (error !== void 0) app.notify(error, "error");
|
|
14191
15274
|
return {
|
|
14192
15275
|
kind: "success",
|
|
14193
15276
|
text: `forked as ${next.agent.session.id}`
|
|
@@ -14197,8 +15280,8 @@ function registerTuiCommands(runner) {
|
|
|
14197
15280
|
commands.register({
|
|
14198
15281
|
name: "status",
|
|
14199
15282
|
description: "Show session stats and identity",
|
|
14200
|
-
handler: () => {
|
|
14201
|
-
const liveAgent =
|
|
15283
|
+
handler: async () => {
|
|
15284
|
+
const liveAgent = await requireAgent();
|
|
14202
15285
|
const stats = computeStats(liveAgent.session.events);
|
|
14203
15286
|
let contextTokens;
|
|
14204
15287
|
const meter = ctx.get("tokenMeter");
|
|
@@ -14373,6 +15456,225 @@ function registerTuiCommands(runner) {
|
|
|
14373
15456
|
}
|
|
14374
15457
|
});
|
|
14375
15458
|
refreshCompletions();
|
|
15459
|
+
return { refreshSkills: () => registerSkillCommands() };
|
|
15460
|
+
}
|
|
15461
|
+
//#endregion
|
|
15462
|
+
//#region src/diag.ts
|
|
15463
|
+
/**
|
|
15464
|
+
* Structured process diagnostics for the TUI.
|
|
15465
|
+
*
|
|
15466
|
+
* The TUI cannot rely on `ctx.logger` for human-visible troubleshooting:
|
|
15467
|
+
* no dsh server-side package registers a logger exporter, so those lines
|
|
15468
|
+
* only fill cordis's in-memory buffer. This module owns a dedicated
|
|
15469
|
+
* diagnostics channel instead — one line per event, written to stderr
|
|
15470
|
+
* (warn/error by default, matching the legacy `[tui]` fd-2 lines) and to a
|
|
15471
|
+
* log file (info by default) under `$DSH_HOME/logs/`.
|
|
15472
|
+
*
|
|
15473
|
+
* Configuration (env):
|
|
15474
|
+
* - `DSH_PI_TUI_LOG` — log file path, or `off` to disable the file sink.
|
|
15475
|
+
* - `DSH_PI_TUI_LOG_LEVEL` — one of debug/info/warn/error (default `info`).
|
|
15476
|
+
*
|
|
15477
|
+
* Writes are best-effort and never throw; a failed file sink is disabled
|
|
15478
|
+
* silently so diagnostics can never take the TUI down.
|
|
15479
|
+
* @module @xmoon76/dsh-pi-tui/diag
|
|
15480
|
+
*/
|
|
15481
|
+
const LEVEL_RANK = {
|
|
15482
|
+
debug: 0,
|
|
15483
|
+
info: 1,
|
|
15484
|
+
warn: 2,
|
|
15485
|
+
error: 3
|
|
15486
|
+
};
|
|
15487
|
+
/** `2026-08-15T10:00:00.000+08:00` — local wall time with the zone offset. */
|
|
15488
|
+
function formatDiagTime(date) {
|
|
15489
|
+
const pad = (value, width = 2) => String(value).padStart(width, "0");
|
|
15490
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
15491
|
+
const offset = `${offsetMinutes >= 0 ? "+" : "-"}${pad(Math.floor(Math.abs(offsetMinutes) / 60))}:${pad(Math.abs(offsetMinutes) % 60)}`;
|
|
15492
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}${offset}`;
|
|
15493
|
+
}
|
|
15494
|
+
/** Render a single field value for the `k=v` line format. */
|
|
15495
|
+
function formatField(value) {
|
|
15496
|
+
if (typeof value === "string") return value;
|
|
15497
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null || value === void 0) return String(value);
|
|
15498
|
+
try {
|
|
15499
|
+
return JSON.stringify(value);
|
|
15500
|
+
} catch {
|
|
15501
|
+
return String(value);
|
|
15502
|
+
}
|
|
15503
|
+
}
|
|
15504
|
+
/** The dsh home directory (`$DSH_HOME`, defaulting to `~/.dsh`). */
|
|
15505
|
+
function dshHome(env) {
|
|
15506
|
+
const explicit = env.DSH_HOME;
|
|
15507
|
+
if (explicit !== void 0 && explicit !== "") return explicit;
|
|
15508
|
+
return join(homedir(), ".dsh");
|
|
15509
|
+
}
|
|
15510
|
+
/** Build the default diagnostics log path: `$DSH_HOME/logs/pi-tui-<pid>.log`. */
|
|
15511
|
+
function defaultDiagLogPath(env) {
|
|
15512
|
+
const configured = env.DSH_PI_TUI_LOG;
|
|
15513
|
+
if (configured === "off" || configured === "") return void 0;
|
|
15514
|
+
if (configured !== void 0) return configured;
|
|
15515
|
+
return join(dshHome(env), "logs", `pi-tui-${process.pid}.log`);
|
|
15516
|
+
}
|
|
15517
|
+
/** Resolve the effective file level from `DSH_PI_TUI_LOG_LEVEL`. */
|
|
15518
|
+
function diagLevelFromEnv(env) {
|
|
15519
|
+
const configured = env.DSH_PI_TUI_LOG_LEVEL;
|
|
15520
|
+
if (configured === "debug" || configured === "info" || configured === "warn" || configured === "error") return configured;
|
|
15521
|
+
return "info";
|
|
15522
|
+
}
|
|
15523
|
+
/** Create the diagnostics channel from the process environment. */
|
|
15524
|
+
function diagFromEnv(env) {
|
|
15525
|
+
return createDiag({
|
|
15526
|
+
fileLevel: diagLevelFromEnv(env),
|
|
15527
|
+
filePath: defaultDiagLogPath(env)
|
|
15528
|
+
});
|
|
15529
|
+
}
|
|
15530
|
+
/** Create one diagnostics channel. Writes are best-effort, never throw. */
|
|
15531
|
+
function createDiag(options = {}) {
|
|
15532
|
+
const fileLevel = options.fileLevel ?? "info";
|
|
15533
|
+
const stderrLevel = options.stderrLevel ?? "warn";
|
|
15534
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
15535
|
+
const sinks = [...options.sinks ?? []];
|
|
15536
|
+
let fileHandle;
|
|
15537
|
+
let fileDisabled = false;
|
|
15538
|
+
if (options.filePath !== void 0) try {
|
|
15539
|
+
const dir = options.filePath.slice(0, Math.max(options.filePath.lastIndexOf("/"), 0));
|
|
15540
|
+
if (dir !== "") mkdirSync(dir, { recursive: true });
|
|
15541
|
+
fileHandle = openSync(options.filePath, "a");
|
|
15542
|
+
} catch {
|
|
15543
|
+
fileDisabled = true;
|
|
15544
|
+
}
|
|
15545
|
+
const emit = (level, message, fields) => {
|
|
15546
|
+
const stamp = formatDiagTime(now());
|
|
15547
|
+
const suffix = fields === void 0 ? "" : ` ${Object.entries(fields).map(([key, value]) => `${key}=${formatField(value)}`).join(" ")}`;
|
|
15548
|
+
const line = `[tui] ${stamp} ${level.toUpperCase()} ${message}${suffix}\n`;
|
|
15549
|
+
for (const sink of sinks) try {
|
|
15550
|
+
sink.write(line);
|
|
15551
|
+
} catch {}
|
|
15552
|
+
if (stderrLevel !== "off" && LEVEL_RANK[level] >= LEVEL_RANK[stderrLevel]) try {
|
|
15553
|
+
writeSync(2, line);
|
|
15554
|
+
} catch {}
|
|
15555
|
+
if (fileHandle !== void 0 && !fileDisabled && LEVEL_RANK[level] >= LEVEL_RANK[fileLevel]) try {
|
|
15556
|
+
writeSync(fileHandle, line);
|
|
15557
|
+
} catch {
|
|
15558
|
+
fileDisabled = true;
|
|
15559
|
+
try {
|
|
15560
|
+
writeSync(2, `[tui] ${formatDiagTime(now())} WARN diag file sink disabled\n`);
|
|
15561
|
+
} catch {}
|
|
15562
|
+
}
|
|
15563
|
+
};
|
|
15564
|
+
return {
|
|
15565
|
+
debug: (message, fields) => emit("debug", message, fields),
|
|
15566
|
+
info: (message, fields) => emit("info", message, fields),
|
|
15567
|
+
warn: (message, fields) => emit("warn", message, fields),
|
|
15568
|
+
error: (message, fields) => emit("error", message, fields),
|
|
15569
|
+
dispose: () => {
|
|
15570
|
+
if (fileHandle !== void 0) {
|
|
15571
|
+
try {
|
|
15572
|
+
closeSync(fileHandle);
|
|
15573
|
+
} catch {}
|
|
15574
|
+
fileHandle = void 0;
|
|
15575
|
+
}
|
|
15576
|
+
}
|
|
15577
|
+
};
|
|
15578
|
+
}
|
|
15579
|
+
//#endregion
|
|
15580
|
+
//#region src/guard.ts
|
|
15581
|
+
/** Fresh guard state: forces one real read on the first check. */
|
|
15582
|
+
function freshGuardState() {
|
|
15583
|
+
return {
|
|
15584
|
+
revision: "",
|
|
15585
|
+
diverged: false,
|
|
15586
|
+
fileEvents: 0,
|
|
15587
|
+
error: void 0
|
|
15588
|
+
};
|
|
15589
|
+
}
|
|
15590
|
+
function statRevision(stat) {
|
|
15591
|
+
return `${stat.size}:${stat.mtimeMs}`;
|
|
15592
|
+
}
|
|
15593
|
+
/**
|
|
15594
|
+
* Check whether an external process has written the session since the last
|
|
15595
|
+
* consistent observation. Mutates `state` (revision + divergence latch).
|
|
15596
|
+
* Never throws.
|
|
15597
|
+
*/
|
|
15598
|
+
async function checkDivergence(persistence, session, stat, state) {
|
|
15599
|
+
if (persistence === void 0) return {
|
|
15600
|
+
kind: "unavailable",
|
|
15601
|
+
revision: state.revision,
|
|
15602
|
+
reason: "no-persistence"
|
|
15603
|
+
};
|
|
15604
|
+
const artifact = persistence.locate({
|
|
15605
|
+
id: session.id,
|
|
15606
|
+
cwd: session.header?.cwd
|
|
15607
|
+
});
|
|
15608
|
+
if (artifact === void 0) return {
|
|
15609
|
+
kind: "unavailable",
|
|
15610
|
+
revision: state.revision,
|
|
15611
|
+
reason: "no-artifact"
|
|
15612
|
+
};
|
|
15613
|
+
let current;
|
|
15614
|
+
try {
|
|
15615
|
+
current = stat(artifact.path);
|
|
15616
|
+
} catch (error) {
|
|
15617
|
+
if (error.code === "ENOENT") return {
|
|
15618
|
+
kind: "ok",
|
|
15619
|
+
revision: state.revision
|
|
15620
|
+
};
|
|
15621
|
+
state.diverged = true;
|
|
15622
|
+
return {
|
|
15623
|
+
kind: "unreadable",
|
|
15624
|
+
revision: state.revision,
|
|
15625
|
+
error: error instanceof Error ? error.message : String(error)
|
|
15626
|
+
};
|
|
15627
|
+
}
|
|
15628
|
+
const revision = statRevision(current);
|
|
15629
|
+
if (revision === state.revision) {
|
|
15630
|
+
if (!state.diverged) return {
|
|
15631
|
+
kind: "ok",
|
|
15632
|
+
revision
|
|
15633
|
+
};
|
|
15634
|
+
return state.error !== void 0 ? {
|
|
15635
|
+
kind: "unreadable",
|
|
15636
|
+
revision,
|
|
15637
|
+
error: state.error
|
|
15638
|
+
} : {
|
|
15639
|
+
kind: "diverged",
|
|
15640
|
+
revision,
|
|
15641
|
+
fileEvents: state.fileEvents,
|
|
15642
|
+
memoryEvents: session.events.length
|
|
15643
|
+
};
|
|
15644
|
+
}
|
|
15645
|
+
let fileEvents;
|
|
15646
|
+
try {
|
|
15647
|
+
fileEvents = (await persistence.readFrom(session.id, 0)).events.length;
|
|
15648
|
+
} catch (error) {
|
|
15649
|
+
state.revision = revision;
|
|
15650
|
+
state.diverged = true;
|
|
15651
|
+
state.fileEvents = -1;
|
|
15652
|
+
state.error = error instanceof Error ? error.message : String(error);
|
|
15653
|
+
return {
|
|
15654
|
+
kind: "unreadable",
|
|
15655
|
+
revision,
|
|
15656
|
+
error: state.error
|
|
15657
|
+
};
|
|
15658
|
+
}
|
|
15659
|
+
const memoryEvents = session.events.length;
|
|
15660
|
+
state.revision = revision;
|
|
15661
|
+
state.fileEvents = fileEvents;
|
|
15662
|
+
state.error = void 0;
|
|
15663
|
+
if (fileEvents > memoryEvents) {
|
|
15664
|
+
state.diverged = true;
|
|
15665
|
+
return {
|
|
15666
|
+
kind: "diverged",
|
|
15667
|
+
revision,
|
|
15668
|
+
fileEvents,
|
|
15669
|
+
memoryEvents
|
|
15670
|
+
};
|
|
15671
|
+
}
|
|
15672
|
+
state.diverged = false;
|
|
15673
|
+
return {
|
|
15674
|
+
kind: "ok",
|
|
15675
|
+
revision,
|
|
15676
|
+
fileEvents
|
|
15677
|
+
};
|
|
14376
15678
|
}
|
|
14377
15679
|
//#endregion
|
|
14378
15680
|
//#region src/index.ts
|
|
@@ -14402,6 +15704,27 @@ const WINDOW_TURNS = 15;
|
|
|
14402
15704
|
/** Coalesced repaint interval for streaming events, in ms. */
|
|
14403
15705
|
const REPAINT_FLUSH_MS = 50;
|
|
14404
15706
|
/**
|
|
15707
|
+
* Slash commands that need no session: before the first user message
|
|
15708
|
+
* (deferred start) they run locally without creating one. Everything else
|
|
15709
|
+
* dispatches through `commands.execute`, which creates the session lazily
|
|
15710
|
+
* (the command line IS the first user input). Commands in this set must
|
|
15711
|
+
* tolerate `liveAgent === undefined` in their handlers.
|
|
15712
|
+
*/
|
|
15713
|
+
const SESSIONLESS_COMMANDS = /* @__PURE__ */ new Set([
|
|
15714
|
+
"exit",
|
|
15715
|
+
"settings",
|
|
15716
|
+
"help",
|
|
15717
|
+
"login",
|
|
15718
|
+
"logout",
|
|
15719
|
+
"model",
|
|
15720
|
+
"reload",
|
|
15721
|
+
"sessions",
|
|
15722
|
+
"resume",
|
|
15723
|
+
"search",
|
|
15724
|
+
"new",
|
|
15725
|
+
"fork"
|
|
15726
|
+
]);
|
|
15727
|
+
/**
|
|
14405
15728
|
* The installed dsh version (e.g. `0.1.0-rc.6`), resolved from the launcher's
|
|
14406
15729
|
* real path: `process.argv[1]` is the `dsh` bin, whose realpath walks up to
|
|
14407
15730
|
* the `@deepseek-ai/dsh/package.json` that owns it. The version the welcome
|
|
@@ -14465,18 +15788,6 @@ function gitBranch(cwd) {
|
|
|
14465
15788
|
function shortCwd(cwd) {
|
|
14466
15789
|
return cwd.split("/").filter(Boolean).slice(-2).join("/") || cwd;
|
|
14467
15790
|
}
|
|
14468
|
-
/**
|
|
14469
|
-
* Write raw bytes to a file descriptor, bypassing the cordis-wrapped
|
|
14470
|
-
* `process.stderr` (whose `writeSync` is missing). Startup diagnostics go to
|
|
14471
|
-
* fd 2 so they never corrupt the TUI frame on stdout.
|
|
14472
|
-
* @param fd - the file descriptor (2 for stderr).
|
|
14473
|
-
* @param text - the bytes to write.
|
|
14474
|
-
*/
|
|
14475
|
-
function writeFd2(fd, text) {
|
|
14476
|
-
try {
|
|
14477
|
-
writeFileSync(fd, text);
|
|
14478
|
-
} catch {}
|
|
14479
|
-
}
|
|
14480
15791
|
/** Shell commands the approval dialog flags as dangerous (kimi-inspired). */
|
|
14481
15792
|
const DANGER_PATTERNS = [
|
|
14482
15793
|
/\bmkfs(\.\w+)?\b/,
|
|
@@ -14488,6 +15799,9 @@ const DANGER_PATTERNS = [
|
|
|
14488
15799
|
/>+\s*\/dev\/sd/,
|
|
14489
15800
|
/\bcurl\b[^\n|]*\|\s*(ba)?sh\b/
|
|
14490
15801
|
];
|
|
15802
|
+
/** Divergence-guard notices (user-facing, English like the rest of the TUI). */
|
|
15803
|
+
const GUARD_BLOCKED_NOTIFY = "This session may be open in another dsh process (TUI/web); send blocked. Press Enter again to force (may corrupt the session log)";
|
|
15804
|
+
const GUARD_FORCED_NOTIFY = "Forced send — the session may be written by another process; the log may be damaged";
|
|
14491
15805
|
/**
|
|
14492
15806
|
* Whether a shell command matches a destructive pattern. `rm` is treated
|
|
14493
15807
|
* specially: any spelling of recursive + force flags (`rm -rf`, `rm -r -f`,
|
|
@@ -14502,6 +15816,36 @@ function dangerCommand(command) {
|
|
|
14502
15816
|
return DANGER_PATTERNS.some((pattern) => pattern.test(command));
|
|
14503
15817
|
}
|
|
14504
15818
|
/**
|
|
15819
|
+
* The dsh profile this process was launched with (the `--profile` flag),
|
|
15820
|
+
* so the exit-time resume hint names the profile the TUI actually runs
|
|
15821
|
+
* under. Falls back to `pi-tui` when the flag is absent (the TUI bundle
|
|
15822
|
+
* cannot load without a profile, so this is defensive only).
|
|
15823
|
+
* @param argv - the process argument vector.
|
|
15824
|
+
* @param fallback - the default profile.
|
|
15825
|
+
*/
|
|
15826
|
+
function runningProfile(argv = process.argv, fallback = "pi-tui") {
|
|
15827
|
+
for (let i = argv.length - 1; i >= 0; i--) {
|
|
15828
|
+
const arg = argv[i];
|
|
15829
|
+
if (arg.startsWith("--profile=")) return arg.slice(10);
|
|
15830
|
+
if (arg === "--profile" && i + 1 < argv.length && argv[i + 1] !== void 0 && argv[i + 1] !== "") return argv[i + 1];
|
|
15831
|
+
}
|
|
15832
|
+
return fallback;
|
|
15833
|
+
}
|
|
15834
|
+
/**
|
|
15835
|
+
* The interactive-quit resume hint (pi parity): `dsh --profile <p>
|
|
15836
|
+
* --session <id>`, printed after the terminal restores so the user can
|
|
15837
|
+
* re-enter the session later. Returns undefined when there is no session
|
|
15838
|
+
* to resume (deferred start never created one).
|
|
15839
|
+
* @param profile - the running profile ({@link runningProfile}).
|
|
15840
|
+
* @param sessionId - the live session id.
|
|
15841
|
+
* @returns the resume command line, or undefined without a session.
|
|
15842
|
+
*/
|
|
15843
|
+
function resumeCommand(profile, sessionId) {
|
|
15844
|
+
const id = sessionId.trim();
|
|
15845
|
+
if (id === "") return void 0;
|
|
15846
|
+
return `dsh --profile ${profile} --session ${id}`;
|
|
15847
|
+
}
|
|
15848
|
+
/**
|
|
14505
15849
|
* The active goal badge text from the session log, or undefined. The latest
|
|
14506
15850
|
* `goal/change` wins; a clear or completed goal hides the badge.
|
|
14507
15851
|
* @param events - the session log.
|
|
@@ -14630,6 +15974,7 @@ function apply(ctx, config) {
|
|
|
14630
15974
|
if (exit === void 0) throw new Error("tui-runner: the launcher must provide ctx.appExit before the tree mounts");
|
|
14631
15975
|
const startup = ctx.get(TUI_STARTUP_SERVICE);
|
|
14632
15976
|
if (startup === void 0) return;
|
|
15977
|
+
const diag = diagFromEnv(process.env);
|
|
14633
15978
|
const sessionId = config.sessionId !== void 0 && config.sessionId !== "" ? config.sessionId : startup.sessionId;
|
|
14634
15979
|
(async () => {
|
|
14635
15980
|
await ctx.get("loader")?.await();
|
|
@@ -14649,6 +15994,14 @@ function apply(ctx, config) {
|
|
|
14649
15994
|
const compose = (presetId) => composeAgent(ctx, selected, presetId);
|
|
14650
15995
|
const withPresetMeta = (composition) => composition.agentPreset === void 0 ? {} : { agentPreset: composition.agentPreset };
|
|
14651
15996
|
const launchPreset = startup.presetId ?? (process.env.DSH_PI_TUI_PRESET?.trim() || void 0);
|
|
15997
|
+
diag.info("boot", {
|
|
15998
|
+
pid: process.pid,
|
|
15999
|
+
dsh: dshVersion() ?? "unknown",
|
|
16000
|
+
bundle: packageVersion(),
|
|
16001
|
+
cwd: process.cwd(),
|
|
16002
|
+
session: sessionId ?? "(deferred)",
|
|
16003
|
+
preset: launchPreset ?? "default"
|
|
16004
|
+
});
|
|
14652
16005
|
/** Resolve the launch composition, falling back to the default on an unknown id. */
|
|
14653
16006
|
const launchComposition = async () => {
|
|
14654
16007
|
try {
|
|
@@ -14656,6 +16009,10 @@ function apply(ctx, config) {
|
|
|
14656
16009
|
} catch (error) {
|
|
14657
16010
|
const message = error instanceof Error ? error.message : String(error);
|
|
14658
16011
|
ctx.logger.warn(`tui-runner: launch preset unavailable: ${message}`);
|
|
16012
|
+
diag.warn("preset unavailable", {
|
|
16013
|
+
preset: launchPreset ?? "default",
|
|
16014
|
+
error: message
|
|
16015
|
+
});
|
|
14659
16016
|
return {
|
|
14660
16017
|
composition: await compose(),
|
|
14661
16018
|
failure: `preset "${launchPreset}" unavailable; started with the default`
|
|
@@ -14672,14 +16029,36 @@ function apply(ctx, config) {
|
|
|
14672
16029
|
agentOptions,
|
|
14673
16030
|
setup: composition.setup
|
|
14674
16031
|
});
|
|
16032
|
+
diag.info("resume ok", {
|
|
16033
|
+
session: sessionId,
|
|
16034
|
+
seq: handle.agent.session.events.length,
|
|
16035
|
+
preset: recorded ?? "default"
|
|
16036
|
+
});
|
|
14675
16037
|
if (launchPreset !== void 0 && launchPreset !== recorded) try {
|
|
14676
|
-
if ((await recomposeBlank(ctx, handle.agent, launchPreset)).kind === "locked")
|
|
16038
|
+
if ((await recomposeBlank(ctx, handle.agent, launchPreset)).kind === "locked") {
|
|
16039
|
+
const message = `session ${sessionId} has started; its agent preset ${recorded} is fixed, ignoring --preset ${launchPreset}`;
|
|
16040
|
+
ctx.logger.warn(`tui-runner: ${message}`);
|
|
16041
|
+
diag.warn("preset ignored on resume", {
|
|
16042
|
+
session: sessionId,
|
|
16043
|
+
preset: launchPreset
|
|
16044
|
+
});
|
|
16045
|
+
}
|
|
14677
16046
|
} catch (error) {
|
|
14678
|
-
|
|
16047
|
+
const message = `--preset ${launchPreset} not applied on resume: ${error instanceof Error ? error.message : String(error)}`;
|
|
16048
|
+
ctx.logger.warn(`tui-runner: ${message}`);
|
|
16049
|
+
diag.warn("preset not applied on resume", {
|
|
16050
|
+
session: sessionId,
|
|
16051
|
+
preset: launchPreset,
|
|
16052
|
+
error: message
|
|
16053
|
+
});
|
|
14679
16054
|
}
|
|
14680
16055
|
} catch (error) {
|
|
14681
|
-
|
|
14682
|
-
|
|
16056
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16057
|
+
ctx.logger.warn(`tui-runner: resume ${sessionId} failed: ${message}`);
|
|
16058
|
+
diag.error("resume failed", {
|
|
16059
|
+
session: sessionId,
|
|
16060
|
+
error: message
|
|
16061
|
+
});
|
|
14683
16062
|
resumeFailure = `session ${sessionId} could not be resumed; started a fresh session`;
|
|
14684
16063
|
const launched = await launchComposition();
|
|
14685
16064
|
if (launched.failure !== void 0) resumeFailure = launched.failure;
|
|
@@ -14693,52 +16072,86 @@ function apply(ctx, config) {
|
|
|
14693
16072
|
setup: launched.composition.setup
|
|
14694
16073
|
});
|
|
14695
16074
|
}
|
|
14696
|
-
else {
|
|
14697
|
-
const launched = await launchComposition();
|
|
14698
|
-
if (launched.failure !== void 0) resumeFailure = launched.failure;
|
|
14699
|
-
try {
|
|
14700
|
-
handle = await agents.create({
|
|
14701
|
-
sessionId: SessionId(`session-${randomUUID()}`),
|
|
14702
|
-
meta: {
|
|
14703
|
-
cwd: process.cwd(),
|
|
14704
|
-
...withPresetMeta(launched.composition)
|
|
14705
|
-
},
|
|
14706
|
-
agentOptions,
|
|
14707
|
-
setup: launched.composition.setup
|
|
14708
|
-
});
|
|
14709
|
-
} catch (error) {
|
|
14710
|
-
ctx.logger.warn(`tui-runner: failed to start with preset "${launchPreset ?? "default"}": ${error instanceof Error ? error.message : String(error)}`);
|
|
14711
|
-
resumeFailure = `failed to start with preset "${launchPreset ?? "default"}": ${error instanceof Error ? error.message : String(error)}`;
|
|
14712
|
-
const fallback = await compose();
|
|
14713
|
-
handle = await agents.create({
|
|
14714
|
-
sessionId: SessionId(`session-${randomUUID()}`),
|
|
14715
|
-
meta: {
|
|
14716
|
-
cwd: process.cwd(),
|
|
14717
|
-
...withPresetMeta(fallback)
|
|
14718
|
-
},
|
|
14719
|
-
agentOptions,
|
|
14720
|
-
setup: fallback.setup
|
|
14721
|
-
});
|
|
14722
|
-
}
|
|
14723
|
-
}
|
|
14724
16075
|
let liveHandle = handle;
|
|
14725
|
-
let liveAgent = handle
|
|
14726
|
-
await liveAgent.whenIdle();
|
|
16076
|
+
let liveAgent = handle?.agent;
|
|
16077
|
+
if (liveAgent !== void 0) await liveAgent.whenIdle();
|
|
16078
|
+
let guardState = freshGuardState();
|
|
16079
|
+
let guardForced = false;
|
|
16080
|
+
/**
|
|
16081
|
+
* Run the divergence guard before a session-writing submission. Returns
|
|
16082
|
+
* 'ok' to proceed, 'blocked' to refuse (the caller restores the draft),
|
|
16083
|
+
* 'forced' when the user overrode a still-bad state, and 'unavailable'
|
|
16084
|
+
* when the deployment cannot guard (proceed).
|
|
16085
|
+
*/
|
|
16086
|
+
const guardSend = async () => {
|
|
16087
|
+
if (liveAgent === void 0) return "ok";
|
|
16088
|
+
const session = {
|
|
16089
|
+
id: liveAgent.session.id,
|
|
16090
|
+
header: liveAgent.session.header,
|
|
16091
|
+
events: liveAgent.session.events
|
|
16092
|
+
};
|
|
16093
|
+
const outcome = await checkDivergence(ctx.get("sessionPersistence"), session, (path) => statSync(path), guardState);
|
|
16094
|
+
switch (outcome.kind) {
|
|
16095
|
+
case "ok":
|
|
16096
|
+
guardForced = false;
|
|
16097
|
+
diag.debug("guard ok", {
|
|
16098
|
+
session: session.id,
|
|
16099
|
+
fileEvents: outcome.fileEvents ?? 0,
|
|
16100
|
+
memoryEvents: session.events.length
|
|
16101
|
+
});
|
|
16102
|
+
return "ok";
|
|
16103
|
+
case "diverged":
|
|
16104
|
+
diag.warn("guard diverged", {
|
|
16105
|
+
session: session.id,
|
|
16106
|
+
fileEvents: outcome.fileEvents,
|
|
16107
|
+
memoryEvents: outcome.memoryEvents
|
|
16108
|
+
});
|
|
16109
|
+
if (guardForced) {
|
|
16110
|
+
guardForced = false;
|
|
16111
|
+
return "forced";
|
|
16112
|
+
}
|
|
16113
|
+
return "blocked";
|
|
16114
|
+
case "unreadable":
|
|
16115
|
+
diag.error("guard unreadable", {
|
|
16116
|
+
session: session.id,
|
|
16117
|
+
error: outcome.error
|
|
16118
|
+
});
|
|
16119
|
+
if (guardForced) {
|
|
16120
|
+
guardForced = false;
|
|
16121
|
+
return "forced";
|
|
16122
|
+
}
|
|
16123
|
+
return "blocked";
|
|
16124
|
+
case "unavailable":
|
|
16125
|
+
guardForced = false;
|
|
16126
|
+
return "ok";
|
|
16127
|
+
}
|
|
16128
|
+
};
|
|
14727
16129
|
/** The preset the live agent runs on, when the deployment composes one. */
|
|
14728
|
-
const currentPreset = () =>
|
|
16130
|
+
const currentPreset = () => {
|
|
16131
|
+
if (liveAgent === void 0) return void 0;
|
|
16132
|
+
return ctx.get("agentPresets")?.composedPreset(liveAgent.ctx) ?? resolveSessionPreset(liveAgent.session);
|
|
16133
|
+
};
|
|
14729
16134
|
ctx.effect(function* () {
|
|
14730
16135
|
yield () => {
|
|
14731
16136
|
app?.stop();
|
|
16137
|
+
diag.dispose();
|
|
14732
16138
|
};
|
|
14733
16139
|
});
|
|
14734
16140
|
let folder = new TranscriptFolder();
|
|
14735
|
-
folder.apply(liveAgent.session.events);
|
|
14736
16141
|
let statsFolder = new StatsFolder();
|
|
14737
|
-
|
|
14738
|
-
|
|
16142
|
+
let goalText;
|
|
16143
|
+
if (liveAgent !== void 0) {
|
|
16144
|
+
folder.apply(liveAgent.session.events);
|
|
16145
|
+
statsFolder.apply(liveAgent.session.events);
|
|
16146
|
+
goalText = foldGoal(liveAgent.session.events);
|
|
16147
|
+
}
|
|
14739
16148
|
/** Repaint the welcome card from the live agent's current facts. Re-read
|
|
14740
16149
|
* on every call so a still-blank session's preset switch shows up. */
|
|
14741
16150
|
const updateWelcomeCard = () => {
|
|
16151
|
+
if (liveAgent === void 0) {
|
|
16152
|
+
app.setWelcomeIdle(true);
|
|
16153
|
+
return;
|
|
16154
|
+
}
|
|
14742
16155
|
app.setWelcomeCard({
|
|
14743
16156
|
cwd,
|
|
14744
16157
|
sessionId: liveAgent.session.id,
|
|
@@ -14749,28 +16162,31 @@ function apply(ctx, config) {
|
|
|
14749
16162
|
};
|
|
14750
16163
|
/** Swap the live agent to a new handle, repainting for its session. */
|
|
14751
16164
|
const swapTo = async (next) => {
|
|
16165
|
+
const from = liveAgent?.session.id;
|
|
14752
16166
|
try {
|
|
14753
|
-
|
|
14754
|
-
|
|
16167
|
+
if (liveAgent !== void 0) {
|
|
16168
|
+
await sessions.flush(liveAgent.session);
|
|
16169
|
+
await liveHandle?.dispose();
|
|
16170
|
+
}
|
|
14755
16171
|
liveHandle = next;
|
|
14756
16172
|
liveAgent = next.agent;
|
|
14757
16173
|
await liveAgent.whenIdle();
|
|
14758
16174
|
} catch (error) {
|
|
14759
|
-
|
|
14760
|
-
|
|
16175
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16176
|
+
diag.error("swap failed", {
|
|
16177
|
+
from,
|
|
16178
|
+
error: message
|
|
16179
|
+
});
|
|
16180
|
+
return `swap failed: ${message}`;
|
|
14761
16181
|
}
|
|
14762
|
-
|
|
14763
|
-
|
|
14764
|
-
|
|
14765
|
-
|
|
14766
|
-
|
|
14767
|
-
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
repaint(app, folder);
|
|
14771
|
-
refreshStatus();
|
|
14772
|
-
setTerminalTitle(`dsh-pi-tui · ${shortCwd(cwd)} · ${liveAgent.session.id}`);
|
|
14773
|
-
updateWelcomeCard();
|
|
16182
|
+
guardState = freshGuardState();
|
|
16183
|
+
guardForced = false;
|
|
16184
|
+
await initLiveSession(next.agent);
|
|
16185
|
+
diag.info("switch ok", {
|
|
16186
|
+
from: from ?? "(none)",
|
|
16187
|
+
to: next.agent.session.id,
|
|
16188
|
+
seq: next.agent.session.events.length
|
|
16189
|
+
});
|
|
14774
16190
|
};
|
|
14775
16191
|
/** Hand the TUI over to another persisted session. Never throws: every
|
|
14776
16192
|
* failure (unknown session, broken log, preset mount) returns an error
|
|
@@ -14781,31 +16197,38 @@ function apply(ctx, config) {
|
|
|
14781
16197
|
const next = await agents.resume({
|
|
14782
16198
|
resumeSessionId: SessionId(sessionId),
|
|
14783
16199
|
agentOptions: {
|
|
14784
|
-
provider: liveAgent
|
|
14785
|
-
model: liveAgent
|
|
16200
|
+
provider: liveAgent?.options.provider ?? selection.provider,
|
|
16201
|
+
model: liveAgent?.options.model ?? selection.model
|
|
14786
16202
|
},
|
|
14787
16203
|
setup: composition.setup
|
|
14788
16204
|
});
|
|
14789
16205
|
return swapTo(next);
|
|
14790
16206
|
} catch (error) {
|
|
14791
|
-
|
|
14792
|
-
|
|
16207
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16208
|
+
ctx.logger.warn(`tui-runner: switch to ${sessionId} failed: ${message}`);
|
|
16209
|
+
diag.error("switch failed", {
|
|
16210
|
+
session: sessionId,
|
|
16211
|
+
error: message
|
|
16212
|
+
});
|
|
16213
|
+
return `switch failed: ${message}`;
|
|
14793
16214
|
}
|
|
14794
16215
|
};
|
|
14795
16216
|
const cwd = process.cwd();
|
|
14796
16217
|
/** The footer model label: the live selection (with effort) when one exists. */
|
|
14797
16218
|
const modelLabel = () => {
|
|
14798
16219
|
const selection = selected.current;
|
|
14799
|
-
if (selection
|
|
14800
|
-
|
|
16220
|
+
if (selection !== void 0) return selection.reasoningEffort === void 0 ? `${selection.provider}/${selection.model}` : `${selection.provider}/${selection.model} @${selection.reasoningEffort}`;
|
|
16221
|
+
if (liveAgent === void 0) return "no model";
|
|
16222
|
+
return `${liveAgent.options.provider}/${liveAgent.options.model}`;
|
|
14801
16223
|
};
|
|
14802
16224
|
const refreshStatus = () => {
|
|
14803
16225
|
const stats = statsFolder.snapshot();
|
|
14804
16226
|
let contextTokens;
|
|
14805
16227
|
const meter = ctx.get("tokenMeter");
|
|
14806
|
-
if (meter !== void 0) try {
|
|
16228
|
+
if (meter !== void 0 && liveAgent !== void 0) try {
|
|
14807
16229
|
contextTokens = meter.measure(liveAgent.session).totalTokens;
|
|
14808
16230
|
} catch {}
|
|
16231
|
+
const permission = ctx.get("permissionPresets");
|
|
14809
16232
|
app.setStatus({
|
|
14810
16233
|
model: modelLabel(),
|
|
14811
16234
|
cwd: shortCwd(cwd),
|
|
@@ -14814,6 +16237,7 @@ function apply(ctx, config) {
|
|
|
14814
16237
|
turns: stats.turns,
|
|
14815
16238
|
steps: stats.steps,
|
|
14816
16239
|
statsLine: formatStats(stats),
|
|
16240
|
+
...permission === void 0 || liveAgent === void 0 ? {} : { permission: permission.current(liveAgent.session.events) },
|
|
14817
16241
|
...contextTokens !== void 0 ? {
|
|
14818
16242
|
contextTokens,
|
|
14819
16243
|
contextWindow: stats.contextWindow
|
|
@@ -14822,7 +16246,10 @@ function apply(ctx, config) {
|
|
|
14822
16246
|
};
|
|
14823
16247
|
let app;
|
|
14824
16248
|
const tools = ctx.get("tools");
|
|
14825
|
-
const present = toolPresenterFrom((name) =>
|
|
16249
|
+
const present = toolPresenterFrom((name) => {
|
|
16250
|
+
if (liveAgent === void 0) return void 0;
|
|
16251
|
+
return tools?.get(name, liveAgent);
|
|
16252
|
+
});
|
|
14826
16253
|
const signal = new AbortController().signal;
|
|
14827
16254
|
let localShellController;
|
|
14828
16255
|
/** Run a `!` command locally; the output renders as a local card. */
|
|
@@ -14831,6 +16258,7 @@ function apply(ctx, config) {
|
|
|
14831
16258
|
const command = text.replace(/^!+/, "").trim();
|
|
14832
16259
|
if (command === "") return;
|
|
14833
16260
|
if (includeInContext) {
|
|
16261
|
+
if (liveAgent === void 0) return;
|
|
14834
16262
|
liveAgent.followup(createUserMessage({
|
|
14835
16263
|
content: [{
|
|
14836
16264
|
type: "text",
|
|
@@ -14966,37 +16394,45 @@ function apply(ctx, config) {
|
|
|
14966
16394
|
folder: childFolder
|
|
14967
16395
|
};
|
|
14968
16396
|
repaint(app, childFolder);
|
|
14969
|
-
app.notify(`viewing subagent ${label ?? childId} — Esc returns
|
|
16397
|
+
app.notify(`viewing subagent ${label ?? childId} — Esc returns`, "info");
|
|
14970
16398
|
};
|
|
14971
16399
|
/** Leave the subagent viewer (single Esc). Returns whether it exited. */
|
|
14972
16400
|
const exitView = () => {
|
|
14973
16401
|
if (viewing === void 0) return false;
|
|
14974
16402
|
viewing = void 0;
|
|
14975
16403
|
app.clearLocalMessages();
|
|
16404
|
+
app.clearNotify();
|
|
14976
16405
|
repaint(app, folder);
|
|
14977
16406
|
refreshStatus();
|
|
14978
16407
|
return true;
|
|
14979
16408
|
};
|
|
14980
|
-
|
|
14981
|
-
|
|
14982
|
-
|
|
14983
|
-
|
|
14984
|
-
|
|
14985
|
-
|
|
14986
|
-
|
|
14987
|
-
|
|
14988
|
-
|
|
14989
|
-
|
|
14990
|
-
|
|
14991
|
-
|
|
16409
|
+
/** Error sink for a failed session creation: restore the draft and
|
|
16410
|
+
* surface the reason instead of silently dropping the submission. */
|
|
16411
|
+
const failSubmission = (draft) => (error) => {
|
|
16412
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16413
|
+
ctx.logger.error(`tui-runner: session creation failed: ${message}`);
|
|
16414
|
+
diag.error("session creation failed", { error: message });
|
|
16415
|
+
app.setEditorText(draft);
|
|
16416
|
+
app.notify(`could not start a session: ${message}`, "error");
|
|
16417
|
+
};
|
|
16418
|
+
/** The session-backed dispatch: create the session lazily (the first
|
|
16419
|
+
* user input is the deferred trigger), guard against cross-process
|
|
16420
|
+
* divergence, then execute a registered slash command or follow up. */
|
|
16421
|
+
const dispatchViaSession = (text) => {
|
|
16422
|
+
ensureSession().then(async () => {
|
|
16423
|
+
const verdict = await guardSend();
|
|
16424
|
+
if (verdict === "blocked") {
|
|
16425
|
+
app.setEditorText(text);
|
|
16426
|
+
app.notify(GUARD_BLOCKED_NOTIFY, "error");
|
|
14992
16427
|
return;
|
|
14993
16428
|
}
|
|
16429
|
+
if (verdict === "forced") app.notify(GUARD_FORCED_NOTIFY, "error");
|
|
14994
16430
|
const commands = ctx.get("commands");
|
|
14995
16431
|
if (commands !== void 0) {
|
|
14996
16432
|
const parsed = parseCommand(text);
|
|
14997
|
-
const toggled = parsed?.name === "plan" && parsed.rawInput.trim() === "" && foldPlanMode(liveAgent
|
|
16433
|
+
const toggled = parsed?.name === "plan" && parsed.rawInput.trim() === "" && foldPlanMode(liveAgent?.session.events ?? []) ? "/plan off" : text;
|
|
14998
16434
|
commands.execute(liveAgent, toggled, signal).then((execution) => {
|
|
14999
|
-
if (execution === void 0) liveAgent.followup(createUserMessage({
|
|
16435
|
+
if (execution === void 0 && liveAgent !== void 0) liveAgent.followup(createUserMessage({
|
|
15000
16436
|
content: [{
|
|
15001
16437
|
type: "text",
|
|
15002
16438
|
text
|
|
@@ -15004,40 +16440,142 @@ function apply(ctx, config) {
|
|
|
15004
16440
|
source: { kind: "user" }
|
|
15005
16441
|
}));
|
|
15006
16442
|
}).catch((error) => {
|
|
15007
|
-
|
|
15008
|
-
|
|
16443
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16444
|
+
ctx.logger.error(`tui-runner: command execution failed: ${message}`);
|
|
16445
|
+
diag.error("command execution failed", { error: message });
|
|
16446
|
+
app.notify(message, "error");
|
|
15009
16447
|
});
|
|
15010
16448
|
return;
|
|
15011
16449
|
}
|
|
15012
|
-
liveAgent
|
|
16450
|
+
liveAgent?.followup(createUserMessage({
|
|
15013
16451
|
content: [{
|
|
15014
16452
|
type: "text",
|
|
15015
16453
|
text
|
|
15016
16454
|
}],
|
|
15017
16455
|
source: { kind: "user" }
|
|
15018
16456
|
}));
|
|
16457
|
+
}).catch(failSubmission(text));
|
|
16458
|
+
};
|
|
16459
|
+
/**
|
|
16460
|
+
* Run a sessionless slash command locally — no session, no log, no
|
|
16461
|
+
* persistence. The handler comes from the commands service's global
|
|
16462
|
+
* layer (in-process lookup with no agent is safe: it reads the global
|
|
16463
|
+
* layer only). A sessionless command that failed to register falls back
|
|
16464
|
+
* to the session dispatch, which reports unknown commands as messages.
|
|
16465
|
+
*/
|
|
16466
|
+
const runLocalCommand = (parsed, text) => {
|
|
16467
|
+
const commands = ctx.get("commands");
|
|
16468
|
+
const definition = commands?.find(void 0, parsed.name);
|
|
16469
|
+
if (commands === void 0 || definition === void 0) {
|
|
16470
|
+
dispatchViaSession(text);
|
|
16471
|
+
return;
|
|
16472
|
+
}
|
|
16473
|
+
const invocation = {
|
|
16474
|
+
commandId: `cmd-local-${randomUUID()}`,
|
|
16475
|
+
agent: void 0,
|
|
16476
|
+
rawInput: parsed.rawInput,
|
|
16477
|
+
signal
|
|
16478
|
+
};
|
|
16479
|
+
Promise.resolve(definition.handler(invocation)).then((result) => {
|
|
16480
|
+
if (result !== void 0 && result.kind === "error") app.notify(result.text);
|
|
16481
|
+
}).catch((error) => {
|
|
16482
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16483
|
+
ctx.logger.error(`tui-runner: local command failed: ${message}`);
|
|
16484
|
+
diag.error("local command failed", { error: message });
|
|
16485
|
+
app.notify(message, "error");
|
|
16486
|
+
});
|
|
16487
|
+
};
|
|
16488
|
+
app = startProcessTui({
|
|
16489
|
+
onSubmit: (text) => {
|
|
16490
|
+
const history = app.getInputHistory();
|
|
16491
|
+
if (history.length > 0) tuiSettings?.replace({
|
|
16492
|
+
...tuiSettings.get(),
|
|
16493
|
+
history: {
|
|
16494
|
+
...tuiSettings.get().history,
|
|
16495
|
+
[cwd]: history
|
|
16496
|
+
}
|
|
16497
|
+
});
|
|
16498
|
+
if (text.startsWith("!")) {
|
|
16499
|
+
if (text.startsWith("!!")) ensureSession().then(() => runLocalShell(text)).catch(failSubmission(text));
|
|
16500
|
+
else runLocalShell(text);
|
|
16501
|
+
return;
|
|
16502
|
+
}
|
|
16503
|
+
const parsed = parseCommand(text);
|
|
16504
|
+
if (parsed !== void 0 && liveAgent === void 0 && SESSIONLESS_COMMANDS.has(parsed.name)) {
|
|
16505
|
+
runLocalCommand(parsed, text);
|
|
16506
|
+
return;
|
|
16507
|
+
}
|
|
16508
|
+
dispatchViaSession(text);
|
|
15019
16509
|
},
|
|
15020
16510
|
onExit: () => {
|
|
15021
16511
|
(async () => {
|
|
15022
|
-
|
|
16512
|
+
if (liveAgent !== void 0) {
|
|
16513
|
+
const started = Date.now();
|
|
16514
|
+
try {
|
|
16515
|
+
await sessions.flush(liveAgent.session);
|
|
16516
|
+
diag.info("flush ok", {
|
|
16517
|
+
session: liveAgent.session.id,
|
|
16518
|
+
events: liveAgent.session.events.length,
|
|
16519
|
+
tookMs: Date.now() - started
|
|
16520
|
+
});
|
|
16521
|
+
} catch (error) {
|
|
16522
|
+
diag.error("flush failed", {
|
|
16523
|
+
session: liveAgent.session.id,
|
|
16524
|
+
error: error instanceof Error ? error.message : String(error)
|
|
16525
|
+
});
|
|
16526
|
+
}
|
|
16527
|
+
}
|
|
16528
|
+
diag.info("exit", { code: 0 });
|
|
16529
|
+
diag.dispose();
|
|
15023
16530
|
app.stop();
|
|
16531
|
+
const resume = resumeCommand(runningProfile(), liveAgent?.session.id ?? "");
|
|
16532
|
+
if (resume !== void 0) process.stdout.write(`\n${color.textDim("To resume this session:")} ${resume}\n`);
|
|
15024
16533
|
exit(0);
|
|
15025
16534
|
})();
|
|
15026
16535
|
},
|
|
15027
16536
|
onCancel: () => {
|
|
15028
16537
|
localShellController?.abort();
|
|
15029
|
-
liveAgent
|
|
16538
|
+
liveAgent?.cancel({ kind: "user" });
|
|
15030
16539
|
},
|
|
15031
16540
|
onSteer: (text) => {
|
|
15032
|
-
|
|
15033
|
-
|
|
15034
|
-
|
|
15035
|
-
|
|
15036
|
-
|
|
15037
|
-
|
|
15038
|
-
|
|
15039
|
-
|
|
15040
|
-
|
|
16541
|
+
if (liveAgent !== void 0) {
|
|
16542
|
+
if ([...liveAgent.inbox.nextTurn, ...liveAgent.inbox.nextStep].length === 0 && text.trim() === "") return;
|
|
16543
|
+
} else if (text.trim() === "") return;
|
|
16544
|
+
ensureSession().then(async () => {
|
|
16545
|
+
if (liveAgent === void 0) return;
|
|
16546
|
+
const verdict = await guardSend();
|
|
16547
|
+
if (verdict === "blocked") {
|
|
16548
|
+
app.setEditorText(text);
|
|
16549
|
+
app.notify(GUARD_BLOCKED_NOTIFY, "error");
|
|
16550
|
+
return;
|
|
16551
|
+
}
|
|
16552
|
+
const forced = verdict === "forced";
|
|
16553
|
+
if (forced) app.notify(GUARD_FORCED_NOTIFY, "error");
|
|
16554
|
+
const queued = [...liveAgent.inbox.nextTurn, ...liveAgent.inbox.nextStep];
|
|
16555
|
+
if (queued.length > 0) {
|
|
16556
|
+
const draft = text.trim();
|
|
16557
|
+
const messages = [...queued, ...draft === "" ? [] : [createUserMessage({
|
|
16558
|
+
content: [{
|
|
16559
|
+
type: "text",
|
|
16560
|
+
text: draft
|
|
16561
|
+
}],
|
|
16562
|
+
source: { kind: "user" }
|
|
16563
|
+
})]];
|
|
16564
|
+
liveAgent.inbox.clear();
|
|
16565
|
+
for (const message of messages) liveAgent.steer(message);
|
|
16566
|
+
if (!forced) app.notify(messages.length === 1 ? "steering 1 message" : `steering ${messages.length} messages`, "info");
|
|
16567
|
+
} else {
|
|
16568
|
+
const message = createUserMessage({
|
|
16569
|
+
content: [{
|
|
16570
|
+
type: "text",
|
|
16571
|
+
text
|
|
16572
|
+
}],
|
|
16573
|
+
source: { kind: "user" }
|
|
16574
|
+
});
|
|
16575
|
+
if (liveAgent.status === "running") liveAgent.steer(message);
|
|
16576
|
+
else liveAgent.followup(message);
|
|
16577
|
+
}
|
|
16578
|
+
}).catch(failSubmission(text));
|
|
15041
16579
|
},
|
|
15042
16580
|
openExternalEditor: async (draft) => {
|
|
15043
16581
|
const editor = process.env.VISUAL ?? process.env.EDITOR ?? "vi";
|
|
@@ -15084,15 +16622,34 @@ function apply(ctx, config) {
|
|
|
15084
16622
|
searchCurrent = -1;
|
|
15085
16623
|
repaint(app, activeFolder());
|
|
15086
16624
|
},
|
|
15087
|
-
onSingleEscape: () => exitView()
|
|
16625
|
+
onSingleEscape: () => exitView(),
|
|
16626
|
+
onCyclePermission: () => {
|
|
16627
|
+
if (liveAgent === void 0) return;
|
|
16628
|
+
const permission = ctx.get("permissionPresets");
|
|
16629
|
+
if (permission === void 0) return;
|
|
16630
|
+
const names = permission.names;
|
|
16631
|
+
if (names.length === 0) return;
|
|
16632
|
+
const current = permission.current(liveAgent.session.events);
|
|
16633
|
+
const next = names[(names.indexOf(current) + 1) % names.length] ?? names[0];
|
|
16634
|
+
if (next === void 0 || next === current) return;
|
|
16635
|
+
permission.set(liveAgent.session, next);
|
|
16636
|
+
app.notify(next === "danger-full-access" ? `⚠ ${next} — no approvals` : `permission: ${next}`, next === "danger-full-access" ? "error" : "info");
|
|
16637
|
+
refreshStatus();
|
|
16638
|
+
},
|
|
16639
|
+
onDequeue: () => {
|
|
16640
|
+
if (liveAgent === void 0) return;
|
|
16641
|
+
const queued = [...liveAgent.inbox.nextTurn, ...liveAgent.inbox.nextStep];
|
|
16642
|
+
if (queued.length === 0) return;
|
|
16643
|
+
liveAgent.inbox.clear();
|
|
16644
|
+
const queuedText = queued.map((message) => textOf(message.content)).join("\n\n");
|
|
16645
|
+
const current = app.getDraft();
|
|
16646
|
+
app.setDraft([queuedText, current].filter((part) => part.trim() !== "").join("\n\n"));
|
|
16647
|
+
refreshQueue();
|
|
16648
|
+
}
|
|
15088
16649
|
}, {
|
|
15089
16650
|
present,
|
|
15090
16651
|
workspaceRoot: cwd
|
|
15091
16652
|
});
|
|
15092
|
-
paintNow();
|
|
15093
|
-
setTerminalTitle(`dsh-pi-tui · ${shortCwd(cwd)} · ${liveAgent.session.id}`);
|
|
15094
|
-
updateWelcomeCard();
|
|
15095
|
-
if (resumeFailure !== void 0) app.notify(resumeFailure);
|
|
15096
16653
|
const tuiSettings = ctx.get("settings")?.register(settingsNamespace("dsh-pi-tui"), z.object({
|
|
15097
16654
|
theme: z.string(),
|
|
15098
16655
|
footer: z.string(),
|
|
@@ -15119,12 +16676,125 @@ function apply(ctx, config) {
|
|
|
15119
16676
|
if (tuiSettings?.get().fullscreen === "on") app.setFullscreen(true);
|
|
15120
16677
|
const storedHistory = tuiSettings?.get().history[cwd];
|
|
15121
16678
|
if (storedHistory !== void 0 && storedHistory.length > 0) app.seedInputHistory(storedHistory);
|
|
15122
|
-
|
|
16679
|
+
refreshStatus();
|
|
16680
|
+
const jobs = ctx.get("jobs");
|
|
16681
|
+
if (jobs !== void 0) {
|
|
16682
|
+
const refreshTasks = () => {
|
|
16683
|
+
let tasks = [];
|
|
16684
|
+
try {
|
|
16685
|
+
tasks = jobs.list(liveAgent).filter((job) => job.status === "running" || job.status === "stopping").map((job) => ({
|
|
16686
|
+
label: job.label,
|
|
16687
|
+
status: job.status
|
|
16688
|
+
}));
|
|
16689
|
+
} catch {}
|
|
16690
|
+
app.setTasks(tasks);
|
|
16691
|
+
};
|
|
16692
|
+
jobs.onJobsChanged(() => refreshTasks());
|
|
16693
|
+
refreshTasks();
|
|
16694
|
+
}
|
|
16695
|
+
const refreshQueue = () => {
|
|
16696
|
+
if (liveAgent === void 0) {
|
|
16697
|
+
app.setQueueItems([]);
|
|
16698
|
+
return;
|
|
16699
|
+
}
|
|
16700
|
+
const queue = [...liveAgent.inbox.nextTurn.map((message) => ({
|
|
16701
|
+
id: message.id,
|
|
16702
|
+
text: textOf(message.content),
|
|
16703
|
+
mode: "followup"
|
|
16704
|
+
})), ...liveAgent.inbox.nextStep.map((message) => ({
|
|
16705
|
+
id: message.id,
|
|
16706
|
+
text: textOf(message.content),
|
|
16707
|
+
mode: "steer"
|
|
16708
|
+
}))];
|
|
16709
|
+
app.setQueueItems(queue);
|
|
16710
|
+
};
|
|
16711
|
+
refreshQueue();
|
|
16712
|
+
/** Rebuild every live-session surface after resume, create, or swap. */
|
|
16713
|
+
const initLiveSession = async (agent) => {
|
|
16714
|
+
folder = new TranscriptFolder();
|
|
16715
|
+
folder.apply(agent.session.events);
|
|
16716
|
+
statsFolder = new StatsFolder();
|
|
16717
|
+
statsFolder.apply(agent.session.events);
|
|
16718
|
+
goalText = foldGoal(agent.session.events);
|
|
16719
|
+
app.setWorking(workingFromLog(agent.session.events));
|
|
16720
|
+
app.setSessionTitle(foldSessionTitle(agent.session.events)?.title);
|
|
16721
|
+
app.clearLocalMessages();
|
|
16722
|
+
app.clearNotify();
|
|
16723
|
+
repaint(app, folder);
|
|
16724
|
+
refreshStatus();
|
|
16725
|
+
refreshQueue();
|
|
16726
|
+
setTerminalTitle(`dsh-pi-tui · ${shortCwd(cwd)} · ${agent.session.id}`);
|
|
16727
|
+
updateWelcomeCard();
|
|
16728
|
+
registerCommands();
|
|
16729
|
+
refreshSkillCommands();
|
|
16730
|
+
};
|
|
16731
|
+
/**
|
|
16732
|
+
* Create the first session lazily — the FIRST user message triggers it
|
|
16733
|
+
* (deferred session creation). Opening the TUI with no --session carries
|
|
16734
|
+
* zero session side-effects: no agent, no log, no persistence.
|
|
16735
|
+
*/
|
|
16736
|
+
let creating;
|
|
16737
|
+
const ensureSession = async () => {
|
|
16738
|
+
if (liveAgent !== void 0) return;
|
|
16739
|
+
if (creating !== void 0) return creating;
|
|
16740
|
+
creating = (async () => {
|
|
16741
|
+
const launched = await launchComposition();
|
|
16742
|
+
if (launched.failure !== void 0) resumeFailure = launched.failure;
|
|
16743
|
+
try {
|
|
16744
|
+
const created = await agents.create({
|
|
16745
|
+
sessionId: SessionId(`session-${randomUUID()}`),
|
|
16746
|
+
meta: {
|
|
16747
|
+
cwd: process.cwd(),
|
|
16748
|
+
...withPresetMeta(launched.composition)
|
|
16749
|
+
},
|
|
16750
|
+
agentOptions,
|
|
16751
|
+
setup: launched.composition.setup
|
|
16752
|
+
});
|
|
16753
|
+
liveHandle = created;
|
|
16754
|
+
liveAgent = created.agent;
|
|
16755
|
+
await liveAgent.whenIdle();
|
|
16756
|
+
await initLiveSession(liveAgent);
|
|
16757
|
+
} catch (error) {
|
|
16758
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16759
|
+
ctx.logger.warn(`tui-runner: failed to start with preset "${launchPreset ?? "default"}": ${message}`);
|
|
16760
|
+
diag.warn("preset mount failed", {
|
|
16761
|
+
preset: launchPreset ?? "default",
|
|
16762
|
+
error: message
|
|
16763
|
+
});
|
|
16764
|
+
resumeFailure = `failed to start with preset "${launchPreset ?? "default"}": ${message}`;
|
|
16765
|
+
const fallback = await compose();
|
|
16766
|
+
const created = await agents.create({
|
|
16767
|
+
sessionId: SessionId(`session-${randomUUID()}`),
|
|
16768
|
+
meta: {
|
|
16769
|
+
cwd: process.cwd(),
|
|
16770
|
+
...withPresetMeta(fallback)
|
|
16771
|
+
},
|
|
16772
|
+
agentOptions,
|
|
16773
|
+
setup: fallback.setup
|
|
16774
|
+
});
|
|
16775
|
+
liveHandle = created;
|
|
16776
|
+
liveAgent = created.agent;
|
|
16777
|
+
await liveAgent.whenIdle();
|
|
16778
|
+
await initLiveSession(liveAgent);
|
|
16779
|
+
}
|
|
16780
|
+
if (resumeFailure !== void 0) {
|
|
16781
|
+
app.notify(resumeFailure, "error");
|
|
16782
|
+
resumeFailure = void 0;
|
|
16783
|
+
}
|
|
16784
|
+
})().finally(() => {
|
|
16785
|
+
creating = void 0;
|
|
16786
|
+
});
|
|
16787
|
+
return creating;
|
|
16788
|
+
};
|
|
16789
|
+
let commandsRegistered = false;
|
|
16790
|
+
let refreshSkills;
|
|
16791
|
+
const runner = {
|
|
15123
16792
|
ctx,
|
|
15124
16793
|
app,
|
|
15125
16794
|
get liveAgent() {
|
|
15126
16795
|
return liveAgent;
|
|
15127
16796
|
},
|
|
16797
|
+
ensureSession,
|
|
15128
16798
|
get selected() {
|
|
15129
16799
|
return selected;
|
|
15130
16800
|
},
|
|
@@ -15144,9 +16814,43 @@ function apply(ctx, config) {
|
|
|
15144
16814
|
updateWelcomeCard,
|
|
15145
16815
|
enterView,
|
|
15146
16816
|
exit
|
|
15147
|
-
}
|
|
15148
|
-
|
|
16817
|
+
};
|
|
16818
|
+
const registerCommands = () => {
|
|
16819
|
+
if (commandsRegistered) return;
|
|
16820
|
+
if (ctx.get("commands") === void 0) return;
|
|
16821
|
+
commandsRegistered = true;
|
|
16822
|
+
try {
|
|
16823
|
+
refreshSkills = registerTuiCommands(runner).refreshSkills;
|
|
16824
|
+
} catch (error) {
|
|
16825
|
+
commandsRegistered = false;
|
|
16826
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16827
|
+
ctx.logger.error(`tui-runner: command registration failed: ${message}`);
|
|
16828
|
+
diag.error("command registration failed", { error: message });
|
|
16829
|
+
app.notify(`command registration failed: ${message}`, "error");
|
|
16830
|
+
}
|
|
16831
|
+
};
|
|
16832
|
+
/** Rebuild the agent-scoped per-skill commands after a session exists. */
|
|
16833
|
+
const refreshSkillCommands = () => {
|
|
16834
|
+
if (refreshSkills === void 0) return;
|
|
16835
|
+
refreshSkills().catch((error) => {
|
|
16836
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16837
|
+
ctx.logger.warn(`tui-runner: skill command refresh failed: ${message}`);
|
|
16838
|
+
diag.warn("skill command refresh failed", { error: message });
|
|
16839
|
+
});
|
|
16840
|
+
};
|
|
16841
|
+
if (liveAgent !== void 0) await initLiveSession(liveAgent);
|
|
16842
|
+
else {
|
|
16843
|
+
app.setWelcomeIdle(true);
|
|
16844
|
+
refreshStatus();
|
|
16845
|
+
setTerminalTitle(`dsh-pi-tui · ${shortCwd(cwd)}`);
|
|
16846
|
+
}
|
|
16847
|
+
registerCommands();
|
|
16848
|
+
if (resumeFailure !== void 0) {
|
|
16849
|
+
app.notify(resumeFailure, "error");
|
|
16850
|
+
resumeFailure = void 0;
|
|
16851
|
+
}
|
|
15149
16852
|
ctx.on("session/event", (session, event) => {
|
|
16853
|
+
if (liveAgent === void 0) return;
|
|
15150
16854
|
if (viewing !== void 0) {
|
|
15151
16855
|
if (session.id !== viewing.id) return;
|
|
15152
16856
|
viewing.folder.apply([event]);
|
|
@@ -15160,10 +16864,13 @@ function apply(ctx, config) {
|
|
|
15160
16864
|
folder.apply([event]);
|
|
15161
16865
|
statsFolder.apply([event]);
|
|
15162
16866
|
if (event.type === "goal/change") goalText = foldGoal([event]);
|
|
15163
|
-
|
|
16867
|
+
const isKnob = event.type === "permission/preset" || event.type === "approval/policy" || event.type === "sandbox/mode";
|
|
16868
|
+
if (!isKnob) schedulePaint();
|
|
15164
16869
|
if (event.type === "todo/write") app.setTodoSummary(event.data.todos);
|
|
15165
16870
|
if (event.type === "plan/mode") app.setPlanMode(event.data.active);
|
|
15166
16871
|
if (event.type === "session/title") app.setSessionTitle(foldSessionTitle([event])?.title);
|
|
16872
|
+
if (isKnob) refreshStatus();
|
|
16873
|
+
if (event.type === "agent/inbox/spliced") queueMicrotask(refreshQueue);
|
|
15167
16874
|
if (event.type === "turn/start") app.setWorking(true);
|
|
15168
16875
|
else if (event.type === "turn/end") {
|
|
15169
16876
|
app.setWorking(false);
|
|
@@ -15172,14 +16879,16 @@ function apply(ctx, config) {
|
|
|
15172
16879
|
sessions.flush(liveAgent.session);
|
|
15173
16880
|
} else if (event.type === "step/start") refreshStatus();
|
|
15174
16881
|
});
|
|
15175
|
-
|
|
15176
|
-
|
|
15177
|
-
|
|
15178
|
-
|
|
15179
|
-
|
|
15180
|
-
|
|
15181
|
-
|
|
15182
|
-
|
|
16882
|
+
if (liveAgent !== void 0) {
|
|
16883
|
+
app.setPlanMode(foldPlanMode(liveAgent.session.events));
|
|
16884
|
+
app.setWorking(workingFromLog(liveAgent.session.events));
|
|
16885
|
+
app.setSessionTitle(foldSessionTitle(liveAgent.session.events)?.title);
|
|
16886
|
+
for (let index = liveAgent.session.events.length - 1; index >= 0; index -= 1) {
|
|
16887
|
+
const event = liveAgent.session.events[index];
|
|
16888
|
+
if (event.type === "todo/write") {
|
|
16889
|
+
app.setTodoSummary(event.data.todos);
|
|
16890
|
+
break;
|
|
16891
|
+
}
|
|
15183
16892
|
}
|
|
15184
16893
|
}
|
|
15185
16894
|
ctx.on("approval/request", (req, next) => {
|
|
@@ -15199,8 +16908,10 @@ function apply(ctx, config) {
|
|
|
15199
16908
|
id: question.id,
|
|
15200
16909
|
question: question.question,
|
|
15201
16910
|
...question.header !== void 0 ? { header: question.header } : {},
|
|
16911
|
+
...question.detail !== void 0 ? { detail: question.detail } : {},
|
|
15202
16912
|
...question.options !== void 0 ? { options: question.options } : {},
|
|
15203
|
-
...question.multiSelect !== void 0 ? { multiSelect: question.multiSelect } : {}
|
|
16913
|
+
...question.multiSelect !== void 0 ? { multiSelect: question.multiSelect } : {},
|
|
16914
|
+
...question.intent !== void 0 ? { intent: question.intent } : {}
|
|
15204
16915
|
})), request.signal)).map((answer) => ({
|
|
15205
16916
|
id: answer.id,
|
|
15206
16917
|
selected: answer.selected,
|
|
@@ -15208,9 +16919,12 @@ function apply(ctx, config) {
|
|
|
15208
16919
|
})) };
|
|
15209
16920
|
} });
|
|
15210
16921
|
})().catch((error) => {
|
|
15211
|
-
|
|
16922
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16923
|
+
ctx.logger.error(`tui-runner: ${message}`);
|
|
16924
|
+
diag.error("fatal", { error: message });
|
|
16925
|
+
diag.dispose();
|
|
15212
16926
|
exit(1);
|
|
15213
16927
|
});
|
|
15214
16928
|
}
|
|
15215
16929
|
//#endregion
|
|
15216
|
-
export { Config, apply, composeAgent, dangerCommand, forkSeed, inject, name, recomposeBlank, recordedPreset };
|
|
16930
|
+
export { Config, apply, composeAgent, dangerCommand, forkSeed, inject, name, recomposeBlank, recordedPreset, resumeCommand, runningProfile };
|