@songtonyli/dsh-cli 0.1.11 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/dsh.mjs +1 -1
- package/node_modules/@deepseek-ai/dsh-client-ui-sidebar/lib/client.js +1 -1
- package/node_modules/@deepseek-ai/dsh-tui-app/README.i18n.yaml +2 -2
- package/node_modules/@deepseek-ai/dsh-tui-app/README.md +25 -14
- package/node_modules/@deepseek-ai/dsh-tui-app/README.zh.md +25 -14
- package/node_modules/@deepseek-ai/dsh-tui-app/lib/index.js +1219 -175
- package/package.json +1 -1
|
@@ -416,6 +416,20 @@ function toolResultLines(view, content) {
|
|
|
416
416
|
}
|
|
417
417
|
}
|
|
418
418
|
/**
|
|
419
|
+
* Cut rows to a maximum, naming what was left out on one trailing row. Every
|
|
420
|
+
* surface that shows part of something longer folds it this way - the collapsed
|
|
421
|
+
* tool card, an approval's call detail, the focused-section inspector - and each
|
|
422
|
+
* names the key that shows the rest.
|
|
423
|
+
* @param lines - the full rows.
|
|
424
|
+
* @param keep - rows kept ahead of the marker.
|
|
425
|
+
* @param marker - builds the trailing row from the number of rows left out.
|
|
426
|
+
* @returns the rows to draw; a copy of `lines` when they all fit.
|
|
427
|
+
*/
|
|
428
|
+
function foldRows(lines, keep, marker) {
|
|
429
|
+
if (lines.length <= keep) return [...lines];
|
|
430
|
+
return [...lines.slice(0, keep), marker(lines.length - keep)];
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
419
433
|
* Cut a card body to its collapsed preview.
|
|
420
434
|
* @param lines - the full body rows.
|
|
421
435
|
* @param previewLines - rows kept when collapsed.
|
|
@@ -423,9 +437,8 @@ function toolResultLines(view, content) {
|
|
|
423
437
|
* @returns the rows to draw, with a trailing count of hidden rows when cut.
|
|
424
438
|
*/
|
|
425
439
|
function previewLines(lines, previewLines, expanded) {
|
|
426
|
-
if (expanded
|
|
427
|
-
|
|
428
|
-
return [...lines.slice(0, previewLines), `… ${String(hidden)} more line${hidden === 1 ? "" : "s"} (Ctrl+O expands)`];
|
|
440
|
+
if (expanded) return [...lines];
|
|
441
|
+
return foldRows(lines, previewLines, (hidden) => `… ${String(hidden)} more line${hidden === 1 ? "" : "s"} (Ctrl+O expands)`);
|
|
429
442
|
}
|
|
430
443
|
//#endregion
|
|
431
444
|
//#region lib/types/catalog.js
|
|
@@ -814,6 +827,16 @@ const LUMINANCE = {
|
|
|
814
827
|
};
|
|
815
828
|
/** Largest value one color channel encodes. */
|
|
816
829
|
const CHANNEL_MAX = 255;
|
|
830
|
+
/** Encoded sRGB value below which the transfer function is the linear segment. */
|
|
831
|
+
const SRGB_LINEAR_CUT = .04045;
|
|
832
|
+
/** Linear intensity below which the sRGB transfer function is the linear segment. */
|
|
833
|
+
const LINEAR_SRGB_CUT = .0031308;
|
|
834
|
+
/** Slope of the sRGB transfer function's linear segment. */
|
|
835
|
+
const SRGB_LINEAR_SLOPE = 12.92;
|
|
836
|
+
/** Offset of the sRGB transfer function's power segment. */
|
|
837
|
+
const SRGB_OFFSET = .055;
|
|
838
|
+
/** Exponent of the sRGB transfer function's power segment. */
|
|
839
|
+
const SRGB_GAMMA = 2.4;
|
|
817
840
|
/**
|
|
818
841
|
* Grapheme segmenter for the reverse column walk. pi-tui keeps its own
|
|
819
842
|
* segmenter private, so this module holds one; grapheme segmentation does not
|
|
@@ -842,44 +865,68 @@ function resolveFadeCapability(input) {
|
|
|
842
865
|
return term.includes("256color") ? "ansi256" : "dim";
|
|
843
866
|
}
|
|
844
867
|
/**
|
|
845
|
-
* Build the brightness ramp a chunk climbs
|
|
846
|
-
*
|
|
847
|
-
*
|
|
848
|
-
*
|
|
868
|
+
* Build the brightness ramp a chunk climbs: level `k` sits at
|
|
869
|
+
* `t = (k + 1) / steps` of the way from `bg` to `fg`, eased by the smoothstep
|
|
870
|
+
* `t * t * (3 - 2 * t)` and interpolated with each channel in linear light.
|
|
871
|
+
* Both shape the ramp against what the eye reads rather than what the byte
|
|
872
|
+
* says: sRGB bytes are gamma-encoded, so mixing them directly bunches the
|
|
873
|
+
* visible change into the dark end, and an even ramp of levels arrives with a
|
|
874
|
+
* hard start and stop. The last level is a copy of `fg` rather than a computed
|
|
875
|
+
* value, so settled text and the last faded frame carry identical color.
|
|
849
876
|
* @param bg - the terminal background color.
|
|
850
877
|
* @param fg - the normal foreground color.
|
|
851
878
|
* @param steps - brightness levels; defaults to {@link FADE_STEPS}.
|
|
852
879
|
* @returns `steps` levels, darkest first.
|
|
853
880
|
*/
|
|
854
|
-
function buildFadeRamp(bg, fg, steps =
|
|
881
|
+
function buildFadeRamp(bg, fg, steps = 8) {
|
|
855
882
|
return Array.from({ length: steps }, (_unused, level) => {
|
|
856
883
|
if (level === steps - 1) return {
|
|
857
884
|
r: fg.r,
|
|
858
885
|
g: fg.g,
|
|
859
886
|
b: fg.b
|
|
860
887
|
};
|
|
861
|
-
const
|
|
888
|
+
const position = (level + 1) / steps;
|
|
889
|
+
const eased = position * position * (3 - 2 * position);
|
|
862
890
|
return {
|
|
863
|
-
r: mix(bg.r, fg.r,
|
|
864
|
-
g: mix(bg.g, fg.g,
|
|
865
|
-
b: mix(bg.b, fg.b,
|
|
891
|
+
r: mix(bg.r, fg.r, eased),
|
|
892
|
+
g: mix(bg.g, fg.g, eased),
|
|
893
|
+
b: mix(bg.b, fg.b, eased)
|
|
866
894
|
};
|
|
867
895
|
});
|
|
868
896
|
}
|
|
869
897
|
/**
|
|
870
|
-
* Interpolate one channel.
|
|
871
|
-
* @param from - the background channel.
|
|
872
|
-
* @param to - the foreground channel.
|
|
898
|
+
* Interpolate one channel in linear light.
|
|
899
|
+
* @param from - the background channel, sRGB-encoded.
|
|
900
|
+
* @param to - the foreground channel, sRGB-encoded.
|
|
873
901
|
* @param ratio - position between them, 0 at `from` and 1 at `to`.
|
|
874
|
-
* @returns the channel value rounded to a byte.
|
|
902
|
+
* @returns the sRGB-encoded channel value rounded to a byte.
|
|
875
903
|
*/
|
|
876
904
|
function mix(from, to, ratio) {
|
|
877
|
-
|
|
905
|
+
const linear = toLinear(from);
|
|
906
|
+
return toSrgb(linear + (toLinear(to) - linear) * ratio);
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Decode one sRGB channel to linear light, by the sRGB transfer function.
|
|
910
|
+
* @param value - the channel byte.
|
|
911
|
+
* @returns the linear intensity in 0..1.
|
|
912
|
+
*/
|
|
913
|
+
function toLinear(value) {
|
|
914
|
+
const encoded = value / CHANNEL_MAX;
|
|
915
|
+
return encoded <= SRGB_LINEAR_CUT ? encoded / SRGB_LINEAR_SLOPE : ((encoded + SRGB_OFFSET) / 1.055) ** SRGB_GAMMA;
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Encode linear light back to an sRGB channel byte.
|
|
919
|
+
* @param value - the linear intensity in 0..1.
|
|
920
|
+
* @returns the channel byte.
|
|
921
|
+
*/
|
|
922
|
+
function toSrgb(value) {
|
|
923
|
+
const encoded = value <= LINEAR_SRGB_CUT ? SRGB_LINEAR_SLOPE * value : 1.055 * value ** (1 / SRGB_GAMMA) - SRGB_OFFSET;
|
|
924
|
+
return Math.round(encoded * CHANNEL_MAX);
|
|
878
925
|
}
|
|
879
926
|
/**
|
|
880
927
|
* The SGR sequence one age draws under.
|
|
881
928
|
* @param style - the capability and the ramp.
|
|
882
|
-
* @param age -
|
|
929
|
+
* @param age - the brightness level; levels past the ramp draw its last one.
|
|
883
930
|
* @returns the sequence to open the run with, or the empty string when the
|
|
884
931
|
* chunk draws as the component rendered it, which is also what an empty ramp
|
|
885
932
|
* yields under a color capability.
|
|
@@ -926,32 +973,66 @@ function channel(value) {
|
|
|
926
973
|
function restoreFor(capability) {
|
|
927
974
|
return capability === "dim" ? RESET_INTENSITY : RESET_FOREGROUND;
|
|
928
975
|
}
|
|
976
|
+
/** Every SGR sequence, the sequences a block's own palette styling writes. */
|
|
977
|
+
const SGR_SEQUENCE = /\u001b\[[0-9;]*m/g;
|
|
929
978
|
/**
|
|
930
|
-
*
|
|
931
|
-
*
|
|
979
|
+
* Draw whole rendered lines at one brightness level, for a block that fades in
|
|
980
|
+
* as a unit rather than word by word.
|
|
981
|
+
*
|
|
982
|
+
* The level is opened at the start of the line and reasserted after every SGR
|
|
983
|
+
* the line already carries, so the palette colors inside a card - the status
|
|
984
|
+
* glyph, the dim body rule, the bold tool name - are overridden while the card
|
|
985
|
+
* fades and come back on their own once it settles. Each line ends with the
|
|
986
|
+
* sequence that undoes what the level set - the terminal's own foreground, or
|
|
987
|
+
* its normal intensity in the two-level mode - so the level never leaks past
|
|
988
|
+
* the line it was applied to. Empty lines come back byte-identical, so
|
|
989
|
+
* the renderer leaves the blank rows around a card alone.
|
|
990
|
+
* @param lines - the rendered lines of the block.
|
|
991
|
+
* @param age - the brightness level to draw them at.
|
|
992
|
+
* @param style - the capability and the ramp.
|
|
993
|
+
* @param from - first line the level is applied to; the lines before it come
|
|
994
|
+
* back byte-identical, which is how a caller keeps rows the renderer can no
|
|
995
|
+
* longer repaint out of the fade.
|
|
996
|
+
* @returns the lines at that level; copies when the style writes no sequence
|
|
997
|
+
* for this age, which is what the `none` capability and a settled age yield.
|
|
998
|
+
*/
|
|
999
|
+
function recolorLines(lines, age, style, from = 0) {
|
|
1000
|
+
const sgr = fadeSgr(style, age);
|
|
1001
|
+
if (sgr === "") return [...lines];
|
|
1002
|
+
const restore = restoreFor(style.capability);
|
|
1003
|
+
return lines.map((line, index) => index < from || line === "" ? line : `${sgr}${line.replace(SGR_SEQUENCE, (match) => match + sgr)}${restore}`);
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* The tail of one streaming region - the visible text or the reasoning of one
|
|
1007
|
+
* message: the chunks young enough to be recolored, each ageing on the wall
|
|
1008
|
+
* clock from the moment it became visible.
|
|
932
1009
|
*
|
|
933
1010
|
* Deltas are split at word boundaries, which reads more smoothly than raw
|
|
934
1011
|
* token edges. A delta that ends mid-word leaves that word open, and the next
|
|
935
1012
|
* delta extends it rather than starting a second chunk, so the word keeps the
|
|
936
|
-
*
|
|
1013
|
+
* moment it first became visible.
|
|
937
1014
|
*
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
1015
|
+
* Ages are read, never counted. A chunk's age is the elapsed time since it
|
|
1016
|
+
* became visible divided by `stepMs`, computed at the moment a render asks for
|
|
1017
|
+
* it, so a render triggered by a delta between two fade periods draws every
|
|
1018
|
+
* word at its own level and words do not move in lockstep. That is what makes
|
|
1019
|
+
* the trailing edge continuous rather than banded.
|
|
1020
|
+
*
|
|
1021
|
+
* Arrival rate changes the tail's length, never its depth: whatever the rate,
|
|
1022
|
+
* a chunk is at the foreground `steps * stepMs` after it appeared, so a fast
|
|
1023
|
+
* stream leaves a longer trail of brightening words and never a darker or a
|
|
1024
|
+
* lasting one. The tail is bounded in time, so it stays on at any rate.
|
|
944
1025
|
*/
|
|
945
1026
|
var FadeTracker = class {
|
|
946
1027
|
steps;
|
|
947
|
-
|
|
948
|
-
now
|
|
1028
|
+
stepMs;
|
|
1029
|
+
now;
|
|
949
1030
|
chunks = [];
|
|
950
1031
|
openChunk = void 0;
|
|
951
|
-
arrivals = /* @__PURE__ */ new Set();
|
|
952
1032
|
constructor(options) {
|
|
953
|
-
this.steps = options
|
|
954
|
-
this.
|
|
1033
|
+
this.steps = options.steps ?? 8;
|
|
1034
|
+
this.stepMs = options.stepMs ?? 33;
|
|
1035
|
+
this.now = options.now;
|
|
955
1036
|
}
|
|
956
1037
|
/**
|
|
957
1038
|
* Take one stream event.
|
|
@@ -959,42 +1040,39 @@ var FadeTracker = class {
|
|
|
959
1040
|
*/
|
|
960
1041
|
append(delta) {
|
|
961
1042
|
if (delta === "") return;
|
|
962
|
-
this.
|
|
963
|
-
if (this.isFastStream()) {
|
|
964
|
-
this.flush();
|
|
965
|
-
return;
|
|
966
|
-
}
|
|
1043
|
+
const at = this.now();
|
|
967
1044
|
const open = this.openChunk;
|
|
968
1045
|
const text = open === void 0 ? delta : open.text + delta;
|
|
969
|
-
const
|
|
1046
|
+
const bornAt = open === void 0 ? at : open.bornAt;
|
|
970
1047
|
if (open !== void 0) this.chunks.pop();
|
|
971
1048
|
this.openChunk = void 0;
|
|
972
1049
|
const words = text.match(/\s*\S+\s*/g);
|
|
973
1050
|
if (words === null) {
|
|
974
1051
|
this.openChunk = {
|
|
975
1052
|
text,
|
|
976
|
-
|
|
1053
|
+
bornAt
|
|
977
1054
|
};
|
|
978
1055
|
this.chunks.push(this.openChunk);
|
|
979
1056
|
return;
|
|
980
1057
|
}
|
|
981
1058
|
for (const [index, word] of words.entries()) this.chunks.push({
|
|
982
1059
|
text: word,
|
|
983
|
-
|
|
1060
|
+
bornAt: index === 0 ? bornAt : at
|
|
984
1061
|
});
|
|
985
1062
|
if (!/\s$/.test(text)) this.openChunk = this.chunks.at(-1);
|
|
986
1063
|
}
|
|
987
1064
|
/**
|
|
988
|
-
*
|
|
989
|
-
*
|
|
1065
|
+
* Drop the chunks that reached the step count, which the application runs
|
|
1066
|
+
* once per fade period so a settled chunk stops being matched against the
|
|
1067
|
+
* rendered lines.
|
|
1068
|
+
* @returns whether a chunk still draws below the last brightness level, and
|
|
1069
|
+
* so whether the tail keeps moving after this period.
|
|
990
1070
|
*/
|
|
991
1071
|
tick() {
|
|
992
|
-
const
|
|
993
|
-
this.
|
|
994
|
-
this.chunks = this.chunks.filter((chunk) => this.now - chunk.born < this.steps);
|
|
1072
|
+
const moving = this.needsRepaint();
|
|
1073
|
+
this.chunks = this.chunks.filter((chunk) => this.ageOf(chunk) < this.steps);
|
|
995
1074
|
if (this.openChunk !== void 0 && !this.chunks.includes(this.openChunk)) this.openChunk = void 0;
|
|
996
|
-
|
|
997
|
-
return changing;
|
|
1075
|
+
return moving;
|
|
998
1076
|
}
|
|
999
1077
|
/**
|
|
1000
1078
|
* Whether the current frame still differs from the settled rendering.
|
|
@@ -1002,36 +1080,98 @@ var FadeTracker = class {
|
|
|
1002
1080
|
* is the only reason to keep ticking.
|
|
1003
1081
|
*/
|
|
1004
1082
|
needsRepaint() {
|
|
1005
|
-
return this.chunks.some((chunk) => this.
|
|
1083
|
+
return this.chunks.some((chunk) => this.ageOf(chunk) < this.steps - 1);
|
|
1006
1084
|
}
|
|
1007
1085
|
/**
|
|
1008
1086
|
* The tail {@link recolorTail} recolors.
|
|
1009
|
-
* @returns one span per tracked chunk, oldest first, each with
|
|
1087
|
+
* @returns one span per tracked chunk, oldest first, each with the age it
|
|
1088
|
+
* carries at this instant.
|
|
1010
1089
|
*/
|
|
1011
1090
|
spans() {
|
|
1012
1091
|
return this.chunks.map((chunk) => ({
|
|
1013
1092
|
text: chunk.text,
|
|
1014
|
-
age: this.
|
|
1093
|
+
age: this.ageOf(chunk)
|
|
1015
1094
|
}));
|
|
1016
1095
|
}
|
|
1017
1096
|
/**
|
|
1018
1097
|
* Drop the whole tail so every chunk drawn so far renders at the foreground,
|
|
1019
1098
|
* and start tracking again from the next delta. The application calls this on
|
|
1020
|
-
* a terminal width change and at stream end.
|
|
1021
|
-
* a resize says nothing about the arrival rate.
|
|
1099
|
+
* a terminal width change and at stream end.
|
|
1022
1100
|
*/
|
|
1023
1101
|
flush() {
|
|
1024
1102
|
this.chunks = [];
|
|
1025
1103
|
this.openChunk = void 0;
|
|
1026
1104
|
}
|
|
1027
1105
|
/**
|
|
1028
|
-
* The
|
|
1029
|
-
* @
|
|
1106
|
+
* The brightness level one chunk draws at right now.
|
|
1107
|
+
* @param chunk - the tracked chunk.
|
|
1108
|
+
* @returns levels elapsed since it became visible, 0 for a chunk younger than one level.
|
|
1109
|
+
*/
|
|
1110
|
+
ageOf(chunk) {
|
|
1111
|
+
return Math.floor((this.now() - chunk.bornAt) / this.stepMs);
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
/**
|
|
1115
|
+
* The age of one whole block that fades in as a unit - a tool card's rows,
|
|
1116
|
+
* which arrive complete rather than word by word.
|
|
1117
|
+
*
|
|
1118
|
+
* The last level is withheld the way the word tail withholds it: that level is
|
|
1119
|
+
* an assumed foreground, so a block one level below the end is handed back to
|
|
1120
|
+
* the terminal's own colors instead, and nothing jumps color as it settles.
|
|
1121
|
+
*/
|
|
1122
|
+
var BlockFadeClock = class {
|
|
1123
|
+
options;
|
|
1124
|
+
constructor(options) {
|
|
1125
|
+
this.options = options;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* The brightness level the block draws at right now.
|
|
1129
|
+
* @returns the level, or undefined once the block reached the last drawn
|
|
1130
|
+
* level and renders in the colors the component itself produced.
|
|
1131
|
+
*/
|
|
1132
|
+
age() {
|
|
1133
|
+
const { bornAt, stepMs, steps, now } = this.options;
|
|
1134
|
+
const age = Math.floor((now() - bornAt) / stepMs);
|
|
1135
|
+
return age < steps - 1 ? age : void 0;
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Whether this block still draws below the last brightness level.
|
|
1139
|
+
* @returns true while {@link BlockFadeClock.age} yields a level.
|
|
1030
1140
|
*/
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1141
|
+
needsRepaint() {
|
|
1142
|
+
return this.age() !== void 0;
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
/**
|
|
1146
|
+
* The block fades running right now. The application arms its repaint while
|
|
1147
|
+
* any member still moves, so the registry holds only members that have not
|
|
1148
|
+
* settled: {@link FadeRegistry.tick} drops the settled ones once per period,
|
|
1149
|
+
* and a session change clears the whole set.
|
|
1150
|
+
*/
|
|
1151
|
+
var FadeRegistry = class {
|
|
1152
|
+
members = /* @__PURE__ */ new Set();
|
|
1153
|
+
/**
|
|
1154
|
+
* Track one fade until it settles.
|
|
1155
|
+
* @param fade - the fade to follow.
|
|
1156
|
+
*/
|
|
1157
|
+
add(fade) {
|
|
1158
|
+
this.members.add(fade);
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Whether any tracked fade still moves.
|
|
1162
|
+
* @returns true while one of them needs another repaint.
|
|
1163
|
+
*/
|
|
1164
|
+
needsRepaint() {
|
|
1165
|
+
for (const member of this.members) if (member.needsRepaint()) return true;
|
|
1166
|
+
return false;
|
|
1167
|
+
}
|
|
1168
|
+
/** Forget the fades that settled, so a long session accumulates none of them. */
|
|
1169
|
+
tick() {
|
|
1170
|
+
for (const member of this.members) if (!member.needsRepaint()) this.members.delete(member);
|
|
1171
|
+
}
|
|
1172
|
+
/** Forget every tracked fade, which the application does when it draws another session. */
|
|
1173
|
+
clear() {
|
|
1174
|
+
this.members.clear();
|
|
1035
1175
|
}
|
|
1036
1176
|
};
|
|
1037
1177
|
/**
|
|
@@ -1056,10 +1196,13 @@ var FadeTracker = class {
|
|
|
1056
1196
|
* @param lines - the rendered lines of the streaming block, newest text last.
|
|
1057
1197
|
* @param spans - the tail from {@link FadeTracker.spans}, oldest first.
|
|
1058
1198
|
* @param style - the capability and the ramp.
|
|
1199
|
+
* @param from - first line the tail may recolor; the lines before it come back
|
|
1200
|
+
* byte-identical, which is how a caller keeps rows the renderer can no longer
|
|
1201
|
+
* repaint out of the fade.
|
|
1059
1202
|
* @returns the lines with the tail recolored; lines the tail does not cover
|
|
1060
1203
|
* are returned byte-identical, so the renderer leaves them alone.
|
|
1061
1204
|
*/
|
|
1062
|
-
function recolorTail(lines, spans, style) {
|
|
1205
|
+
function recolorTail(lines, spans, style, from = 0) {
|
|
1063
1206
|
if (style.capability === "none" || spans.length === 0 || lines.length === 0) return [...lines];
|
|
1064
1207
|
const cells = cellsFromEnd(lines);
|
|
1065
1208
|
const runs = [];
|
|
@@ -1077,7 +1220,7 @@ function recolorTail(lines, spans, style) {
|
|
|
1077
1220
|
}
|
|
1078
1221
|
const restore = restoreFor(style.capability);
|
|
1079
1222
|
return lines.map((text, index) => {
|
|
1080
|
-
const lineRuns = byLine.get(index);
|
|
1223
|
+
const lineRuns = index < from ? void 0 : byLine.get(index);
|
|
1081
1224
|
return lineRuns === void 0 ? text : paintLine(text, lineRuns, restore);
|
|
1082
1225
|
});
|
|
1083
1226
|
}
|
|
@@ -1281,20 +1424,74 @@ function editorTheme(palette) {
|
|
|
1281
1424
|
* Transcript components: one pi-tui component per rendered fact (a user
|
|
1282
1425
|
* prompt, an assistant reply, a tool card, a notice). Each owns its display
|
|
1283
1426
|
* state and re-renders from it at any width.
|
|
1427
|
+
*
|
|
1428
|
+
* The prompt, the reply, and the tool card are also the navigable blocks the
|
|
1429
|
+
* keyboard walks (`./navigation.ts`): each reports its sections as plain source
|
|
1430
|
+
* rows and draws a two-column gutter beside them while it holds the focus -
|
|
1431
|
+
* accented on the focused section's own lines and dim on the rest of the
|
|
1432
|
+
* block. The gutter narrows the width the block's content wraps at and is
|
|
1433
|
+
* prepended after any fade recoloring, so the fade keeps matching the block's
|
|
1434
|
+
* own text and the gutter's styling never enters that match.
|
|
1284
1435
|
* @module @deepseek-ai/dsh-tui-app/blocks
|
|
1285
1436
|
*/
|
|
1437
|
+
/** Columns the focus gutter takes from the width a block's content wraps at. */
|
|
1438
|
+
const GUTTER_WIDTH = 2;
|
|
1439
|
+
/** The gutter beside the lines of a focused block that are not the focused section. */
|
|
1440
|
+
const BLOCK_GUTTER = "│ ";
|
|
1441
|
+
/** The gutter beside the lines of the focused section itself. */
|
|
1442
|
+
const PART_GUTTER = "┃ ";
|
|
1443
|
+
/** What the call section reports for a tool the model called with no arguments. */
|
|
1444
|
+
const NO_ARGUMENTS = "(no arguments)";
|
|
1445
|
+
/** What the result section reports for a tool that answered with nothing. */
|
|
1446
|
+
const NO_OUTPUT = "(no output)";
|
|
1447
|
+
/**
|
|
1448
|
+
* Prepend the focus gutter to every line of a block that holds the focus.
|
|
1449
|
+
* @param palette - the palette the gutter marks are styled with.
|
|
1450
|
+
* @param lines - the block's final rendered lines, fades already applied.
|
|
1451
|
+
* @param focused - whether the line at one index belongs to the focused section.
|
|
1452
|
+
* @returns the lines behind their gutter.
|
|
1453
|
+
*/
|
|
1454
|
+
function withGutter(palette, lines, focused) {
|
|
1455
|
+
return lines.map((line, index) => `${focused(index) ? palette.accent(PART_GUTTER) : palette.dim(BLOCK_GUTTER)}${line}`);
|
|
1456
|
+
}
|
|
1286
1457
|
/** A prompt the user submitted, drawn with a leading `›`. */
|
|
1287
1458
|
var UserBlock = class {
|
|
1288
1459
|
theme;
|
|
1289
1460
|
text;
|
|
1290
|
-
|
|
1461
|
+
turn;
|
|
1462
|
+
navigable = true;
|
|
1463
|
+
blockKind = "user";
|
|
1464
|
+
/** The section drawn as focused; absent while the keyboard is elsewhere. */
|
|
1465
|
+
highlight;
|
|
1466
|
+
constructor(theme, text, turn) {
|
|
1291
1467
|
this.theme = theme;
|
|
1292
1468
|
this.text = text;
|
|
1469
|
+
this.turn = turn;
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* The prompt as one navigable section.
|
|
1473
|
+
* @returns the single `user` part, carrying the submitted text.
|
|
1474
|
+
*/
|
|
1475
|
+
parts() {
|
|
1476
|
+
return [{
|
|
1477
|
+
kind: "user",
|
|
1478
|
+
rows: this.text.split("\n")
|
|
1479
|
+
}];
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Draw this prompt with the focus gutter, or without it.
|
|
1483
|
+
* @param part - the focused section, or undefined to clear the mark.
|
|
1484
|
+
*/
|
|
1485
|
+
setHighlight(part) {
|
|
1486
|
+
this.highlight = part;
|
|
1293
1487
|
}
|
|
1294
1488
|
invalidate() {}
|
|
1295
1489
|
render(width) {
|
|
1296
1490
|
const palette = this.theme.palette;
|
|
1297
|
-
|
|
1491
|
+
const marked = this.highlight !== void 0;
|
|
1492
|
+
const inner = marked ? Math.max(1, width - GUTTER_WIDTH) : width;
|
|
1493
|
+
const lines = ["", ...wrapTextWithAnsi(this.text, Math.max(1, inner - 2)).map((line, index) => `${palette.accent(index === 0 ? "›" : " ")} ${palette.bold(line)}`)];
|
|
1494
|
+
return marked ? withGutter(palette, lines, () => true) : lines;
|
|
1298
1495
|
}
|
|
1299
1496
|
};
|
|
1300
1497
|
/** A dim one-line notice about the session (a stopped turn, a command result, a model switch). */
|
|
@@ -1315,28 +1512,66 @@ var NoticeBlock = class {
|
|
|
1315
1512
|
* An assistant reply: streamed reasoning above streamed Markdown text. The
|
|
1316
1513
|
* durable `assistant/message` replaces both with the committed content.
|
|
1317
1514
|
*
|
|
1318
|
-
* A block that is streaming right now can carry a {@link FadeRender}
|
|
1319
|
-
*
|
|
1320
|
-
*
|
|
1321
|
-
*
|
|
1515
|
+
* A block that is streaming right now can carry a {@link FadeRender} for its
|
|
1516
|
+
* text and another for its reasoning, each drawing that region's newest words
|
|
1517
|
+
* dimmed and brightening. A block rebuilt from history carries neither, and
|
|
1518
|
+
* {@link AssistantBlock.commit} drops the ones a streaming block had, so
|
|
1519
|
+
* settled text is never recolored.
|
|
1322
1520
|
*/
|
|
1323
1521
|
var AssistantBlock = class {
|
|
1324
1522
|
theme;
|
|
1523
|
+
turn;
|
|
1524
|
+
navigable = true;
|
|
1525
|
+
blockKind = "assistant";
|
|
1325
1526
|
reasoning = "";
|
|
1326
1527
|
text = "";
|
|
1327
1528
|
interrupted = false;
|
|
1328
1529
|
markdown;
|
|
1329
1530
|
reasoningText;
|
|
1330
1531
|
fade;
|
|
1331
|
-
/** Width of the last render that drew a tail; absent before the first one. */
|
|
1532
|
+
/** Width of the last render that drew a text tail; absent before the first one. */
|
|
1332
1533
|
fadeWidth;
|
|
1333
|
-
|
|
1534
|
+
reasoningFade;
|
|
1535
|
+
/** Width of the last render that drew a reasoning tail; absent before the first one. */
|
|
1536
|
+
reasoningFadeWidth;
|
|
1537
|
+
/** First line of this block either tail may still recolor. */
|
|
1538
|
+
repaintFloor = 0;
|
|
1539
|
+
/** The section drawn as focused; absent while the keyboard is elsewhere. */
|
|
1540
|
+
highlight;
|
|
1541
|
+
constructor(theme, turn) {
|
|
1334
1542
|
this.theme = theme;
|
|
1543
|
+
this.turn = turn;
|
|
1335
1544
|
const palette = theme.palette;
|
|
1336
1545
|
this.markdown = new Markdown("", 0, 0, markdownTheme(palette));
|
|
1337
1546
|
this.reasoningText = new Text("", 0, 0);
|
|
1338
1547
|
}
|
|
1339
1548
|
/**
|
|
1549
|
+
* The message as navigable sections, carrying the model's own text: the
|
|
1550
|
+
* reasoning as it was streamed or committed, and the reply as Markdown
|
|
1551
|
+
* source rather than the rendering the transcript draws.
|
|
1552
|
+
* @returns the reasoning part while the message has reasoning, then the
|
|
1553
|
+
* reply, which is present from the start and empty until text arrives.
|
|
1554
|
+
*/
|
|
1555
|
+
parts() {
|
|
1556
|
+
const parts = [];
|
|
1557
|
+
if (this.reasoning.trim() !== "") parts.push({
|
|
1558
|
+
kind: "reasoning",
|
|
1559
|
+
rows: this.reasoning.split("\n")
|
|
1560
|
+
});
|
|
1561
|
+
parts.push({
|
|
1562
|
+
kind: "reply",
|
|
1563
|
+
rows: this.text.split("\n")
|
|
1564
|
+
});
|
|
1565
|
+
return parts;
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Draw this message with the focus gutter, or without it.
|
|
1569
|
+
* @param part - the focused section, or undefined to clear the mark.
|
|
1570
|
+
*/
|
|
1571
|
+
setHighlight(part) {
|
|
1572
|
+
this.highlight = part;
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1340
1575
|
* Draw this block's newest text through `fade` until it commits.
|
|
1341
1576
|
* @param fade - the tail and drawing settings of the running stream.
|
|
1342
1577
|
*/
|
|
@@ -1344,6 +1579,26 @@ var AssistantBlock = class {
|
|
|
1344
1579
|
this.fade = fade;
|
|
1345
1580
|
}
|
|
1346
1581
|
/**
|
|
1582
|
+
* Draw this block's newest reasoning through `fade` until it commits.
|
|
1583
|
+
* @param fade - the tail and drawing settings of the running stream.
|
|
1584
|
+
*/
|
|
1585
|
+
setReasoningFade(fade) {
|
|
1586
|
+
this.reasoningFade = fade;
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Hand the rows the renderer can no longer repaint back to the colors this
|
|
1590
|
+
* block drew them in, so a tail that is still moving never rewrites them.
|
|
1591
|
+
* @param floor - this block's own first repaintable line; the application
|
|
1592
|
+
* raises it as the frame grows and never lowers it.
|
|
1593
|
+
* @returns whether the floor took rows away from a tail that is drawing
|
|
1594
|
+
* right now, and so whether the frame differs from the one just built.
|
|
1595
|
+
*/
|
|
1596
|
+
setRepaintFloor(floor) {
|
|
1597
|
+
if (floor <= this.repaintFloor) return false;
|
|
1598
|
+
this.repaintFloor = floor;
|
|
1599
|
+
return drawnSpans(this.fade).length > 0 || drawnSpans(this.reasoningFade).length > 0;
|
|
1600
|
+
}
|
|
1601
|
+
/**
|
|
1347
1602
|
* Append streamed visible text.
|
|
1348
1603
|
* @param delta - the text delta.
|
|
1349
1604
|
*/
|
|
@@ -1370,6 +1625,7 @@ var AssistantBlock = class {
|
|
|
1370
1625
|
this.reasoning = reasoning;
|
|
1371
1626
|
this.interrupted = interrupted;
|
|
1372
1627
|
this.fade = void 0;
|
|
1628
|
+
this.reasoningFade = void 0;
|
|
1373
1629
|
this.markdown.setText(text);
|
|
1374
1630
|
this.reasoningText.setText(this.theme.palette.dim(this.theme.palette.italic(reasoning.trimEnd())));
|
|
1375
1631
|
}
|
|
@@ -1378,11 +1634,52 @@ var AssistantBlock = class {
|
|
|
1378
1634
|
this.reasoningText.invalidate();
|
|
1379
1635
|
}
|
|
1380
1636
|
render(width) {
|
|
1637
|
+
const marked = this.highlight !== void 0;
|
|
1638
|
+
const inner = marked ? Math.max(1, width - GUTTER_WIDTH) : width;
|
|
1381
1639
|
const lines = [""];
|
|
1382
|
-
|
|
1383
|
-
|
|
1640
|
+
const reasoning = {
|
|
1641
|
+
from: 0,
|
|
1642
|
+
to: 0
|
|
1643
|
+
};
|
|
1644
|
+
const reply = {
|
|
1645
|
+
from: 0,
|
|
1646
|
+
to: 0
|
|
1647
|
+
};
|
|
1648
|
+
if (this.reasoning.trim() !== "") {
|
|
1649
|
+
reasoning.from = lines.length;
|
|
1650
|
+
lines.push(...this.renderReasoning(inner, lines.length));
|
|
1651
|
+
reasoning.to = lines.length;
|
|
1652
|
+
lines.push("");
|
|
1653
|
+
}
|
|
1654
|
+
if (this.text !== "") {
|
|
1655
|
+
reply.from = lines.length;
|
|
1656
|
+
lines.push(...this.renderText(inner, lines.length));
|
|
1657
|
+
reply.to = lines.length;
|
|
1658
|
+
}
|
|
1384
1659
|
if (this.interrupted) lines.push(this.theme.palette.dim("[interrupted]"));
|
|
1385
|
-
return lines;
|
|
1660
|
+
if (!marked) return lines;
|
|
1661
|
+
const section = this.highlight === "reasoning" ? reasoning : reply;
|
|
1662
|
+
return withGutter(this.theme.palette, lines, (index) => index >= section.from && index < section.to);
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* The reasoning lines, with the streaming tail recolored.
|
|
1666
|
+
*
|
|
1667
|
+
* The ramp climbs towards the assumed terminal foreground, but the reasoning
|
|
1668
|
+
* draws inside the faint and italic sequences `appendReasoning` wrapped it
|
|
1669
|
+
* in, and a recolored run reasserts the styling in force at its start. The
|
|
1670
|
+
* tail therefore arrives at the dim foreground the settled reasoning carries,
|
|
1671
|
+
* not at the plain one.
|
|
1672
|
+
* @param width - the width the reasoning lays out in.
|
|
1673
|
+
* @param at - index of the region's first line in this block's render.
|
|
1674
|
+
* @returns the lines to draw.
|
|
1675
|
+
*/
|
|
1676
|
+
renderReasoning(width, at) {
|
|
1677
|
+
const fade = this.reasoningFade;
|
|
1678
|
+
const lines = this.reasoningText.render(width);
|
|
1679
|
+
if (fade === void 0) return lines;
|
|
1680
|
+
if (this.reasoningFadeWidth !== void 0 && this.reasoningFadeWidth !== width) fade.flush();
|
|
1681
|
+
this.reasoningFadeWidth = width;
|
|
1682
|
+
return recolorTail(lines, drawnSpans(fade), fade.style(), this.repaintFloor - at);
|
|
1386
1683
|
}
|
|
1387
1684
|
/**
|
|
1388
1685
|
* The Markdown lines, with the streaming tail recolored.
|
|
@@ -1391,36 +1688,102 @@ var AssistantBlock = class {
|
|
|
1391
1688
|
* given, so it gets the Markdown lines alone: the reasoning above them and
|
|
1392
1689
|
* any marker below them would put the newest chunk somewhere other than the
|
|
1393
1690
|
* end and drop the whole tail to the plain foreground.
|
|
1394
|
-
*
|
|
1395
|
-
* Only chunks younger than `steps - 1` are handed over. The last ramp level
|
|
1396
|
-
* is an assumed foreground - pi-tui reports the terminal background but not
|
|
1397
|
-
* its foreground - so the oldest visible level is left to draw in the
|
|
1398
|
-
* terminal's own foreground, which is also what the chunk draws in once it
|
|
1399
|
-
* settles. No chunk can therefore jump color as it leaves the tail.
|
|
1400
1691
|
* @param width - the width the Markdown lays out in.
|
|
1692
|
+
* @param at - index of the region's first line in this block's render.
|
|
1401
1693
|
* @returns the lines to draw.
|
|
1402
1694
|
*/
|
|
1403
|
-
renderText(width) {
|
|
1695
|
+
renderText(width, at) {
|
|
1404
1696
|
const fade = this.fade;
|
|
1405
1697
|
const lines = this.markdown.render(width);
|
|
1406
1698
|
if (fade === void 0) return lines;
|
|
1407
1699
|
if (this.fadeWidth !== void 0 && this.fadeWidth !== width) fade.flush();
|
|
1408
1700
|
this.fadeWidth = width;
|
|
1409
|
-
return recolorTail(lines, fade
|
|
1701
|
+
return recolorTail(lines, drawnSpans(fade), fade.style(), this.repaintFloor - at);
|
|
1410
1702
|
}
|
|
1411
1703
|
};
|
|
1412
|
-
/**
|
|
1704
|
+
/**
|
|
1705
|
+
* A tool call card: status glyph, tool name, headline, then a foldable body.
|
|
1706
|
+
*
|
|
1707
|
+
* The card arrives in two pieces, and each fades in on its own: the header and
|
|
1708
|
+
* the call rows when the call is logged, the result rows when the tool
|
|
1709
|
+
* answers. A card rebuilt from history carries neither fade.
|
|
1710
|
+
*/
|
|
1413
1711
|
var ToolBlock = class {
|
|
1414
1712
|
theme;
|
|
1415
1713
|
name;
|
|
1416
1714
|
call;
|
|
1715
|
+
turn;
|
|
1716
|
+
navigable = true;
|
|
1717
|
+
blockKind = "tool";
|
|
1417
1718
|
status = "running";
|
|
1418
1719
|
resultLines = [];
|
|
1419
1720
|
expanded = false;
|
|
1420
|
-
|
|
1721
|
+
fade;
|
|
1722
|
+
resultFade;
|
|
1723
|
+
/** First line of this card either fade may still recolor. */
|
|
1724
|
+
repaintFloor = 0;
|
|
1725
|
+
/** The section drawn as focused; absent while the keyboard is elsewhere. */
|
|
1726
|
+
highlight;
|
|
1727
|
+
constructor(theme, name, call, turn) {
|
|
1421
1728
|
this.theme = theme;
|
|
1422
1729
|
this.name = name;
|
|
1423
1730
|
this.call = call;
|
|
1731
|
+
this.turn = turn;
|
|
1732
|
+
}
|
|
1733
|
+
/** The card headline, as the section heading names this call. */
|
|
1734
|
+
get title() {
|
|
1735
|
+
return this.call.title;
|
|
1736
|
+
}
|
|
1737
|
+
/**
|
|
1738
|
+
* The card as navigable sections, carrying its untruncated rows whatever the
|
|
1739
|
+
* collapsed body shows.
|
|
1740
|
+
* @returns the call section, then the result section once the tool answered.
|
|
1741
|
+
*/
|
|
1742
|
+
parts() {
|
|
1743
|
+
const call = [...this.call.title === "" ? [] : [this.call.title], ...this.call.lines];
|
|
1744
|
+
const parts = [{
|
|
1745
|
+
kind: "call",
|
|
1746
|
+
rows: call.length === 0 ? [NO_ARGUMENTS] : call
|
|
1747
|
+
}];
|
|
1748
|
+
if (this.status !== "running") parts.push({
|
|
1749
|
+
kind: "result",
|
|
1750
|
+
rows: this.resultLines.length === 0 ? [NO_OUTPUT] : this.resultLines
|
|
1751
|
+
});
|
|
1752
|
+
return parts;
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Draw this card with the focus gutter, or without it.
|
|
1756
|
+
* @param part - the focused section, or undefined to clear the mark.
|
|
1757
|
+
*/
|
|
1758
|
+
setHighlight(part) {
|
|
1759
|
+
this.highlight = part;
|
|
1760
|
+
}
|
|
1761
|
+
/**
|
|
1762
|
+
* Draw the header and the call rows through `fade` until it settles.
|
|
1763
|
+
* @param fade - the level and drawing settings of this card's own fade.
|
|
1764
|
+
*/
|
|
1765
|
+
setFade(fade) {
|
|
1766
|
+
this.fade = fade;
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Draw the result rows through `fade` until it settles.
|
|
1770
|
+
* @param fade - the level and drawing settings of the result's own fade.
|
|
1771
|
+
*/
|
|
1772
|
+
setResultFade(fade) {
|
|
1773
|
+
this.resultFade = fade;
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Hand the rows the renderer can no longer repaint back to the colors this
|
|
1777
|
+
* card drew them in, so a fade that is still climbing never rewrites them.
|
|
1778
|
+
* @param floor - this card's own first repaintable line; the application
|
|
1779
|
+
* raises it as the frame grows and never lowers it.
|
|
1780
|
+
* @returns whether the floor took rows away from a fade that is drawing
|
|
1781
|
+
* right now, and so whether the frame differs from the one just built.
|
|
1782
|
+
*/
|
|
1783
|
+
setRepaintFloor(floor) {
|
|
1784
|
+
if (floor <= this.repaintFloor) return false;
|
|
1785
|
+
this.repaintFloor = floor;
|
|
1786
|
+
return this.fade?.age() !== void 0 || this.resultFade?.age() !== void 0;
|
|
1424
1787
|
}
|
|
1425
1788
|
/**
|
|
1426
1789
|
* Attach the result rows and settle the status.
|
|
@@ -1441,17 +1804,63 @@ var ToolBlock = class {
|
|
|
1441
1804
|
invalidate() {}
|
|
1442
1805
|
render(width) {
|
|
1443
1806
|
const palette = this.theme.palette;
|
|
1807
|
+
const marked = this.highlight !== void 0;
|
|
1808
|
+
const outer = marked ? Math.max(1, width - GUTTER_WIDTH) : width;
|
|
1444
1809
|
const header = `${this.status === "running" ? palette.warning("●") : this.status === "done" ? palette.success("●") : palette.error("●")} ${palette.bold(this.name)}${this.call.title === "" ? "" : ` ${palette.dim(this.call.title)}`}`;
|
|
1445
|
-
const
|
|
1446
|
-
const
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1810
|
+
const body = [...this.call.lines, ...this.resultLines];
|
|
1811
|
+
const shown = previewLines(body, this.theme.toolPreviewLines, this.expanded);
|
|
1812
|
+
const callCount = Math.min(this.call.lines.length, shown.length);
|
|
1813
|
+
const inner = Math.max(1, outer - 4);
|
|
1814
|
+
const rows = (lines) => lines.flatMap((line) => wrapTextWithAnsi(line, inner).map((part) => ` ${palette.dim("│")} ${part}`));
|
|
1815
|
+
const callLines = [...wrapTextWithAnsi(header, outer), ...rows(shown.slice(0, callCount))];
|
|
1816
|
+
const resultLines = rows(shown.slice(callCount));
|
|
1817
|
+
const call = faded(callLines, this.fade, this.repaintFloor - 1);
|
|
1818
|
+
const result = faded(resultLines, this.resultFade, this.repaintFloor - 1 - callLines.length);
|
|
1819
|
+
const lines = [
|
|
1449
1820
|
"",
|
|
1450
|
-
...
|
|
1451
|
-
...
|
|
1821
|
+
...call,
|
|
1822
|
+
...result
|
|
1452
1823
|
];
|
|
1824
|
+
if (!marked) return lines;
|
|
1825
|
+
const cut = !this.expanded && body.length > this.theme.toolPreviewLines;
|
|
1826
|
+
const sections = lines.length - (cut ? rows(shown.slice(-1)).length : 0);
|
|
1827
|
+
const section = this.highlight === "result" ? {
|
|
1828
|
+
from: 1 + callLines.length,
|
|
1829
|
+
to: lines.length
|
|
1830
|
+
} : {
|
|
1831
|
+
from: 1,
|
|
1832
|
+
to: 1 + callLines.length
|
|
1833
|
+
};
|
|
1834
|
+
return withGutter(palette, lines, (index) => index >= section.from && index < section.to && index < sections);
|
|
1453
1835
|
}
|
|
1454
1836
|
};
|
|
1837
|
+
/**
|
|
1838
|
+
* Draw one group of a card's rows at the level its fade reports.
|
|
1839
|
+
* @param lines - the group's final rendered lines.
|
|
1840
|
+
* @param fade - the group's fade; absent for a card that never faded.
|
|
1841
|
+
* @param from - first line of the group the fade may recolor.
|
|
1842
|
+
* @returns the lines at that level, or the lines themselves once the fade
|
|
1843
|
+
* settled or was never attached.
|
|
1844
|
+
*/
|
|
1845
|
+
function faded(lines, fade, from) {
|
|
1846
|
+
if (fade === void 0) return lines;
|
|
1847
|
+
const age = fade.age();
|
|
1848
|
+
return age === void 0 ? lines : recolorLines(lines, age, fade.style(), from);
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* The tail chunks one streaming region still draws below the terminal's own
|
|
1852
|
+
* foreground.
|
|
1853
|
+
*
|
|
1854
|
+
* The last ramp level is an assumed foreground - pi-tui reports the terminal
|
|
1855
|
+
* background but not its foreground - so a chunk that reached it is left to
|
|
1856
|
+
* draw in the terminal's own foreground, which is also what it draws in once it
|
|
1857
|
+
* leaves the tail. No chunk can therefore jump color as it settles.
|
|
1858
|
+
* @param fade - the region's fade; absent for a region that never faded.
|
|
1859
|
+
* @returns the spans to recolor, oldest first.
|
|
1860
|
+
*/
|
|
1861
|
+
function drawnSpans(fade) {
|
|
1862
|
+
return fade === void 0 ? [] : fade.spans().filter((span) => span.age < fade.steps - 1);
|
|
1863
|
+
}
|
|
1455
1864
|
//#endregion
|
|
1456
1865
|
//#region lib/types/completion.js
|
|
1457
1866
|
/**
|
|
@@ -1685,6 +2094,267 @@ async function exportSessionZip(ctx, sessionId, directory, signal) {
|
|
|
1685
2094
|
return path;
|
|
1686
2095
|
}
|
|
1687
2096
|
//#endregion
|
|
2097
|
+
//#region lib/types/inspector.js
|
|
2098
|
+
/**
|
|
2099
|
+
* The docked inspector above the editor: while the keyboard walks the
|
|
2100
|
+
* transcript it shows the focused section in full - what it is, which part of
|
|
2101
|
+
* its block is held, and the start of that part's own rows.
|
|
2102
|
+
*
|
|
2103
|
+
* It is the focus indicator that is always on screen. A block is also marked
|
|
2104
|
+
* in place, but only while the renderer can still repaint its first line
|
|
2105
|
+
* (`./navigation.ts`), so the heading says when the marked block itself has
|
|
2106
|
+
* scrolled out of reach. Rows arrive as the block's source text with no fade
|
|
2107
|
+
* and no palette styling of their own, so a section that is still streaming
|
|
2108
|
+
* grows here as it arrives.
|
|
2109
|
+
* @module @deepseek-ai/dsh-tui-app/inspector
|
|
2110
|
+
*/
|
|
2111
|
+
/** What separates two labels of the parts strip. */
|
|
2112
|
+
const SEPARATOR$3 = " · ";
|
|
2113
|
+
/** Brackets the parts strip, so the strip reads as one row of choices. */
|
|
2114
|
+
const STRIP_ENDS = ["‹ ", " ›"];
|
|
2115
|
+
/** What the heading appends while the focused block lies outside the repaint window. */
|
|
2116
|
+
const OFF_SCREEN = " · off screen";
|
|
2117
|
+
/** The keys the inspector answers, drawn dim under its rows. */
|
|
2118
|
+
const HINT = "↑ ↓ blocks · ← → parts · Enter page · Esc back";
|
|
2119
|
+
/**
|
|
2120
|
+
* Draw the focused section.
|
|
2121
|
+
* @param view - the section to show.
|
|
2122
|
+
* @param render - the palette, the row budget, and the width.
|
|
2123
|
+
* @returns the inspector's lines: a blank separator, the heading, the parts
|
|
2124
|
+
* strip when the block has more than one part, the folded rows, and the hints.
|
|
2125
|
+
*/
|
|
2126
|
+
function renderInspector(view, render) {
|
|
2127
|
+
const { palette, width } = render;
|
|
2128
|
+
const suffix = view.highlighted ? "" : palette.dim(OFF_SCREEN);
|
|
2129
|
+
const lines = ["", `${palette.bold(palette.accent(view.heading))}${suffix}`];
|
|
2130
|
+
if (view.parts.length > 1) lines.push(partsStrip(view.parts, palette));
|
|
2131
|
+
const wrapped = view.rows.flatMap((row) => wrapTextWithAnsi(row, Math.max(1, width)));
|
|
2132
|
+
lines.push(...foldRows(wrapped, render.previewLines, (hidden) => palette.dim(`… ${String(hidden)} more row${hidden === 1 ? "" : "s"} · Enter opens the page`)));
|
|
2133
|
+
lines.push(palette.dim(HINT));
|
|
2134
|
+
return lines;
|
|
2135
|
+
}
|
|
2136
|
+
/**
|
|
2137
|
+
* Draw the parts of the focused block as one row of choices.
|
|
2138
|
+
* @param parts - the labels in reading order, the held one marked.
|
|
2139
|
+
* @param palette - the palette the labels are styled with.
|
|
2140
|
+
* @returns the strip line.
|
|
2141
|
+
*/
|
|
2142
|
+
function partsStrip(parts, palette) {
|
|
2143
|
+
const labels = parts.map((part) => part.focused ? palette.accent(part.label) : palette.dim(part.label)).join(palette.dim(SEPARATOR$3));
|
|
2144
|
+
return `${palette.dim(STRIP_ENDS[0])}${labels}${palette.dim(STRIP_ENDS[1])}`;
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* The inspector as a mounted component.
|
|
2148
|
+
*
|
|
2149
|
+
* The view is read once per render rather than pushed in, so a section that is
|
|
2150
|
+
* still streaming and a tool result that has just landed reach the screen with
|
|
2151
|
+
* the frame that draws them, without the application refreshing the pane.
|
|
2152
|
+
*/
|
|
2153
|
+
var InspectorPane = class {
|
|
2154
|
+
view;
|
|
2155
|
+
settings;
|
|
2156
|
+
/**
|
|
2157
|
+
* @param view - reads the section to draw; undefined draws nothing at all,
|
|
2158
|
+
* which is what an unmounted moment and a transcript with no cursor yield.
|
|
2159
|
+
* @param settings - the palette and the row budget; the width arrives per render.
|
|
2160
|
+
*/
|
|
2161
|
+
constructor(view, settings) {
|
|
2162
|
+
this.view = view;
|
|
2163
|
+
this.settings = settings;
|
|
2164
|
+
}
|
|
2165
|
+
invalidate() {}
|
|
2166
|
+
/**
|
|
2167
|
+
* Draw the focused section at `width`.
|
|
2168
|
+
* @param width - the total width the pane lays out in.
|
|
2169
|
+
* @returns the inspector's lines, or none when no section is focused.
|
|
2170
|
+
*/
|
|
2171
|
+
render(width) {
|
|
2172
|
+
const view = this.view();
|
|
2173
|
+
return view === void 0 ? [] : renderInspector(view, {
|
|
2174
|
+
...this.settings,
|
|
2175
|
+
width
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
};
|
|
2179
|
+
//#endregion
|
|
2180
|
+
//#region lib/types/navigation.js
|
|
2181
|
+
/**
|
|
2182
|
+
* The transcript as a list of navigable sections.
|
|
2183
|
+
*
|
|
2184
|
+
* A conversation is drawn as one component per fact, and the keyboard walks it
|
|
2185
|
+
* in two directions: between blocks and, inside a block, between its parts -
|
|
2186
|
+
* the reasoning and the reply of a message, the call and the result of a tool.
|
|
2187
|
+
* This module turns the transcript container's children into that list and
|
|
2188
|
+
* moves one cursor over it. Everything here is plain data: no pi-tui, no
|
|
2189
|
+
* palette, no clock. The rows a part carries are the block's own source text,
|
|
2190
|
+
* so the page `Enter` opens shows what the model wrote rather than a rendering
|
|
2191
|
+
* of it.
|
|
2192
|
+
* @module @deepseek-ai/dsh-tui-app/navigation
|
|
2193
|
+
*/
|
|
2194
|
+
/** What separates two facts inside one heading. */
|
|
2195
|
+
const SEPARATOR$2 = " · ";
|
|
2196
|
+
/**
|
|
2197
|
+
* Whether one transcript child is navigable.
|
|
2198
|
+
* @param child - a child of the transcript container.
|
|
2199
|
+
* @returns true when the child carries the navigable marker.
|
|
2200
|
+
*/
|
|
2201
|
+
function isSectionSource(child) {
|
|
2202
|
+
return typeof child === "object" && child !== null && child.navigable === true;
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* The navigable blocks of a transcript, in drawing order.
|
|
2206
|
+
* @param children - the transcript container's children.
|
|
2207
|
+
* @returns the children that expose sections; notices and printed rows are left out.
|
|
2208
|
+
*/
|
|
2209
|
+
function navigableBlocks(children) {
|
|
2210
|
+
return children.filter(isSectionSource);
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* Hold one index inside a list.
|
|
2214
|
+
* @param index - the wanted index.
|
|
2215
|
+
* @param length - the list length; at least 1.
|
|
2216
|
+
* @returns the index, moved to the nearest end when it falls outside.
|
|
2217
|
+
*/
|
|
2218
|
+
function clampIndex(index, length) {
|
|
2219
|
+
return Math.max(0, Math.min(index, length - 1));
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* How many parts one block carries.
|
|
2223
|
+
* @param blocks - the navigable blocks.
|
|
2224
|
+
* @param index - the block to count.
|
|
2225
|
+
* @returns the count, and 0 for an index past the list.
|
|
2226
|
+
*/
|
|
2227
|
+
function partCount(blocks, index) {
|
|
2228
|
+
return blocks[index]?.parts().length ?? 0;
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Where the keyboard enters the transcript: the newest block, on its last
|
|
2232
|
+
* part, which is the most recent thing the session produced.
|
|
2233
|
+
* @param blocks - the navigable blocks.
|
|
2234
|
+
* @returns the cursor, or undefined when the transcript has nothing to inspect.
|
|
2235
|
+
*/
|
|
2236
|
+
function enterNewest(blocks) {
|
|
2237
|
+
const block = blocks.length - 1;
|
|
2238
|
+
const parts = partCount(blocks, block);
|
|
2239
|
+
if (parts === 0) return void 0;
|
|
2240
|
+
return {
|
|
2241
|
+
block,
|
|
2242
|
+
part: parts - 1
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
/**
|
|
2246
|
+
* Move to another block, landing on its last part.
|
|
2247
|
+
* @param cursor - where the focus sits.
|
|
2248
|
+
* @param step - 1 for the next block, -1 for the previous one.
|
|
2249
|
+
* @param blocks - the navigable blocks.
|
|
2250
|
+
* @returns the new cursor; the cursor itself at either end of the list.
|
|
2251
|
+
*/
|
|
2252
|
+
function moveBlock(cursor, step, blocks) {
|
|
2253
|
+
const block = clampIndex(cursor.block + step, blocks.length);
|
|
2254
|
+
if (block === cursor.block) return cursor;
|
|
2255
|
+
return {
|
|
2256
|
+
block,
|
|
2257
|
+
part: Math.max(0, partCount(blocks, block) - 1)
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Move between the parts of the held block.
|
|
2262
|
+
* @param cursor - where the focus sits.
|
|
2263
|
+
* @param step - 1 for the next part, -1 for the previous one.
|
|
2264
|
+
* @param blocks - the navigable blocks.
|
|
2265
|
+
* @returns the new cursor; the cursor itself at either end of the block.
|
|
2266
|
+
*/
|
|
2267
|
+
function movePart(cursor, step, blocks) {
|
|
2268
|
+
const parts = partCount(blocks, cursor.block);
|
|
2269
|
+
if (parts === 0) return cursor;
|
|
2270
|
+
return {
|
|
2271
|
+
block: cursor.block,
|
|
2272
|
+
part: clampIndex(cursor.part + step, parts)
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
/**
|
|
2276
|
+
* Put a remembered cursor back on the current transcript, which grew a block,
|
|
2277
|
+
* grew a part, or was replaced by another session since it was taken.
|
|
2278
|
+
* @param cursor - the remembered cursor.
|
|
2279
|
+
* @param blocks - the navigable blocks as they are now.
|
|
2280
|
+
* @returns the cursor inside the current list, or undefined when the
|
|
2281
|
+
* transcript has nothing to hold it.
|
|
2282
|
+
*/
|
|
2283
|
+
function clampCursor(cursor, blocks) {
|
|
2284
|
+
if (blocks.length === 0) return void 0;
|
|
2285
|
+
const block = clampIndex(cursor.block, blocks.length);
|
|
2286
|
+
const parts = partCount(blocks, block);
|
|
2287
|
+
if (parts === 0) return void 0;
|
|
2288
|
+
return {
|
|
2289
|
+
block,
|
|
2290
|
+
part: clampIndex(cursor.part, parts)
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
/**
|
|
2294
|
+
* The part the cursor holds.
|
|
2295
|
+
* @param cursor - where the focus sits.
|
|
2296
|
+
* @param blocks - the navigable blocks.
|
|
2297
|
+
* @returns the part, or undefined when the cursor points past the list.
|
|
2298
|
+
*/
|
|
2299
|
+
function partAt(cursor, blocks) {
|
|
2300
|
+
return blocks[cursor.block]?.parts()[cursor.part];
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* What the inspector and the parts strip call one section.
|
|
2304
|
+
* @param kind - the section.
|
|
2305
|
+
* @returns the label; the user's own prompt reads as `you`.
|
|
2306
|
+
*/
|
|
2307
|
+
function partLabel(kind) {
|
|
2308
|
+
return kind === "user" ? "you" : kind;
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* How a block names the held section after its position and turn.
|
|
2312
|
+
* @param block - the held block.
|
|
2313
|
+
* @param part - the held part.
|
|
2314
|
+
* @returns the naming facts, in heading order.
|
|
2315
|
+
*/
|
|
2316
|
+
function subjectOf(block, part) {
|
|
2317
|
+
switch (block.blockKind) {
|
|
2318
|
+
case "user":
|
|
2319
|
+
case "assistant": return [partLabel(part.kind)];
|
|
2320
|
+
case "tool": return [block.title === "" ? block.name : `${block.name} ${block.title}`, block.parts().some((candidate) => candidate.kind === "result") ? partLabel(part.kind) : "running"];
|
|
2321
|
+
/* v8 ignore next -- closed-union exhaustiveness guard */
|
|
2322
|
+
default: return assertNever(block, "tui transcript block kind");
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
/**
|
|
2326
|
+
* The inspector heading of the held section: its position in the transcript,
|
|
2327
|
+
* the turn it belongs to, and what the section is.
|
|
2328
|
+
* @param cursor - where the focus sits.
|
|
2329
|
+
* @param blocks - the navigable blocks.
|
|
2330
|
+
* @returns the heading, or an empty string when the cursor points past the list.
|
|
2331
|
+
*/
|
|
2332
|
+
function sectionHeading(cursor, blocks) {
|
|
2333
|
+
const block = blocks[cursor.block];
|
|
2334
|
+
const part = partAt(cursor, blocks);
|
|
2335
|
+
if (block === void 0 || part === void 0) return "";
|
|
2336
|
+
return [
|
|
2337
|
+
`${String(cursor.block + 1)}/${String(blocks.length)}`,
|
|
2338
|
+
`turn ${String(block.turn)}`,
|
|
2339
|
+
...subjectOf(block, part)
|
|
2340
|
+
].join(SEPARATOR$2);
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* The parts strip of the held block.
|
|
2344
|
+
* @param cursor - where the focus sits.
|
|
2345
|
+
* @param blocks - the navigable blocks.
|
|
2346
|
+
* @returns one label per part in reading order, the held one marked; empty
|
|
2347
|
+
* when the cursor points past the list.
|
|
2348
|
+
*/
|
|
2349
|
+
function partLabels(cursor, blocks) {
|
|
2350
|
+
const block = blocks[cursor.block];
|
|
2351
|
+
if (block === void 0) return [];
|
|
2352
|
+
return block.parts().map((part, index) => ({
|
|
2353
|
+
label: partLabel(part.kind),
|
|
2354
|
+
focused: index === cursor.part
|
|
2355
|
+
}));
|
|
2356
|
+
}
|
|
2357
|
+
//#endregion
|
|
1688
2358
|
//#region lib/types/todos.js
|
|
1689
2359
|
/**
|
|
1690
2360
|
* The agent's todo list as terminal rows: the status glyphs every todo surface
|
|
@@ -2088,9 +2758,9 @@ const WORKSPACE_LABEL_WIDTH = 24;
|
|
|
2088
2758
|
/** The last two segments of a path, with the separators that precede them. */
|
|
2089
2759
|
const PATH_TAIL = /[/\\][^/\\]+[/\\][^/\\]+$/u;
|
|
2090
2760
|
/** The keys the focused bar answers, replacing the usual hints. */
|
|
2091
|
-
const FOCUS_HINTS$1 = `← → select${SEPARATOR$1}Enter details${SEPARATOR$1}Esc back`;
|
|
2092
|
-
/** What the unfocused hints advertise as the
|
|
2093
|
-
const ENTRY_HINT =
|
|
2761
|
+
const FOCUS_HINTS$1 = `← → select${SEPARATOR$1}↑ ↓ regions${SEPARATOR$1}Enter details${SEPARATOR$1}Esc back`;
|
|
2762
|
+
/** What the unfocused hints advertise as the two ways out of the editor. */
|
|
2763
|
+
const ENTRY_HINT = `Shift+↑ transcript${SEPARATOR$1}Shift+↓ status bar`;
|
|
2094
2764
|
/** The command that prints every projection section at once. */
|
|
2095
2765
|
const STATUS_COMMAND_ROW = "/status prints all of these sections";
|
|
2096
2766
|
/**
|
|
@@ -2251,15 +2921,12 @@ const SELECT_MAX_VISIBLE = 8;
|
|
|
2251
2921
|
/** Call-detail rows an approval shows before folding the rest into a count. */
|
|
2252
2922
|
const APPROVAL_DETAIL_MAX_ROWS = 12;
|
|
2253
2923
|
/**
|
|
2254
|
-
*
|
|
2255
|
-
* @param
|
|
2256
|
-
* @
|
|
2257
|
-
* @returns the rows to draw, with a trailing count of hidden rows when cut.
|
|
2924
|
+
* The trailing row an approval's folded call detail carries.
|
|
2925
|
+
* @param hidden - rows left out.
|
|
2926
|
+
* @returns the marker row.
|
|
2258
2927
|
*/
|
|
2259
|
-
function
|
|
2260
|
-
|
|
2261
|
-
const hidden = lines.length - max;
|
|
2262
|
-
return [...lines.slice(0, max), `… ${String(hidden)} more line${hidden === 1 ? "" : "s"}`];
|
|
2928
|
+
function approvalFoldMarker(hidden) {
|
|
2929
|
+
return `… ${String(hidden)} more line${hidden === 1 ? "" : "s"}`;
|
|
2263
2930
|
}
|
|
2264
2931
|
/** One settlement slot: the first value wins, later values are ignored. */
|
|
2265
2932
|
var Settlement = class {
|
|
@@ -2378,7 +3045,7 @@ var ListPrompt = class {
|
|
|
2378
3045
|
var ApprovalPrompt = class extends ListPrompt {
|
|
2379
3046
|
constructor(palette, toolName, reason, detail = []) {
|
|
2380
3047
|
const title = `${palette.warning("?")} ${palette.bold(`Allow ${toolName}?`)}`;
|
|
2381
|
-
super(palette, reason === void 0 ? title : `${title}\n${palette.dim(reason)}`, foldRows(detail, APPROVAL_DETAIL_MAX_ROWS), [{
|
|
3048
|
+
super(palette, reason === void 0 ? title : `${title}\n${palette.dim(reason)}`, foldRows(detail, APPROVAL_DETAIL_MAX_ROWS, approvalFoldMarker), [{
|
|
2382
3049
|
value: "allowed-once",
|
|
2383
3050
|
label: "Allow once",
|
|
2384
3051
|
description: "run this call"
|
|
@@ -2773,6 +3440,89 @@ var ModalQueue = class {
|
|
|
2773
3440
|
}
|
|
2774
3441
|
};
|
|
2775
3442
|
//#endregion
|
|
3443
|
+
//#region lib/types/screen.js
|
|
3444
|
+
/**
|
|
3445
|
+
* The main screen this application renders through, and the repaint rule the
|
|
3446
|
+
* renderer imposes on everything drawn above the terminal's viewport.
|
|
3447
|
+
*
|
|
3448
|
+
* pi-tui's `TuiMainScreen` draws into the terminal's own scrollback. It
|
|
3449
|
+
* repaints differentially only the lines whose index is at or after
|
|
3450
|
+
* `previousViewportTop`, the top of the last written frame's viewport, and
|
|
3451
|
+
* falls back to a full redraw that writes `ESC[2J ESC[H ESC[3J` - which
|
|
3452
|
+
* discards the terminal's scrollback - as soon as a line above it changed.
|
|
3453
|
+
* `previousViewportTop` is a high-water mark: every frame raises it towards
|
|
3454
|
+
* `frameLines - terminal.rows` and a shorter frame never lowers it again, so a
|
|
3455
|
+
* line that once left the window stays out of reach until the renderer redraws
|
|
3456
|
+
* in full and resets it.
|
|
3457
|
+
*
|
|
3458
|
+
* Anything that redraws lines a component already produced - the streaming
|
|
3459
|
+
* fade, a card fade, the focus gutter - therefore asks {@link repaintFloor} how
|
|
3460
|
+
* far into a block the renderer can still follow it, and
|
|
3461
|
+
* {@link GuardedMainScreen} gives the application the one moment where that
|
|
3462
|
+
* question can be answered: after a frame was built and before it is written.
|
|
3463
|
+
* @module @deepseek-ai/dsh-tui-app/screen
|
|
3464
|
+
*/
|
|
3465
|
+
/**
|
|
3466
|
+
* How many times one frame is offered to the guard before it is written. Two
|
|
3467
|
+
* passes let the guard answer for the geometry its own first pass produced -
|
|
3468
|
+
* a gutter rewraps the block it marks - and end on a frame the application's
|
|
3469
|
+
* own record of what is marked describes.
|
|
3470
|
+
*/
|
|
3471
|
+
const SETTLE_PASSES = 2;
|
|
3472
|
+
/**
|
|
3473
|
+
* The first line of one block the renderer can still repaint.
|
|
3474
|
+
* @param start - index of the block's first line in the frame.
|
|
3475
|
+
* @param viewportTop - the frame's first repaintable line, as
|
|
3476
|
+
* {@link GuardedMainScreen} hands it to its guard.
|
|
3477
|
+
* @returns the block's own first repaintable line index, 0 while the whole
|
|
3478
|
+
* block lies inside the window.
|
|
3479
|
+
*/
|
|
3480
|
+
function repaintFloor(start, viewportTop) {
|
|
3481
|
+
return Math.max(0, viewportTop - start);
|
|
3482
|
+
}
|
|
3483
|
+
/**
|
|
3484
|
+
* The main screen that settles each frame between building it and writing it.
|
|
3485
|
+
*
|
|
3486
|
+
* The guard is handed the frame's first repaintable line: the higher of the
|
|
3487
|
+
* renderer's own `previousViewportTop` and the boundary the frame just built
|
|
3488
|
+
* imposes once it is written. The first term is what the renderer judges this
|
|
3489
|
+
* write against. The second keeps every decision one frame ahead of that
|
|
3490
|
+
* judgement, so what the guard changes today it can still change back
|
|
3491
|
+
* tomorrow. Settling before the write also means a fade reaches the terminal
|
|
3492
|
+
* already settled instead of changing in the next frame, by which time its
|
|
3493
|
+
* lines may sit above the window.
|
|
3494
|
+
*/
|
|
3495
|
+
var GuardedMainScreen = class extends TuiMainScreen {
|
|
3496
|
+
guard;
|
|
3497
|
+
/**
|
|
3498
|
+
* @param terminal - the terminal the tree renders into.
|
|
3499
|
+
* @param showHardwareCursor - whether the terminal's own cursor is the caret.
|
|
3500
|
+
* @param guard - given the frame's first repaintable line and the width it
|
|
3501
|
+
* was built at, applies what that geometry decides and reports whether it
|
|
3502
|
+
* changed any line; run at most {@link SETTLE_PASSES} times per frame, each
|
|
3503
|
+
* `true` on a frame that is built again.
|
|
3504
|
+
*/
|
|
3505
|
+
constructor(terminal, showHardwareCursor, guard) {
|
|
3506
|
+
super(terminal, showHardwareCursor);
|
|
3507
|
+
this.guard = guard;
|
|
3508
|
+
}
|
|
3509
|
+
/**
|
|
3510
|
+
* Build the frame, let the guard settle what its geometry decides, and build
|
|
3511
|
+
* it again for every pass that changed a line.
|
|
3512
|
+
* @param width - the total width the tree lays out in.
|
|
3513
|
+
* @returns the frame to write, settled against its own geometry.
|
|
3514
|
+
*/
|
|
3515
|
+
render(width) {
|
|
3516
|
+
const previousTop = this.captureRenderState().previousViewportTop;
|
|
3517
|
+
let lines = super.render(width);
|
|
3518
|
+
for (let pass = 0; pass < SETTLE_PASSES; pass += 1) {
|
|
3519
|
+
if (!this.guard(Math.max(previousTop, lines.length - this.terminal.rows), width)) break;
|
|
3520
|
+
lines = super.render(width);
|
|
3521
|
+
}
|
|
3522
|
+
return lines;
|
|
3523
|
+
}
|
|
3524
|
+
};
|
|
3525
|
+
//#endregion
|
|
2776
3526
|
//#region lib/types/sessions.js
|
|
2777
3527
|
/**
|
|
2778
3528
|
* Session facts the terminal shows and switches between: the persisted
|
|
@@ -2908,7 +3658,10 @@ function renderSubagentPanel(view, render) {
|
|
|
2908
3658
|
* the editor it keeps two docked regions the keyboard can take over — the
|
|
2909
3659
|
* subagent panel and the status bar — and one repeating tick advances their
|
|
2910
3660
|
* elapsed counters and re-reads a stale subagent listing. A second tick, at
|
|
2911
|
-
* its own period, brightens the text of the message streaming right now
|
|
3661
|
+
* its own period, brightens the text of the message streaming right now and
|
|
3662
|
+
* the tool cards that just landed, each only as far up the frame as the
|
|
3663
|
+
* renderer repaints without discarding the terminal's scrollback
|
|
3664
|
+
* (`./screen.ts`).
|
|
2912
3665
|
* @module @deepseek-ai/dsh-tui-app/app
|
|
2913
3666
|
*/
|
|
2914
3667
|
/** A second Ctrl+C inside this window quits. */
|
|
@@ -3086,6 +3839,8 @@ var TuiApp = class {
|
|
|
3086
3839
|
statusSlot = new Container();
|
|
3087
3840
|
loader;
|
|
3088
3841
|
modalSlot = new Container();
|
|
3842
|
+
/** Draws the focused transcript section, and nothing at all while the keyboard is elsewhere. */
|
|
3843
|
+
inspector;
|
|
3089
3844
|
editor;
|
|
3090
3845
|
/** Holds {@link panel} exactly while the bound session has subagent rows. */
|
|
3091
3846
|
panelSlot = new Container();
|
|
@@ -3108,8 +3863,16 @@ var TuiApp = class {
|
|
|
3108
3863
|
home = homedir();
|
|
3109
3864
|
/** The segments of the last footer draw, in bar order. */
|
|
3110
3865
|
segments = [];
|
|
3111
|
-
/** Which
|
|
3866
|
+
/** Which region owns the keyboard. */
|
|
3112
3867
|
focus = "editor";
|
|
3868
|
+
/**
|
|
3869
|
+
* Where the transcript focus sits, kept across a page and a return to the
|
|
3870
|
+
* editor. A cursor the current transcript cannot place names nothing, which
|
|
3871
|
+
* is what a session change leaves behind.
|
|
3872
|
+
*/
|
|
3873
|
+
cursor;
|
|
3874
|
+
/** The section drawn with the focus gutter right now; set only by {@link TuiApp.settleFrame}. */
|
|
3875
|
+
highlighted;
|
|
3113
3876
|
/** The segment the status bar holds; read only while the bar has focus. */
|
|
3114
3877
|
barSelection = FIRST_FOOTER_SEGMENT;
|
|
3115
3878
|
/** The descendant listing the last reconcile produced, in pre-order. */
|
|
@@ -3128,8 +3891,14 @@ var TuiApp = class {
|
|
|
3128
3891
|
ticker;
|
|
3129
3892
|
/** Disposer of the fade tick while it is armed. */
|
|
3130
3893
|
fadeTicker;
|
|
3131
|
-
/** The tail of the message streaming right now; absent between messages. */
|
|
3132
|
-
|
|
3894
|
+
/** The visible-text tail of the message streaming right now; absent between messages. */
|
|
3895
|
+
textTail;
|
|
3896
|
+
/** The reasoning tail of the message streaming right now; absent between messages. */
|
|
3897
|
+
reasoningTail;
|
|
3898
|
+
/** The card fades running right now; each drops itself once it settles. */
|
|
3899
|
+
blockFades = new FadeRegistry();
|
|
3900
|
+
/** Set while {@link TuiApp.bind} replays a session's history, which draws its cards settled. */
|
|
3901
|
+
replaying = false;
|
|
3133
3902
|
/** Whether this terminal draws a ramp at all, decided once at start. */
|
|
3134
3903
|
fading = false;
|
|
3135
3904
|
/** How streamed text is drawn; `none` until the background query settles. */
|
|
@@ -3154,8 +3923,12 @@ var TuiApp = class {
|
|
|
3154
3923
|
palette,
|
|
3155
3924
|
toolPreviewLines: deps.toolPreviewLines
|
|
3156
3925
|
};
|
|
3157
|
-
this.tui = new
|
|
3926
|
+
this.tui = new GuardedMainScreen(deps.terminal, true, (viewportTop, width) => this.settleFrame(viewportTop, width));
|
|
3158
3927
|
this.header = new Text("", 0, 0);
|
|
3928
|
+
this.inspector = new InspectorPane(() => this.inspectorView(), {
|
|
3929
|
+
palette,
|
|
3930
|
+
previewLines: deps.focusPreviewLines
|
|
3931
|
+
});
|
|
3159
3932
|
this.loader = new Loader(this.tui, palette.accent, palette.dim, "thinking");
|
|
3160
3933
|
this.loader.stop();
|
|
3161
3934
|
this.editor = new BarCursorEditor(this.tui, editorTheme(palette), { paddingX: 1 });
|
|
@@ -3173,15 +3946,17 @@ var TuiApp = class {
|
|
|
3173
3946
|
slot: this.modalSlot,
|
|
3174
3947
|
focusAfter: this.editor
|
|
3175
3948
|
});
|
|
3176
|
-
|
|
3949
|
+
const tree = [
|
|
3177
3950
|
this.header,
|
|
3178
3951
|
this.chat,
|
|
3179
3952
|
this.statusSlot,
|
|
3180
3953
|
this.modalSlot,
|
|
3954
|
+
this.inspector,
|
|
3181
3955
|
this.editor,
|
|
3182
3956
|
this.panelSlot,
|
|
3183
3957
|
this.footer
|
|
3184
|
-
]
|
|
3958
|
+
];
|
|
3959
|
+
for (const child of tree) this.tui.addChild(child);
|
|
3185
3960
|
}
|
|
3186
3961
|
get agent() {
|
|
3187
3962
|
return this.bound.agent;
|
|
@@ -3229,10 +4004,11 @@ var TuiApp = class {
|
|
|
3229
4004
|
if (initialPrompt !== void 0) this.submit(initialPrompt);
|
|
3230
4005
|
}
|
|
3231
4006
|
/**
|
|
3232
|
-
* Decide whether streamed text
|
|
3233
|
-
* palette, the environment, and the reduced-motion preference. A
|
|
3234
|
-
* that draws no ramp tracks no tail
|
|
3235
|
-
* costs there exactly what it
|
|
4007
|
+
* Decide whether streamed text and tool cards fade at all, once per run,
|
|
4008
|
+
* from the palette, the environment, and the reduced-motion preference. A
|
|
4009
|
+
* terminal that draws no ramp tracks no tail, attaches no card fade, and
|
|
4010
|
+
* arms no fade tick, so streaming costs there exactly what it does with the
|
|
4011
|
+
* effect switched off.
|
|
3236
4012
|
*/
|
|
3237
4013
|
startFade() {
|
|
3238
4014
|
const capability = resolveFadeCapability({
|
|
@@ -3280,6 +4056,8 @@ var TuiApp = class {
|
|
|
3280
4056
|
/** Draw `next` as the terminal's session: clear the transcript and replay its history. */
|
|
3281
4057
|
bind(next) {
|
|
3282
4058
|
this.bound = next;
|
|
4059
|
+
this.focusEditor();
|
|
4060
|
+
this.highlighted = void 0;
|
|
3283
4061
|
this.chat.clear();
|
|
3284
4062
|
this.toolBlocks.clear();
|
|
3285
4063
|
this.toolArguments.clear();
|
|
@@ -3289,6 +4067,7 @@ var TuiApp = class {
|
|
|
3289
4067
|
this.turnStartedAt = void 0;
|
|
3290
4068
|
this.streaming = void 0;
|
|
3291
4069
|
this.endFade();
|
|
4070
|
+
this.blockFades.clear();
|
|
3292
4071
|
this.usage = EMPTY_USAGE;
|
|
3293
4072
|
this.pending = [];
|
|
3294
4073
|
this.subagentEntries = [];
|
|
@@ -3296,7 +4075,9 @@ var TuiApp = class {
|
|
|
3296
4075
|
this.listingFailure = void 0;
|
|
3297
4076
|
this.subagentsStale = false;
|
|
3298
4077
|
this.setWorking(next.agent.status === "running");
|
|
4078
|
+
this.replaying = true;
|
|
3299
4079
|
for (const event of next.history) this.onSessionEvent(next.agent.session, event);
|
|
4080
|
+
this.replaying = false;
|
|
3300
4081
|
this.refreshHeader();
|
|
3301
4082
|
this.refreshFooter();
|
|
3302
4083
|
this.refreshSubagentPanel();
|
|
@@ -3778,12 +4559,13 @@ var TuiApp = class {
|
|
|
3778
4559
|
}
|
|
3779
4560
|
if (this.focus === "bar") return this.onStatusBarKey(data);
|
|
3780
4561
|
if (this.focus === "panel") return this.onPanelKey(data);
|
|
4562
|
+
if (this.focus === "transcript") return this.onTranscriptKey(data);
|
|
3781
4563
|
if (matchesKey(data, "shift+up")) {
|
|
3782
|
-
this.
|
|
4564
|
+
this.focusTranscript();
|
|
3783
4565
|
return { consume: true };
|
|
3784
4566
|
}
|
|
3785
|
-
if (matchesKey(data, "shift+down")
|
|
3786
|
-
this.
|
|
4567
|
+
if (matchesKey(data, "shift+down")) {
|
|
4568
|
+
this.focusBelowEditor();
|
|
3787
4569
|
return { consume: true };
|
|
3788
4570
|
}
|
|
3789
4571
|
if (matchesKey(data, "escape") && !this.editor.isShowingAutocomplete()) {
|
|
@@ -3812,18 +4594,38 @@ var TuiApp = class {
|
|
|
3812
4594
|
}
|
|
3813
4595
|
}
|
|
3814
4596
|
/**
|
|
3815
|
-
* Answer
|
|
3816
|
-
*
|
|
4597
|
+
* Answer the keys every non-editor region answers the same way: `Escape`
|
|
4598
|
+
* hands the keyboard back to the editor, `Shift+Up` names the conversation,
|
|
4599
|
+
* and `Shift+Down` the status bar.
|
|
3817
4600
|
* @param data - the raw key bytes.
|
|
3818
|
-
* @returns the consume marker the
|
|
4601
|
+
* @returns the consume marker when the key named a region, `undefined` when the focused region owns the key.
|
|
3819
4602
|
*/
|
|
3820
|
-
|
|
3821
|
-
if (matchesKey(data, "escape")
|
|
4603
|
+
onRegionKey(data) {
|
|
4604
|
+
if (matchesKey(data, "escape")) {
|
|
3822
4605
|
this.focusEditor();
|
|
3823
4606
|
return { consume: true };
|
|
3824
4607
|
}
|
|
3825
4608
|
if (matchesKey(data, "shift+up")) {
|
|
3826
|
-
this.
|
|
4609
|
+
this.focusTranscript();
|
|
4610
|
+
return { consume: true };
|
|
4611
|
+
}
|
|
4612
|
+
if (matchesKey(data, "shift+down")) {
|
|
4613
|
+
this.focusBar();
|
|
4614
|
+
return { consume: true };
|
|
4615
|
+
}
|
|
4616
|
+
}
|
|
4617
|
+
/**
|
|
4618
|
+
* Answer one key while the status bar holds focus. Every key is consumed
|
|
4619
|
+
* here, so nothing typed at the bar reaches the editor.
|
|
4620
|
+
* @param data - the raw key bytes.
|
|
4621
|
+
* @returns the consume marker the input listener returns.
|
|
4622
|
+
*/
|
|
4623
|
+
onStatusBarKey(data) {
|
|
4624
|
+
const region = this.onRegionKey(data);
|
|
4625
|
+
if (region !== void 0) return region;
|
|
4626
|
+
if (matchesKey(data, "up")) {
|
|
4627
|
+
if (this.panelView.rows.length > 0) this.focusPanel(this.panelView.rows.length - 1);
|
|
4628
|
+
else this.focusTranscript();
|
|
3827
4629
|
return { consume: true };
|
|
3828
4630
|
}
|
|
3829
4631
|
if (matchesKey(data, "left") || matchesKey(data, "shift+tab")) {
|
|
@@ -3838,27 +4640,60 @@ var TuiApp = class {
|
|
|
3838
4640
|
return { consume: true };
|
|
3839
4641
|
}
|
|
3840
4642
|
/**
|
|
3841
|
-
* Answer one key while the
|
|
3842
|
-
*
|
|
3843
|
-
* global meaning.
|
|
4643
|
+
* Answer one key while the transcript holds focus. Every key is consumed
|
|
4644
|
+
* here, so nothing typed while reading the conversation reaches the editor;
|
|
4645
|
+
* `Ctrl+C` and `Ctrl+D` never reach this far and keep their global meaning.
|
|
3844
4646
|
* @param data - the raw key bytes.
|
|
3845
4647
|
* @returns the consume marker the input listener returns.
|
|
3846
4648
|
*/
|
|
3847
|
-
|
|
3848
|
-
|
|
4649
|
+
onTranscriptKey(data) {
|
|
4650
|
+
const region = this.onRegionKey(data);
|
|
4651
|
+
if (region !== void 0) return region;
|
|
4652
|
+
const section = this.focusedSection();
|
|
4653
|
+
/* v8 ignore next 5 -- only a session change empties the transcript, and it hands the keyboard back first */
|
|
4654
|
+
if (section === void 0) {
|
|
3849
4655
|
this.focusEditor();
|
|
3850
4656
|
return { consume: true };
|
|
3851
4657
|
}
|
|
3852
|
-
|
|
3853
|
-
|
|
4658
|
+
const { cursor, blocks } = section;
|
|
4659
|
+
if (matchesKey(data, "up")) {
|
|
4660
|
+
this.moveCursor(moveBlock(cursor, -1, blocks));
|
|
4661
|
+
return { consume: true };
|
|
4662
|
+
}
|
|
4663
|
+
if (matchesKey(data, "down")) {
|
|
4664
|
+
if (cursor.block === blocks.length - 1) this.focusBelowEditor();
|
|
4665
|
+
else this.moveCursor(moveBlock(cursor, 1, blocks));
|
|
3854
4666
|
return { consume: true };
|
|
3855
4667
|
}
|
|
4668
|
+
if (matchesKey(data, "left")) {
|
|
4669
|
+
this.moveCursor(movePart(cursor, -1, blocks));
|
|
4670
|
+
return { consume: true };
|
|
4671
|
+
}
|
|
4672
|
+
if (matchesKey(data, "right")) {
|
|
4673
|
+
this.moveCursor(movePart(cursor, 1, blocks));
|
|
4674
|
+
return { consume: true };
|
|
4675
|
+
}
|
|
4676
|
+
if (matchesKey(data, "enter")) this.openSection(section);
|
|
4677
|
+
return { consume: true };
|
|
4678
|
+
}
|
|
4679
|
+
/**
|
|
4680
|
+
* Answer one key while the subagent panel holds focus. Every key is
|
|
4681
|
+
* consumed here; `Ctrl+C` and `Ctrl+D` never reach this far, keeping their
|
|
4682
|
+
* global meaning.
|
|
4683
|
+
* @param data - the raw key bytes.
|
|
4684
|
+
* @returns the consume marker the input listener returns.
|
|
4685
|
+
*/
|
|
4686
|
+
onPanelKey(data) {
|
|
4687
|
+
const region = this.onRegionKey(data);
|
|
4688
|
+
if (region !== void 0) return region;
|
|
3856
4689
|
if (matchesKey(data, "up")) {
|
|
3857
|
-
this.
|
|
4690
|
+
if (this.panelSelectionIndex(this.panelView.rows) === 0) this.focusTranscript();
|
|
4691
|
+
else this.movePanel(-1);
|
|
3858
4692
|
return { consume: true };
|
|
3859
4693
|
}
|
|
3860
4694
|
if (matchesKey(data, "down")) {
|
|
3861
|
-
this.
|
|
4695
|
+
if (this.panelSelectionIndex(this.panelView.rows) === this.panelView.rows.length - 1) this.focusBar();
|
|
4696
|
+
else this.movePanel(1);
|
|
3862
4697
|
return { consume: true };
|
|
3863
4698
|
}
|
|
3864
4699
|
if (matchesKey(data, "enter")) this.navigate("subagent details", () => this.openPanelRow());
|
|
@@ -3903,16 +4738,15 @@ var TuiApp = class {
|
|
|
3903
4738
|
this.refreshFooter();
|
|
3904
4739
|
}
|
|
3905
4740
|
/**
|
|
3906
|
-
* Move the panel's selection,
|
|
4741
|
+
* Move the panel's selection, stopping at both ends of the drawn rows. The
|
|
3907
4742
|
* rows behind a `+<n> more` row are not selectable; `/subagents` walks the
|
|
3908
4743
|
* complete tree.
|
|
3909
4744
|
* @param step - 1 for the next row, -1 for the previous one.
|
|
3910
4745
|
*/
|
|
3911
4746
|
movePanel(step) {
|
|
3912
4747
|
const rows = this.panelView.rows;
|
|
3913
|
-
const
|
|
3914
|
-
|
|
3915
|
-
/* v8 ignore next -- the wrapped index stays inside the panel's own rows */
|
|
4748
|
+
const next = rows[Math.max(0, Math.min(this.panelSelectionIndex(rows) + step, rows.length - 1))];
|
|
4749
|
+
/* v8 ignore next -- the clamped index stays inside the panel's own rows */
|
|
3916
4750
|
if (next !== void 0) this.panelSelection = next.id;
|
|
3917
4751
|
this.refreshSubagentPanel();
|
|
3918
4752
|
}
|
|
@@ -3921,13 +4755,103 @@ var TuiApp = class {
|
|
|
3921
4755
|
this.barSelection = FIRST_FOOTER_SEGMENT;
|
|
3922
4756
|
this.focusRegion("bar");
|
|
3923
4757
|
}
|
|
4758
|
+
/**
|
|
4759
|
+
* Give the keyboard to the subagent panel on one of its rows.
|
|
4760
|
+
* @param index - the row the selection lands on.
|
|
4761
|
+
*/
|
|
4762
|
+
focusPanel(index) {
|
|
4763
|
+
this.panelSelection = this.panelView.rows[index]?.id;
|
|
4764
|
+
this.focusRegion("panel");
|
|
4765
|
+
}
|
|
4766
|
+
/**
|
|
4767
|
+
* Give the keyboard to the region under the editor: the subagent panel's
|
|
4768
|
+
* first row while the panel is drawn, and the status bar otherwise.
|
|
4769
|
+
*/
|
|
4770
|
+
focusBelowEditor() {
|
|
4771
|
+
if (this.panelView.rows.length > 0) this.focusPanel(0);
|
|
4772
|
+
else this.focusBar();
|
|
4773
|
+
}
|
|
4774
|
+
/**
|
|
4775
|
+
* Give the keyboard to the transcript on its newest block. A session with
|
|
4776
|
+
* nothing to inspect yet says so and leaves the keyboard where it was.
|
|
4777
|
+
*/
|
|
4778
|
+
focusTranscript() {
|
|
4779
|
+
const cursor = enterNewest(navigableBlocks(this.chat.children));
|
|
4780
|
+
if (cursor === void 0) {
|
|
4781
|
+
this.notice("nothing in the transcript to inspect yet");
|
|
4782
|
+
return;
|
|
4783
|
+
}
|
|
4784
|
+
this.cursor = cursor;
|
|
4785
|
+
this.focusRegion("transcript");
|
|
4786
|
+
this.tui.requestRender();
|
|
4787
|
+
}
|
|
4788
|
+
/**
|
|
4789
|
+
* Put the transcript focus on another section.
|
|
4790
|
+
* @param cursor - where the focus moves to.
|
|
4791
|
+
*/
|
|
4792
|
+
moveCursor(cursor) {
|
|
4793
|
+
this.cursor = cursor;
|
|
4794
|
+
this.tui.requestRender();
|
|
4795
|
+
}
|
|
4796
|
+
/**
|
|
4797
|
+
* Read the remembered cursor against the transcript as it is drawn right
|
|
4798
|
+
* now: it grew parts and blocks since the cursor was taken, and a session
|
|
4799
|
+
* change replaced the blocks it named altogether. The cursor itself is left
|
|
4800
|
+
* alone, so a page and a trip through the editor come back to the same
|
|
4801
|
+
* section; {@link TuiApp.focusTranscript} replaces it.
|
|
4802
|
+
* @returns the section the cursor names, or undefined when the transcript
|
|
4803
|
+
* has nothing to hold it.
|
|
4804
|
+
*/
|
|
4805
|
+
focusedSection() {
|
|
4806
|
+
const blocks = navigableBlocks(this.chat.children);
|
|
4807
|
+
const cursor = this.cursor === void 0 ? void 0 : clampCursor(this.cursor, blocks);
|
|
4808
|
+
if (cursor === void 0) return void 0;
|
|
4809
|
+
const block = blocks[cursor.block];
|
|
4810
|
+
const part = block?.parts()[cursor.part];
|
|
4811
|
+
/* v8 ignore next -- clampCursor settles on a block that carries the part it names */
|
|
4812
|
+
if (block === void 0 || part === void 0) return void 0;
|
|
4813
|
+
return {
|
|
4814
|
+
cursor,
|
|
4815
|
+
blocks,
|
|
4816
|
+
block,
|
|
4817
|
+
part
|
|
4818
|
+
};
|
|
4819
|
+
}
|
|
4820
|
+
/**
|
|
4821
|
+
* Open the focused section as a read-only page and come back to it.
|
|
4822
|
+
* @param section - the focused section.
|
|
4823
|
+
*/
|
|
4824
|
+
openSection(section) {
|
|
4825
|
+
const { cursor, blocks } = section;
|
|
4826
|
+
const prompt = new DetailPrompt(this.deps.palette, sectionHeading(cursor, blocks), section.part.rows);
|
|
4827
|
+
this.navigate("section", async () => {
|
|
4828
|
+
await this.showModal(prompt);
|
|
4829
|
+
this.focusRegion("transcript");
|
|
4830
|
+
});
|
|
4831
|
+
}
|
|
4832
|
+
/**
|
|
4833
|
+
* The focused section as the docked inspector draws it.
|
|
4834
|
+
* @returns the view, or undefined while the keyboard is not in the
|
|
4835
|
+
* transcript, which draws no inspector at all.
|
|
4836
|
+
*/
|
|
4837
|
+
inspectorView() {
|
|
4838
|
+
const section = this.focusedSection();
|
|
4839
|
+
if (section === void 0 || this.focus !== "transcript") return void 0;
|
|
4840
|
+
const { cursor, blocks } = section;
|
|
4841
|
+
return {
|
|
4842
|
+
heading: sectionHeading(cursor, blocks),
|
|
4843
|
+
parts: partLabels(cursor, blocks),
|
|
4844
|
+
rows: section.part.rows,
|
|
4845
|
+
highlighted: this.highlighted !== void 0
|
|
4846
|
+
};
|
|
4847
|
+
}
|
|
3924
4848
|
/** Hand the keyboard back to the editor; a no-op while the editor already has it. */
|
|
3925
4849
|
focusEditor() {
|
|
3926
4850
|
this.focusRegion("editor");
|
|
3927
4851
|
}
|
|
3928
4852
|
/**
|
|
3929
|
-
* Move the keyboard between the
|
|
3930
|
-
*
|
|
4853
|
+
* Move the keyboard between the regions and redraw the docked ones, so the
|
|
4854
|
+
* region losing focus stops drawing its selection.
|
|
3931
4855
|
* @param region - the region that takes the keyboard.
|
|
3932
4856
|
*/
|
|
3933
4857
|
focusRegion(region) {
|
|
@@ -3986,7 +4910,7 @@ var TuiApp = class {
|
|
|
3986
4910
|
});
|
|
3987
4911
|
this.submittedIds.add(message.id);
|
|
3988
4912
|
const shown = attachments.length === 0 ? text : `${text}\n${attachments.map((attachment) => `[${attachment.block.type}: ${attachment.name}]`).join(" ")}`;
|
|
3989
|
-
this.chat.addChild(new UserBlock(this.theme, shown));
|
|
4913
|
+
this.chat.addChild(new UserBlock(this.theme, shown, this.turn));
|
|
3990
4914
|
if (agent.status !== "running") agent.followup(message);
|
|
3991
4915
|
else if (mode === "steer") {
|
|
3992
4916
|
agent.steer(message);
|
|
@@ -4093,8 +5017,9 @@ var TuiApp = class {
|
|
|
4093
5017
|
"@ completes paths and sessions (workspace, ../, ~/, absolute) · / completes commands",
|
|
4094
5018
|
"Esc stops the running turn · Ctrl+O expands or collapses tool output",
|
|
4095
5019
|
"Shift+Tab cycles the current model's reasoning effort for the next request",
|
|
4096
|
-
"Shift+Up focuses the
|
|
4097
|
-
"
|
|
5020
|
+
"Shift+Up focuses the transcript, Shift+Down the subagent panel or the status bar",
|
|
5021
|
+
"Then ↑ ↓ move between blocks, panel rows, and the bar; ← → move between a block's parts or the bar's segments",
|
|
5022
|
+
"Enter opens the focused section or segment, Esc returns to the input",
|
|
4098
5023
|
"Ctrl+C clears the input (twice quits) · Ctrl+D on an empty input quits"
|
|
4099
5024
|
];
|
|
4100
5025
|
this.chat.addChild(new Text([
|
|
@@ -4535,7 +5460,7 @@ var TuiApp = class {
|
|
|
4535
5460
|
}
|
|
4536
5461
|
streamingBlock() {
|
|
4537
5462
|
if (this.streaming === void 0) {
|
|
4538
|
-
this.streaming = new AssistantBlock(this.theme);
|
|
5463
|
+
this.streaming = new AssistantBlock(this.theme, this.turn);
|
|
4539
5464
|
this.chat.addChild(this.streaming);
|
|
4540
5465
|
}
|
|
4541
5466
|
return this.streaming;
|
|
@@ -4550,48 +5475,109 @@ var TuiApp = class {
|
|
|
4550
5475
|
const block = this.streamingBlock();
|
|
4551
5476
|
block.appendText(delta);
|
|
4552
5477
|
if (!this.fading) return;
|
|
4553
|
-
if (this.
|
|
4554
|
-
this.
|
|
4555
|
-
block.setFade(
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
5478
|
+
if (this.textTail === void 0) {
|
|
5479
|
+
this.textTail = this.createTail();
|
|
5480
|
+
block.setFade(this.tailRender(() => this.textTail));
|
|
5481
|
+
}
|
|
5482
|
+
this.textTail.append(delta);
|
|
5483
|
+
this.updateFadeTicker();
|
|
5484
|
+
}
|
|
5485
|
+
/**
|
|
5486
|
+
* Take one reasoning delta, which fades in on its own tail: the reasoning
|
|
5487
|
+
* and the visible text of one message stream at different times and each
|
|
5488
|
+
* brightens from the moment its own words appeared.
|
|
5489
|
+
* @param delta - the streamed reasoning delta.
|
|
5490
|
+
*/
|
|
5491
|
+
appendStreamedReasoning(delta) {
|
|
5492
|
+
const block = this.streamingBlock();
|
|
5493
|
+
block.appendReasoning(delta);
|
|
5494
|
+
if (!this.fading) return;
|
|
5495
|
+
if (this.reasoningTail === void 0) {
|
|
5496
|
+
this.reasoningTail = this.createTail();
|
|
5497
|
+
block.setReasoningFade(this.tailRender(() => this.reasoningTail));
|
|
4563
5498
|
}
|
|
4564
|
-
this.
|
|
5499
|
+
this.reasoningTail.append(delta);
|
|
4565
5500
|
this.updateFadeTicker();
|
|
4566
5501
|
}
|
|
4567
5502
|
/**
|
|
4568
|
-
*
|
|
4569
|
-
*
|
|
4570
|
-
|
|
5503
|
+
* Start tracking one streaming region against the application's clock.
|
|
5504
|
+
* @returns the tail deltas are appended to.
|
|
5505
|
+
*/
|
|
5506
|
+
createTail() {
|
|
5507
|
+
return new FadeTracker({
|
|
5508
|
+
steps: this.deps.fadeSteps,
|
|
5509
|
+
stepMs: this.deps.fadeStepMs,
|
|
5510
|
+
now: () => this.deps.now()
|
|
5511
|
+
});
|
|
5512
|
+
}
|
|
5513
|
+
/**
|
|
5514
|
+
* The live view of one tail the streaming block reads per render.
|
|
5515
|
+
* @param tail - reads the field the tail is held in, so a render after the
|
|
5516
|
+
* message settled sees the empty tail rather than the one it was given.
|
|
5517
|
+
* @returns the spans, the drawing settings, and the width-change flush.
|
|
5518
|
+
*/
|
|
5519
|
+
tailRender(tail) {
|
|
5520
|
+
return {
|
|
5521
|
+
spans: () => tail()?.spans() ?? [],
|
|
5522
|
+
style: () => this.fadeStyle,
|
|
5523
|
+
steps: this.deps.fadeSteps,
|
|
5524
|
+
flush: () => {
|
|
5525
|
+
this.flushFade();
|
|
5526
|
+
}
|
|
5527
|
+
};
|
|
5528
|
+
}
|
|
5529
|
+
/**
|
|
5530
|
+
* Fade one group of a card's rows in from the terminal background, from the
|
|
5531
|
+
* instant the log carried them. A terminal that draws no ramp and a session
|
|
5532
|
+
* being replayed fade nothing.
|
|
5533
|
+
* @param attach - hands the fade to the block that draws those rows.
|
|
5534
|
+
*/
|
|
5535
|
+
fadeBlock(attach) {
|
|
5536
|
+
if (!this.fading || this.replaying) return;
|
|
5537
|
+
const clock = new BlockFadeClock({
|
|
5538
|
+
bornAt: this.deps.now(),
|
|
5539
|
+
stepMs: this.deps.fadeStepMs,
|
|
5540
|
+
steps: this.deps.fadeSteps,
|
|
5541
|
+
now: () => this.deps.now()
|
|
5542
|
+
});
|
|
5543
|
+
this.blockFades.add(clock);
|
|
5544
|
+
attach({
|
|
5545
|
+
age: () => clock.age(),
|
|
5546
|
+
style: () => this.fadeStyle
|
|
5547
|
+
});
|
|
5548
|
+
this.updateFadeTicker();
|
|
5549
|
+
}
|
|
5550
|
+
/**
|
|
5551
|
+
* Settle what the current message has drawn and stop tracking it: both
|
|
5552
|
+
* tails go, so their text renders at the terminal's foreground from the next
|
|
5553
|
+
* render on. Card fades are left to expire on their own clock, because a
|
|
5554
|
+
* card that landed at the end of a turn keeps brightening after it.
|
|
4571
5555
|
*/
|
|
4572
5556
|
endFade() {
|
|
4573
|
-
this.
|
|
5557
|
+
this.textTail = void 0;
|
|
5558
|
+
this.reasoningTail = void 0;
|
|
4574
5559
|
this.updateFadeTicker();
|
|
4575
5560
|
}
|
|
4576
5561
|
/**
|
|
4577
|
-
* Settle what is drawn while the message keeps streaming, which
|
|
4578
|
-
*
|
|
4579
|
-
*
|
|
5562
|
+
* Settle what is drawn while the message keeps streaming, which a block asks
|
|
5563
|
+
* for after a width change: the columns either tail was matched against no
|
|
5564
|
+
* longer describe the rewrapped lines.
|
|
4580
5565
|
*/
|
|
4581
5566
|
flushFade() {
|
|
4582
|
-
this.
|
|
5567
|
+
this.textTail?.flush();
|
|
5568
|
+
this.reasoningTail?.flush();
|
|
4583
5569
|
this.updateFadeTicker();
|
|
4584
5570
|
}
|
|
4585
5571
|
/**
|
|
4586
|
-
* Arm the fade tick while a
|
|
4587
|
-
* and disarm it otherwise, so a session that is not
|
|
4588
|
-
* timer. Exactly one runs at a time, and a stopped
|
|
5572
|
+
* Arm the fade tick while streamed text or a card still draws below the last
|
|
5573
|
+
* brightness level and disarm it otherwise, so a session that is not
|
|
5574
|
+
* streaming runs no fade timer. Exactly one runs at a time, and a stopped
|
|
5575
|
+
* app runs none.
|
|
4589
5576
|
*/
|
|
4590
5577
|
updateFadeTicker() {
|
|
4591
|
-
|
|
4592
|
-
if (!this.stopped && tail !== void 0 && tail.needsRepaint()) {
|
|
5578
|
+
if (!this.stopped && this.fadesMoving()) {
|
|
4593
5579
|
this.fadeTicker ??= this.deps.tick(() => {
|
|
4594
|
-
this.onFadeTick(
|
|
5580
|
+
this.onFadeTick();
|
|
4595
5581
|
}, this.deps.fadeStepMs);
|
|
4596
5582
|
return;
|
|
4597
5583
|
}
|
|
@@ -4601,14 +5587,64 @@ var TuiApp = class {
|
|
|
4601
5587
|
ticker();
|
|
4602
5588
|
}
|
|
4603
5589
|
/**
|
|
4604
|
-
*
|
|
4605
|
-
*
|
|
4606
|
-
*
|
|
4607
|
-
|
|
4608
|
-
|
|
5590
|
+
* Whether anything the application fades still draws below the last
|
|
5591
|
+
* brightness level.
|
|
5592
|
+
* @returns true while a tail or a card fade keeps changing what is drawn.
|
|
5593
|
+
*/
|
|
5594
|
+
fadesMoving() {
|
|
5595
|
+
return this.textTail?.needsRepaint() === true || this.reasoningTail?.needsRepaint() === true || this.blockFades.needsRepaint();
|
|
5596
|
+
}
|
|
5597
|
+
/**
|
|
5598
|
+
* Apply everything the frame's geometry decides, on the frame that was just
|
|
5599
|
+
* built and before it is written.
|
|
5600
|
+
*
|
|
5601
|
+
* Two effects redraw lines a component already produced and are therefore
|
|
5602
|
+
* held to the renderer's repaint window ({@link GuardedMainScreen}): a
|
|
5603
|
+
* running fade, which is told each block's own first repaintable line, and
|
|
5604
|
+
* the focus gutter, which a block gains or loses only while its first line
|
|
5605
|
+
* lies inside the window. A focused block above the window simply stays
|
|
5606
|
+
* unmarked, and the inspector reports that instead.
|
|
5607
|
+
* @param viewportTop - the frame's first repaintable line.
|
|
5608
|
+
* @param width - the width it was built at.
|
|
5609
|
+
* @returns whether anything changed a line, so the frame is built again
|
|
5610
|
+
* before it is written.
|
|
5611
|
+
*/
|
|
5612
|
+
settleFrame(viewportTop, width) {
|
|
5613
|
+
const section = this.focusedSection();
|
|
5614
|
+
const wanted = section !== void 0 && this.focus === "transcript" ? {
|
|
5615
|
+
block: section.block,
|
|
5616
|
+
part: section.part.kind
|
|
5617
|
+
} : void 0;
|
|
5618
|
+
const fades = this.fadesMoving();
|
|
5619
|
+
if (!fades && wanted === void 0 && this.highlighted === void 0) return false;
|
|
5620
|
+
let start = this.header.render(width).length;
|
|
5621
|
+
let wantedStart;
|
|
5622
|
+
let changed = false;
|
|
5623
|
+
for (const child of this.chat.children) {
|
|
5624
|
+
if (isSectionSource(child) && child === wanted?.block) wantedStart = start;
|
|
5625
|
+
if (fades && (child instanceof AssistantBlock || child instanceof ToolBlock)) {
|
|
5626
|
+
if (child.setRepaintFloor(repaintFloor(start, viewportTop))) changed = true;
|
|
5627
|
+
}
|
|
5628
|
+
start += child.render(width).length;
|
|
5629
|
+
}
|
|
5630
|
+
const target = wantedStart !== void 0 && repaintFloor(wantedStart, viewportTop) === 0 ? wanted : void 0;
|
|
5631
|
+
const current = this.highlighted;
|
|
5632
|
+
if (current?.block === target?.block && current?.part === target?.part) return changed;
|
|
5633
|
+
current?.block.setHighlight(void 0);
|
|
5634
|
+
target?.block.setHighlight(target.part);
|
|
5635
|
+
this.highlighted = target;
|
|
5636
|
+
return true;
|
|
5637
|
+
}
|
|
5638
|
+
/**
|
|
5639
|
+
* One fade period: every tracked fade drops what settled since the last one,
|
|
5640
|
+
* the render request redraws the levels the clock moved, and the disarm
|
|
5641
|
+
* check follows. Ages come from the clock, so this period repaints the
|
|
5642
|
+
* levels the elapsed time asks for however long the period itself ran.
|
|
4609
5643
|
*/
|
|
4610
|
-
onFadeTick(
|
|
4611
|
-
|
|
5644
|
+
onFadeTick() {
|
|
5645
|
+
this.textTail?.tick();
|
|
5646
|
+
this.reasoningTail?.tick();
|
|
5647
|
+
this.blockFades.tick();
|
|
4612
5648
|
this.tui.requestRender();
|
|
4613
5649
|
this.updateFadeTicker();
|
|
4614
5650
|
}
|
|
@@ -4625,7 +5661,7 @@ var TuiApp = class {
|
|
|
4625
5661
|
if (chunk.text !== "") this.appendStreamedText(chunk.text);
|
|
4626
5662
|
break;
|
|
4627
5663
|
case "reasoning-delta":
|
|
4628
|
-
if (chunk.text !== "") this.
|
|
5664
|
+
if (chunk.text !== "") this.appendStreamedReasoning(chunk.text);
|
|
4629
5665
|
break;
|
|
4630
5666
|
case "tool-call-delta":
|
|
4631
5667
|
if (chunk.name !== void 0) this.loader.setMessage(`calling ${chunk.name}`);
|
|
@@ -4684,10 +5720,13 @@ var TuiApp = class {
|
|
|
4684
5720
|
const { callId, name, arguments: argumentsJson } = event.data;
|
|
4685
5721
|
const args = parseArguments(argumentsJson);
|
|
4686
5722
|
this.toolArguments.set(callId, args);
|
|
4687
|
-
const block = new ToolBlock(this.theme, name, toolCallText(argumentsJson, this.presentCall(name, args)));
|
|
5723
|
+
const block = new ToolBlock(this.theme, name, toolCallText(argumentsJson, this.presentCall(name, args)), this.turn);
|
|
4688
5724
|
block.setExpanded(this.toolsExpanded);
|
|
4689
5725
|
this.toolBlocks.set(callId, block);
|
|
4690
5726
|
this.chat.addChild(block);
|
|
5727
|
+
this.fadeBlock((fade) => {
|
|
5728
|
+
block.setFade(fade);
|
|
5729
|
+
});
|
|
4691
5730
|
break;
|
|
4692
5731
|
}
|
|
4693
5732
|
case "tool/result": {
|
|
@@ -4697,6 +5736,9 @@ var TuiApp = class {
|
|
|
4697
5736
|
const isError = result.isError === true;
|
|
4698
5737
|
const view = this.presentResult(block.name, this.toolArguments.get(result.toolCallId), result.content, isError, event.data.meta);
|
|
4699
5738
|
block.setResult(toolResultLines(view, result.content), isError);
|
|
5739
|
+
this.fadeBlock((fade) => {
|
|
5740
|
+
block.setResultFade(fade);
|
|
5741
|
+
});
|
|
4700
5742
|
this.loader.setMessage("thinking");
|
|
4701
5743
|
break;
|
|
4702
5744
|
}
|
|
@@ -4755,7 +5797,7 @@ var TuiApp = class {
|
|
|
4755
5797
|
if (source.kind === "user") {
|
|
4756
5798
|
if (this.submittedIds.has(message.id)) return;
|
|
4757
5799
|
const attachments = message.content.filter((block) => block.type !== "text").map((block) => `[${block.type}]`);
|
|
4758
|
-
this.chat.addChild(new UserBlock(this.theme, [contentText(message.content), ...attachments].filter((part) => part !== "").join("\n")));
|
|
5800
|
+
this.chat.addChild(new UserBlock(this.theme, [contentText(message.content), ...attachments].filter((part) => part !== "").join("\n"), this.turn));
|
|
4759
5801
|
return;
|
|
4760
5802
|
}
|
|
4761
5803
|
if (source.kind === "plugin" && source.form === "notice") this.chat.addChild(new NoticeBlock(this.theme, source.summary));
|
|
@@ -4864,9 +5906,10 @@ const Config = z.object({
|
|
|
4864
5906
|
prompt: z.string(),
|
|
4865
5907
|
resume: z.string(),
|
|
4866
5908
|
toolPreviewLines: z.natural().min(1).default(8),
|
|
5909
|
+
focusPreviewLines: z.natural().min(1).default(12),
|
|
4867
5910
|
liveRefreshMs: z.natural().min(100).default(1e3),
|
|
4868
|
-
streamFadeSteps: z.natural().min(2).default(
|
|
4869
|
-
streamFadeStepMs: z.natural().min(16).default(
|
|
5911
|
+
streamFadeSteps: z.natural().min(2).default(8),
|
|
5912
|
+
streamFadeStepMs: z.natural().min(16).default(33),
|
|
4870
5913
|
reducedMotion: z.boolean().default(false),
|
|
4871
5914
|
openBrowser: z.boolean().default(true)
|
|
4872
5915
|
});
|
|
@@ -5039,6 +6082,7 @@ async function run(ctx, config, host) {
|
|
|
5039
6082
|
terminal: host.createTerminal(),
|
|
5040
6083
|
palette: createPalette(host.color),
|
|
5041
6084
|
toolPreviewLines: config.toolPreviewLines,
|
|
6085
|
+
focusPreviewLines: config.focusPreviewLines,
|
|
5042
6086
|
liveRefreshMs: config.liveRefreshMs,
|
|
5043
6087
|
fadeSteps: config.streamFadeSteps,
|
|
5044
6088
|
fadeStepMs: config.streamFadeStepMs,
|