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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pum-agent",
3
- "version": "0.2.26-beta.3",
3
+ "version": "0.2.27-beta.1",
4
4
  "description": "A compact terminal coding agent powered by pi and OpenTUI.",
5
5
  "license": "MIT",
6
6
  "type": "module",
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 = 900;
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.08;
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.022;
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
- focused={!transcriptFocused && !settingsOpen && !helpOpen && !historyOpen && !statsOpen && !agentSelectorOpen && !triggersOpen && !loginOpen && !providersOpen && !visibleQuestionnaire && !spawnPreview && !newsOpen && !todoVisible}
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 }} />
@@ -98,15 +98,12 @@ function joinPhrases(phrases: readonly string[]): string {
98
98
  }
99
99
 
100
100
  /**
101
- * Summarize one consecutive run of successful tool calls.
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 successful tool type occurs between them.
104
+ * another tool type occurs between them.
105
105
  */
106
- export function summarizeSuccessfulToolCalls(calls: readonly ToolCall[]): MinimalToolSummaryLine {
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 and every non-successful tool call ends a success run.
124
- * Running, failed, and rejected calls remain unchanged with their details.
125
- * `groupEverySuccess` folds commands and mutations in too, which is what makes
126
- * Quiet minimal; Normal leaves them as their own rows.
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
- groupEverySuccess = false,
135
+ groupEverySettled = false,
131
136
  ): MinimalTranscriptLine[] {
132
137
  const result: MinimalTranscriptLine[] = [];
133
- let successful: ToolCall[] = [];
138
+ let grouped: ToolCall[] = [];
134
139
  const groups = (call: ToolCall) =>
135
- groupEverySuccess ? call.state === "ok" : isRoutineSuccessfulTool(call);
140
+ groupEverySettled ? call.state !== "running" : isRoutineSuccessfulTool(call);
136
141
 
137
142
  const flush = () => {
138
- if (successful.length === 0) return;
139
- result.push(summarizeSuccessfulToolCalls(successful));
140
- successful = [];
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
- successful.push(line.call);
152
+ grouped.push(line.call);
146
153
  continue;
147
154
  }
148
155
  flush();
@@ -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
- * Quiet and Normal aggregate routine successful calls. Verbose keeps every
23
- * canonical tool row and changes ToolLine presentation only.
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