pum-agent 0.2.26-beta.3 → 0.2.27-beta.1
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/animation.tsx +45 -3
- package/src/app.tsx +11 -1
- package/src/output-minimal.ts +24 -17
- package/src/transcript-output.ts +3 -2
package/package.json
CHANGED
package/src/animation.tsx
CHANGED
|
@@ -50,7 +50,7 @@ const SHIMMER_WIDTH = 7;
|
|
|
50
50
|
const SHIMMER_TAIL = 120;
|
|
51
51
|
|
|
52
52
|
const CARET = "▊";
|
|
53
|
-
export const CARET_PERIOD_MS =
|
|
53
|
+
export const CARET_PERIOD_MS = 1_800;
|
|
54
54
|
/** Ramp length at each end of the caret's cycle, as a share of the period. */
|
|
55
55
|
const CARET_RAMP = 0.12;
|
|
56
56
|
/** Quantised fade steps, so one ramp costs a bounded number of repaints. */
|
|
@@ -139,7 +139,7 @@ export function weightedGlyph(glyph: string, strength: number): string {
|
|
|
139
139
|
return glyph === "\u2500" && strength >= HEAVY_RULE_STRENGTH ? "\u2501" : glyph;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
const RULE_CHARS_PER_MS = 0.
|
|
142
|
+
const RULE_CHARS_PER_MS = 0.04;
|
|
143
143
|
const RULE_HIGHLIGHT_WIDTH = 10;
|
|
144
144
|
const COMET_CHARS_PER_MS = 0.035;
|
|
145
145
|
const ELECTRIC_FRAME_MS = 140;
|
|
@@ -222,6 +222,48 @@ const ClockContext = createContext<Clock>({
|
|
|
222
222
|
|
|
223
223
|
export const useClock = () => useContext(ClockContext);
|
|
224
224
|
|
|
225
|
+
/** The prompt cursor stays visible for most of each slow blink cycle. */
|
|
226
|
+
export function inputCursorVisible(elapsedMs: number): boolean {
|
|
227
|
+
const phase = ((elapsedMs % CARET_PERIOD_MS) + CARET_PERIOD_MS) % CARET_PERIOD_MS;
|
|
228
|
+
return phase < CARET_PERIOD_MS * 0.65;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Replace the terminal-defined fast cursor blink with the shared slow clock. */
|
|
232
|
+
export function PromptCursorBlink({
|
|
233
|
+
inputRef,
|
|
234
|
+
active,
|
|
235
|
+
}: {
|
|
236
|
+
inputRef: RefObject<TextareaRenderable | null>;
|
|
237
|
+
active: boolean;
|
|
238
|
+
}) {
|
|
239
|
+
const { subscribe, enabled } = useClock();
|
|
240
|
+
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
const input = inputRef.current;
|
|
243
|
+
if (!input) return;
|
|
244
|
+
input.cursorStyle = { style: "block", blinking: false };
|
|
245
|
+
if (!active) {
|
|
246
|
+
input.showCursor = false;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
input.showCursor = true;
|
|
251
|
+
if (!enabled) return;
|
|
252
|
+
|
|
253
|
+
let startedAt: number | undefined;
|
|
254
|
+
let visible = true;
|
|
255
|
+
return subscribe((elapsedMs) => {
|
|
256
|
+
startedAt ??= elapsedMs;
|
|
257
|
+
const next = inputCursorVisible(elapsedMs - startedAt);
|
|
258
|
+
if (next === visible) return;
|
|
259
|
+
visible = next;
|
|
260
|
+
if (inputRef.current) inputRef.current.showCursor = next;
|
|
261
|
+
});
|
|
262
|
+
}, [active, enabled, inputRef, subscribe]);
|
|
263
|
+
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
225
267
|
/**
|
|
226
268
|
* One frame callback for the whole app. Animated components write straight to
|
|
227
269
|
* their own renderable, so no React render happens per frame. The renderer is
|
|
@@ -344,7 +386,7 @@ const RAISED_LETTERS: Record<string, string> = {
|
|
|
344
386
|
};
|
|
345
387
|
|
|
346
388
|
/** How long one crest takes to cross the phrase, and the rest between sweeps. */
|
|
347
|
-
const WAVE_CHARS_PER_MS = 0.
|
|
389
|
+
const WAVE_CHARS_PER_MS = 0.011;
|
|
348
390
|
const WAVE_WIDTH = 5;
|
|
349
391
|
const WAVE_REST_CHARS = 90;
|
|
350
392
|
|
package/src/app.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import { Component, memo, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
|
|
14
14
|
import {
|
|
15
15
|
AnimationProvider,
|
|
16
16
|
PlaceholderWave,
|
|
17
|
+
PromptCursorBlink,
|
|
17
18
|
supportsTrueColor,
|
|
18
19
|
useWorkingRule,
|
|
19
20
|
type WorkingRuleLabel,
|
|
@@ -356,6 +357,9 @@ type PromptTextareaAction =
|
|
|
356
357
|
| "select-buffer-home"
|
|
357
358
|
| "select-buffer-end";
|
|
358
359
|
|
|
360
|
+
/** Half of OpenTUI's default drag auto-scroll rate. */
|
|
361
|
+
export const PROMPT_SCROLL_SPEED = 8;
|
|
362
|
+
|
|
359
363
|
/** Standard editor navigation, with wrapped rows treated as visible lines. */
|
|
360
364
|
export const PROMPT_TEXTAREA_KEY_BINDINGS: Array<{
|
|
361
365
|
name: string;
|
|
@@ -5758,6 +5762,9 @@ export function App({
|
|
|
5758
5762
|
const streamGap = visibleTx.stream
|
|
5759
5763
|
? needsTranscriptGap(lastLine, { kind: "text", role: visibleTx.stream.kind, text: visibleTx.stream.text })
|
|
5760
5764
|
: false;
|
|
5765
|
+
const promptFocused = !transcriptFocused && !settingsOpen && !helpOpen && !historyOpen
|
|
5766
|
+
&& !statsOpen && !agentSelectorOpen && !triggersOpen && !loginOpen && !providersOpen
|
|
5767
|
+
&& !visibleQuestionnaire && !spawnPreview && !newsOpen && !todoVisible;
|
|
5761
5768
|
|
|
5762
5769
|
// One element for the whole transcript, rebuilt only when what it shows
|
|
5763
5770
|
// changes. React walks a child list of this size on every render of the app,
|
|
@@ -5999,12 +6006,15 @@ export function App({
|
|
|
5999
6006
|
wrapMode="word"
|
|
6000
6007
|
keyBindings={PROMPT_TEXTAREA_KEY_BINDINGS}
|
|
6001
6008
|
scrollMargin={1}
|
|
6002
|
-
|
|
6009
|
+
scrollSpeed={PROMPT_SCROLL_SPEED}
|
|
6010
|
+
cursorStyle={{ style: "block", blinking: false }}
|
|
6011
|
+
focused={promptFocused}
|
|
6003
6012
|
onContentChange={handleTextareaChange}
|
|
6004
6013
|
onCursorChange={scheduleInputMetrics}
|
|
6005
6014
|
onSubmit={() => shellModeRef.current ? submitShellCommand() : submitPrompt()}
|
|
6006
6015
|
style={{ width: promptInputColumns, flexShrink: 0, minWidth: 0, height: inputRows }}
|
|
6007
6016
|
/>
|
|
6017
|
+
<PromptCursorBlink inputRef={inputRef} active={promptFocused} />
|
|
6008
6018
|
{/* Reserve six columns on normal terminals. This forces wrapping
|
|
6009
6019
|
before cursor movement can briefly overdraw the terminal edge. */}
|
|
6010
6020
|
<box style={{ width: promptRightColumns, height: inputRows, flexShrink: 0 }} />
|
package/src/output-minimal.ts
CHANGED
|
@@ -98,15 +98,12 @@ function joinPhrases(phrases: readonly string[]): string {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
* Summarize one consecutive run
|
|
101
|
+
* Summarize one consecutive run without exposing arguments or outcomes.
|
|
102
102
|
*
|
|
103
103
|
* Tool types stay in first-occurrence order. Repeated types combine even when
|
|
104
|
-
* another
|
|
104
|
+
* another tool type occurs between them.
|
|
105
105
|
*/
|
|
106
|
-
|
|
107
|
-
if (calls.length === 0 || calls.some((call) => call.state !== "ok")) {
|
|
108
|
-
throw new Error("A minimal tool summary requires one or more successful calls");
|
|
109
|
-
}
|
|
106
|
+
function summarizeToolCalls(calls: readonly ToolCall[]): MinimalToolSummaryLine {
|
|
110
107
|
const counts = new Map<string, number>();
|
|
111
108
|
for (const call of calls) counts.set(call.name, (counts.get(call.name) ?? 0) + 1);
|
|
112
109
|
const phrases = [...counts].map(([name, count]) => minimalToolPhrase(name, count));
|
|
@@ -117,32 +114,42 @@ export function summarizeSuccessfulToolCalls(calls: readonly ToolCall[]): Minima
|
|
|
117
114
|
};
|
|
118
115
|
}
|
|
119
116
|
|
|
117
|
+
/** Validate and summarize a direct list of successful calls. */
|
|
118
|
+
export function summarizeSuccessfulToolCalls(calls: readonly ToolCall[]): MinimalToolSummaryLine {
|
|
119
|
+
if (calls.length === 0 || calls.some((call) => call.state !== "ok")) {
|
|
120
|
+
throw new Error("A minimal tool summary requires one or more successful calls");
|
|
121
|
+
}
|
|
122
|
+
return summarizeToolCalls(calls);
|
|
123
|
+
}
|
|
124
|
+
|
|
120
125
|
/**
|
|
121
126
|
* Convert transcript lines for a grouped output mode.
|
|
122
127
|
*
|
|
123
|
-
* Every non-tool line
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
128
|
+
* Every non-tool line ends a grouped run. Normal also ends a run at commands,
|
|
129
|
+
* mutations, running calls, failed calls, and rejected calls. Quiet groups all
|
|
130
|
+
* settled calls, including failed and rejected calls, but keeps running calls
|
|
131
|
+
* visible until they settle.
|
|
127
132
|
*/
|
|
128
133
|
export function minimalTranscriptLines(
|
|
129
134
|
lines: readonly Line[],
|
|
130
|
-
|
|
135
|
+
groupEverySettled = false,
|
|
131
136
|
): MinimalTranscriptLine[] {
|
|
132
137
|
const result: MinimalTranscriptLine[] = [];
|
|
133
|
-
let
|
|
138
|
+
let grouped: ToolCall[] = [];
|
|
134
139
|
const groups = (call: ToolCall) =>
|
|
135
|
-
|
|
140
|
+
groupEverySettled ? call.state !== "running" : isRoutineSuccessfulTool(call);
|
|
136
141
|
|
|
137
142
|
const flush = () => {
|
|
138
|
-
if (
|
|
139
|
-
result.push(
|
|
140
|
-
|
|
143
|
+
if (grouped.length === 0) return;
|
|
144
|
+
result.push(groupEverySettled
|
|
145
|
+
? summarizeToolCalls(grouped)
|
|
146
|
+
: summarizeSuccessfulToolCalls(grouped));
|
|
147
|
+
grouped = [];
|
|
141
148
|
};
|
|
142
149
|
|
|
143
150
|
for (const line of lines) {
|
|
144
151
|
if (line.kind === "tool" && groups(line.call)) {
|
|
145
|
-
|
|
152
|
+
grouped.push(line.call);
|
|
146
153
|
continue;
|
|
147
154
|
}
|
|
148
155
|
flush();
|
package/src/transcript-output.ts
CHANGED
|
@@ -19,8 +19,9 @@ export function transcriptOutputMode(settings: unknown): TranscriptOutputMode {
|
|
|
19
19
|
*
|
|
20
20
|
* Keep this boundary pure. Mode changes can then rerender the complete live or
|
|
21
21
|
* resumed transcript without rewriting session entries or model context.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* Normal aggregates routine successful calls. Quiet aggregates every settled
|
|
23
|
+
* call, including failed and rejected calls. Verbose keeps every canonical
|
|
24
|
+
* tool row and changes ToolLine presentation only.
|
|
24
25
|
*
|
|
25
26
|
* Inter-agent messages answer to their own setting, not to the mode. What one
|
|
26
27
|
* agent said to another is a different question from how much tool detail to
|