ucode-agent 1.30.0 → 1.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/loop.js +5 -0
- package/src/ui/activity.js +44 -22
- package/src/ui/screen.js +36 -20
package/package.json
CHANGED
package/src/core/loop.js
CHANGED
|
@@ -1034,6 +1034,11 @@ export class Agent {
|
|
|
1034
1034
|
if (message.short) this.short.add(skill.name);
|
|
1035
1035
|
this.push(message);
|
|
1036
1036
|
this.ui.note(`${skill.name} skill loaded for this${message.short ? ' (short form)' : ''}`);
|
|
1037
|
+
// A clear row under it. This is bookkeeping about the turn, not part of
|
|
1038
|
+
// it, and against the first line of thinking the two read as one block —
|
|
1039
|
+
// the notice looks like the opening of the answer rather than a note
|
|
1040
|
+
// about how the answer is being reached.
|
|
1041
|
+
this.ui.blank();
|
|
1037
1042
|
}
|
|
1038
1043
|
}
|
|
1039
1044
|
|
package/src/ui/activity.js
CHANGED
|
@@ -109,26 +109,36 @@ export function shimmer(text, t, { level = chalk.level } = {}) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* The wordmark
|
|
112
|
+
* The wordmark being lit, once, at launch.
|
|
113
113
|
*
|
|
114
|
-
* The first thing anyone sees of a program is the
|
|
115
|
-
*
|
|
116
|
-
* crossing the mark left to right
|
|
117
|
-
*
|
|
118
|
-
* was printed and one that arrived.
|
|
114
|
+
* The first thing anyone sees of a program is the moment before they can type,
|
|
115
|
+
* and ucode was spending it showing a finished picture. A band of light
|
|
116
|
+
* crossing the mark left to right costs nothing and is the difference between
|
|
117
|
+
* a logo that was printed and one that arrived.
|
|
119
118
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
119
|
+
* The light does not pass over a finished wordmark — it paints it. Ahead of
|
|
120
|
+
* the crest the letters sit almost unlit, a silhouette dark enough to read as
|
|
121
|
+
* "not yet" and light enough not to look like a hole in the screen. At the
|
|
122
|
+
* crest they flare to near-white. Behind it they settle into the row's own
|
|
123
|
+
* resting gradient and stay there. So the mark is drawn on by the light rather
|
|
124
|
+
* than glinting under it, and the instant the crest leaves a column that
|
|
125
|
+
* column is already exactly what it will look like for the rest of the
|
|
126
|
+
* session.
|
|
127
|
+
*
|
|
128
|
+
* Every row is lit from the same clock, so the crest is one bar standing
|
|
129
|
+
* upright and travelling across the whole wordmark rather than six separate
|
|
130
|
+
* glints, and the band is wide enough that the edge of it is never a line.
|
|
124
131
|
*
|
|
125
132
|
* Below 256 colours there are no in-between shades to fade through, so the
|
|
126
133
|
* mark is simply drawn finished — a two-colour "sweep" is a flicker.
|
|
127
134
|
*/
|
|
128
|
-
export const SWEEP_MS =
|
|
135
|
+
export const SWEEP_MS = 1300;
|
|
129
136
|
|
|
130
137
|
/** Half the width of the travelling band, in characters. */
|
|
131
|
-
const SWEEP_BAND =
|
|
138
|
+
const SWEEP_BAND = 9;
|
|
139
|
+
|
|
140
|
+
/** The wordmark before the light reaches it: present, not yet lit. */
|
|
141
|
+
const UNLIT_RGB = [0x16, 0x23, 0x3d];
|
|
132
142
|
|
|
133
143
|
export function bannerSweep(line, row, rows, elapsed, { level = chalk.level } = {}) {
|
|
134
144
|
const text = String(line ?? '');
|
|
@@ -142,19 +152,30 @@ export function bannerSweep(line, row, rows, elapsed, { level = chalk.level } =
|
|
|
142
152
|
|
|
143
153
|
let out = '';
|
|
144
154
|
let run = '';
|
|
145
|
-
let
|
|
155
|
+
let runKey = '';
|
|
156
|
+
let runRGB = null;
|
|
146
157
|
const flush = () => {
|
|
147
158
|
if (!run) return;
|
|
148
|
-
const [r, g, b] =
|
|
159
|
+
const [r, g, b] = runRGB;
|
|
149
160
|
out += painter(level).rgb(r, g, b)(run);
|
|
150
161
|
run = '';
|
|
151
162
|
};
|
|
152
163
|
|
|
153
164
|
for (let i = 0; i < text.length; i++) {
|
|
154
|
-
const
|
|
165
|
+
const offset = i - centre;
|
|
166
|
+
// Behind the crest the column is finished; ahead of it, still waiting.
|
|
167
|
+
const base = offset > 0 ? UNLIT_RGB : rest;
|
|
168
|
+
const distance = Math.abs(offset);
|
|
155
169
|
const k = distance < SWEEP_BAND ? (Math.cos((Math.PI * distance) / SWEEP_BAND) + 1) / 2 : 0;
|
|
170
|
+
|
|
171
|
+
// Quantised so neighbours landing on the same shade share one escape code.
|
|
156
172
|
const step = Math.round(k * STEPS);
|
|
157
|
-
|
|
173
|
+
const key = `${offset > 0 ? 'a' : 'b'}${step}`;
|
|
174
|
+
if (key !== runKey) {
|
|
175
|
+
flush();
|
|
176
|
+
runKey = key;
|
|
177
|
+
runRGB = mix(base, PEAK_RGB, step / STEPS);
|
|
178
|
+
}
|
|
158
179
|
run += text[i];
|
|
159
180
|
}
|
|
160
181
|
flush();
|
|
@@ -220,10 +241,11 @@ export function indeterminate(cells, t, { level = chalk.level } = {}) {
|
|
|
220
241
|
* The whole live line: what is happening, that it is still happening, how long
|
|
221
242
|
* it has been happening, and how to stop it.
|
|
222
243
|
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
244
|
+
* Two things move on it, and they say different things. The bar is the steady
|
|
245
|
+
* pulse that means the program is alive at all; the band of light crossing the
|
|
246
|
+
* label is on the label, so it is the thing being done right now that looks
|
|
247
|
+
* live. How long the turn has taken is not here — that is a fact about the
|
|
248
|
+
* session, and it belongs on the status row with the other two.
|
|
227
249
|
*
|
|
228
250
|
* Things are given up from the least useful end as the terminal narrows: the
|
|
229
251
|
* hint first, then the bar shortens, then it goes, then the label is clipped.
|
|
@@ -240,7 +262,7 @@ export function workingLine({
|
|
|
240
262
|
const shortTail = elapsed || '';
|
|
241
263
|
|
|
242
264
|
const draw = (cells, tailText, labelRoom) => {
|
|
243
|
-
let out = `${glyph} ${
|
|
265
|
+
let out = `${glyph} ${shimmer(clip(text, labelRoom), t, { level })}`;
|
|
244
266
|
if (cells) out += ` ${indeterminate(cells, t, { level })}`;
|
|
245
267
|
if (tailText) out += ` ${dim(tailText)}`;
|
|
246
268
|
return out;
|
|
@@ -254,7 +276,7 @@ export function workingLine({
|
|
|
254
276
|
if (labelRoom >= Math.min(MIN_LABEL, text.length)) return draw(cells, tailText, labelRoom);
|
|
255
277
|
}
|
|
256
278
|
|
|
257
|
-
return `${glyph} ${
|
|
279
|
+
return `${glyph} ${shimmer(clip(text, Math.max(1, room - 2)), t, { level })}`;
|
|
258
280
|
}
|
|
259
281
|
|
|
260
282
|
/**
|
package/src/ui/screen.js
CHANGED
|
@@ -100,16 +100,10 @@ const at = (row, col) => `${ESC}[${row};${col}H`;
|
|
|
100
100
|
const title = (t) => `${ESC}]0;${t}\x07`;
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
|
-
* Fixed rows below the header: the gap under it, the
|
|
104
|
-
* the input box
|
|
105
|
-
* status row.
|
|
106
|
-
*
|
|
107
|
-
* The live line's row is held whether anything is running or not. Adding it
|
|
108
|
-
* only while busy would move the whole transcript up a row at the start of
|
|
109
|
-
* every turn and back down at the end of it, which reads as the screen
|
|
110
|
-
* flinching each time you press return.
|
|
103
|
+
* Fixed rows below the header: the gap under it, the gap above the input box,
|
|
104
|
+
* the input box's two borders, the blank row inside it, and the status row.
|
|
111
105
|
*/
|
|
112
|
-
const CHROME_BELOW =
|
|
106
|
+
const CHROME_BELOW = 6;
|
|
113
107
|
|
|
114
108
|
/** How long one sentence of reasoning holds the line before the next takes it. */
|
|
115
109
|
const THOUGHT_HOLD_MS = 1100;
|
|
@@ -930,7 +924,15 @@ export class Screen {
|
|
|
930
924
|
const inner = width - 2; // the space between the two borders
|
|
931
925
|
const chip = this.modeChip();
|
|
932
926
|
const left = ` ${chip} ${chalk.white(this.model || '—')}`;
|
|
933
|
-
|
|
927
|
+
|
|
928
|
+
// How long the turn has taken, back in the box beside the other two facts
|
|
929
|
+
// about the session. It is not on the live line: that line says what is
|
|
930
|
+
// being done, and a clock ticking in the middle of it competes with the
|
|
931
|
+
// words for no reason. Under a second there is no number worth reading.
|
|
932
|
+
const now = Date.now();
|
|
933
|
+
const since = this.activity?.start ?? this.status.since ?? 0;
|
|
934
|
+
const running = this.busy() && since && now - since >= 1000;
|
|
935
|
+
const right = `${running ? `${dim(formatDuration(now - since))} ` : ''}${this.percentChip()} `;
|
|
934
936
|
|
|
935
937
|
// Where a click on the bottom row still counts as hitting the mode chip.
|
|
936
938
|
this.chipTo = 2 + visLen(chip);
|
|
@@ -956,20 +958,27 @@ export class Screen {
|
|
|
956
958
|
activityLine(width = this.width()) {
|
|
957
959
|
if (!this.busy()) return '';
|
|
958
960
|
const now = Date.now();
|
|
959
|
-
const since = this.activity?.start ?? this.status.since ?? now;
|
|
960
961
|
return workingLine({
|
|
961
962
|
glyph: spinnerGlyph(this.tick, now),
|
|
962
963
|
label: this.status.busy ? this.status.text : 'working',
|
|
963
|
-
elapsed: now - since >= 1000 ? formatDuration(now - since) : '',
|
|
964
964
|
hint: 'esc to stop',
|
|
965
965
|
room: Math.max(4, width),
|
|
966
966
|
t: now,
|
|
967
967
|
});
|
|
968
968
|
}
|
|
969
969
|
|
|
970
|
-
/**
|
|
970
|
+
/**
|
|
971
|
+
* Which row the live line is painted on, 1-based, or 0 when it is not shown.
|
|
972
|
+
*
|
|
973
|
+
* It is the last line of the conversation, so its row moves as the
|
|
974
|
+
* conversation grows and stops moving once the viewport is full. Scrolled
|
|
975
|
+
* back, or with the picker open, it is not on screen at all and the cheap
|
|
976
|
+
* repaint has nothing to do.
|
|
977
|
+
*/
|
|
971
978
|
activityRowAt() {
|
|
972
|
-
|
|
979
|
+
if (this.scroll > 0 || this.picker) return 0;
|
|
980
|
+
const index = Math.min(this.lines.length + 1, this.viewportHeight()) - 1;
|
|
981
|
+
return index < 0 ? 0 : this.headerHeight() + 2 + index;
|
|
973
982
|
}
|
|
974
983
|
|
|
975
984
|
/**
|
|
@@ -988,9 +997,10 @@ export class Screen {
|
|
|
988
997
|
}
|
|
989
998
|
const [row, col] = this.caret();
|
|
990
999
|
const width = this.width();
|
|
1000
|
+
const liveAt = this.activityRowAt();
|
|
991
1001
|
this.output.write(
|
|
992
1002
|
HIDE +
|
|
993
|
-
at(
|
|
1003
|
+
(liveAt ? at(liveAt, 1) + CLEAR_LINE + padVis(this.activityLine(width), width) : '') +
|
|
994
1004
|
at(this.rows - 1, 1) + CLEAR_LINE + boxRow(this.statusRow(width), width, this.borderPaint()) +
|
|
995
1005
|
at(row, col) + SHOW
|
|
996
1006
|
);
|
|
@@ -1481,18 +1491,24 @@ export class Screen {
|
|
|
1481
1491
|
const width = this.width();
|
|
1482
1492
|
const height = this.viewportHeight();
|
|
1483
1493
|
|
|
1484
|
-
|
|
1494
|
+
// The live line is the last line of the conversation, not a fixture above
|
|
1495
|
+
// the input box. Pinned down there it sat at the bottom of the screen while
|
|
1496
|
+
// the message that started it was at the top, with the empty middle of the
|
|
1497
|
+
// viewport between them — so the thing being done looked unrelated to the
|
|
1498
|
+
// thing that had been asked. On the end of the transcript it arrives
|
|
1499
|
+
// directly under the prompt, which is where the eye already is.
|
|
1500
|
+
const live = this.activityLine(width);
|
|
1501
|
+
const said = live ? [...this.lines, live] : this.lines;
|
|
1502
|
+
|
|
1503
|
+
const end = Math.max(0, said.length - this.scroll);
|
|
1485
1504
|
const start = Math.max(0, end - height);
|
|
1486
|
-
const window = this.picker ? this.pickerLines(height) :
|
|
1505
|
+
const window = this.picker ? this.pickerLines(height) : said.slice(start, end);
|
|
1487
1506
|
while (window.length < height) window.push('');
|
|
1488
1507
|
|
|
1489
1508
|
const frame = [
|
|
1490
1509
|
...this.headerLines(),
|
|
1491
1510
|
'',
|
|
1492
1511
|
...window,
|
|
1493
|
-
// What is happening right now, in the same column as the steps above it,
|
|
1494
|
-
// because it is the next one of those. Empty between turns.
|
|
1495
|
-
this.activityLine(width),
|
|
1496
1512
|
// Always one clear row between the last thing said and the box you type
|
|
1497
1513
|
// in. Without it the newest line of output sits against the border and
|
|
1498
1514
|
// reads as part of the input rather than as the answer above it.
|