@esso0428/pi-subagents 0.17.1 → 0.17.3

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.
@@ -29,6 +29,44 @@ const MATCH_BG = "\x1b[48;5;238m";
29
29
  const MATCH_BG_CURRENT = "\x1b[48;5;220m\x1b[30m";
30
30
  const BG_RESET = "\x1b[49m\x1b[39m";
31
31
 
32
+ function renderScrollbarCell(
33
+ theme: Theme,
34
+ totalLines: number,
35
+ viewportHeight: number,
36
+ scrollOffset: number,
37
+ lineIndex: number,
38
+ active: boolean,
39
+ ): string {
40
+ const total = Math.max(0, Math.floor(totalLines));
41
+ const viewport = Math.max(1, Math.floor(viewportHeight));
42
+ if (total <= viewport) return theme.fg("scrollbarTrack", " ");
43
+ const maxOffset = Math.max(0, total - viewport);
44
+ const offset = Math.max(0, Math.min(maxOffset, Math.floor(scrollOffset)));
45
+ const thumbHeight = Math.max(1, Math.round((viewport * viewport) / total));
46
+ const thumbStart = Math.round(((viewport - thumbHeight) * offset) / maxOffset);
47
+ const inThumb = lineIndex >= thumbStart && lineIndex < thumbStart + thumbHeight;
48
+ return theme.fg(
49
+ inThumb ? "scrollbarThumb" : "scrollbarTrack",
50
+ inThumb ? (active ? "█" : "┃") : "│",
51
+ );
52
+ }
53
+
54
+ function renderScrollMoreRule(
55
+ theme: Theme,
56
+ width: number,
57
+ direction: "top" | "bottom",
58
+ hiddenLines: number,
59
+ ): string {
60
+ const safeWidth = Math.max(1, Math.floor(width));
61
+ if (hiddenLines <= 0) return theme.fg("border", `├${"─".repeat(safeWidth)}┤`);
62
+ const label = `${direction === "top" ? "↑" : "↓"} ${hiddenLines} more`;
63
+ const labelWidth = visibleWidth(label);
64
+ if (labelWidth + 2 > safeWidth) return theme.fg("border", `├${"─".repeat(safeWidth)}┤`);
65
+ const left = Math.floor((safeWidth - labelWidth) / 2);
66
+ const right = Math.max(0, safeWidth - left - labelWidth);
67
+ return `${theme.fg("border", `├${"─".repeat(left)}`)}${theme.fg("dim", label)}${theme.fg("border", `${"─".repeat(right)}┤`)}`;
68
+ }
69
+
32
70
  /** The live fields needed by the viewer; historical viewers use a static source. */
33
71
  export type ConversationSource = Pick<AgentSession, "messages" | "subscribe">;
34
72
 
@@ -48,6 +86,7 @@ class FullToolPreview implements Component {
48
86
  private totalLines = 1;
49
87
  private pendingTop = false;
50
88
  private hoveredClose = false;
89
+ private scrollbarActive = false;
51
90
 
52
91
  constructor(
53
92
  private readonly tui: TUI,
@@ -99,10 +138,12 @@ class FullToolPreview implements Component {
99
138
  render(width: number): string[] {
100
139
  const safeWidth = Math.max(12, Math.floor(Number.isFinite(width) ? width : 12));
101
140
  const inner = Math.max(1, safeWidth - 2);
141
+ // Reserve the rightmost inner cell for the same track/thumb rail used by
142
+ // pi-tui's native ScrollView layout.
102
143
  const bodyWidth = Math.max(1, inner - 1);
103
144
  const terminalRows = Math.max(1, this.tui.terminal.rows);
104
145
  const viewport = Math.max(1, Math.min(30, Math.floor(terminalRows * 0.8), terminalRows - 6));
105
- const wrapped = this.body.render(bodyWidth);
146
+ const wrapped = this.body.render(Math.max(1, bodyWidth - 1));
106
147
  this.totalLines = wrapped.length;
107
148
  this.pageSize = viewport;
108
149
  this.scrollOffset = Math.min(this.scrollOffset, Math.max(0, this.totalLines - this.pageSize));
@@ -112,15 +153,28 @@ class FullToolPreview implements Component {
112
153
  const clipped = truncateToWidth(text, rowWidth, "…", true);
113
154
  return clipped + " ".repeat(Math.max(0, rowWidth - visibleWidth(clipped)));
114
155
  };
156
+ const hiddenAbove = this.scrollOffset;
157
+ const hiddenBelow = Math.max(0, this.totalLines - this.scrollOffset - this.pageSize);
115
158
  const close = this.theme.fg(this.hoveredClose ? "text" : "dim", "[esc]");
116
159
  const header = this.theme.bold(this.theme.fg("accent", this.title));
117
160
  const status = `${this.scrollOffset + 1}-${Math.min(this.totalLines, this.scrollOffset + this.pageSize)} / ${this.totalLines} lines · j/k gg/G ↑↓ PgUp/PgDn · [esc] close`;
118
161
  return [
119
162
  border(`╭${"─".repeat(inner)}╮`),
120
163
  `${border("│")}${pad(` ${header}`, inner - visibleWidth(close))}${close}${border("│")}`,
121
- `${border("├")}${border("─".repeat(inner))}${border("┤")}`,
122
- ...Array.from({ length: viewport }, (_, index) => `${border("│")}${pad(` ${visible[index] ?? ""}`, bodyWidth)}${border("│")}`),
123
- `${border("├")}${border("─".repeat(inner))}${border("┤")}`,
164
+ renderScrollMoreRule(this.theme, inner, "top", hiddenAbove),
165
+ ...Array.from({ length: viewport }, (_, index) => {
166
+ const content = pad(` ${visible[index] ?? ""}`, bodyWidth);
167
+ const rail = renderScrollbarCell(
168
+ this.theme,
169
+ this.totalLines,
170
+ this.pageSize,
171
+ this.scrollOffset,
172
+ index,
173
+ this.scrollbarActive,
174
+ );
175
+ return `${border("│")}${content}${rail}${border("│")}`;
176
+ }),
177
+ renderScrollMoreRule(this.theme, inner, "bottom", hiddenBelow),
124
178
  `${border("│")}${pad(this.theme.fg("dim", ` ${status}`))}${border("│")}`,
125
179
  border(`╰${"─".repeat(inner)}╯`),
126
180
  ];
@@ -134,6 +188,7 @@ class FullToolPreview implements Component {
134
188
  const next = Math.max(0, Math.min(Math.max(0, this.totalLines - this.pageSize), offset));
135
189
  if (next === this.scrollOffset) return;
136
190
  this.scrollOffset = next;
191
+ this.scrollbarActive = true;
137
192
  this.tui.requestRender();
138
193
  }
139
194
  }
@@ -171,6 +226,10 @@ export class ConversationViewer implements Component {
171
226
  private searchQuery = "";
172
227
  private searchMatches: ConversationMatch[] = [];
173
228
  private currentMatchIdx = -1;
229
+ /** Follow-up drafts, newest first, matching pi editor history semantics. */
230
+ private steerHistory: string[] = [];
231
+ private steerHistoryIndex = -1;
232
+ private steerHistoryDraft = "";
174
233
  private timeline: ConversationTimeline;
175
234
 
176
235
  constructor(
@@ -202,7 +261,16 @@ export class ConversationViewer implements Component {
202
261
 
203
262
  handleInput(data: string): void {
204
263
  if (this.composer) {
205
- this.composer.handleInput(data);
264
+ if (matchesKey(data, Key.alt("up")) || data === "a-up" || data === "alt+up") {
265
+ this.recallSteerDraft();
266
+ } else {
267
+ const before = this.composer.getValue();
268
+ this.composer.handleInput(data);
269
+ if (this.composer && before !== this.composer.getValue()) {
270
+ this.steerHistoryIndex = -1;
271
+ this.steerHistoryDraft = "";
272
+ }
273
+ }
206
274
  this.tui.requestRender();
207
275
  return;
208
276
  }
@@ -442,7 +510,7 @@ export class ConversationViewer implements Component {
442
510
  lines.push(hrMid);
443
511
  if (this.composer) {
444
512
  lines.push(row(this.composer.render(innerW)[0] ?? ""));
445
- const hint = th.fg("dim", "Enter send · Esc cancel");
513
+ const hint = th.fg("dim", "Enter send · Esc cancel · Alt+Up recall");
446
514
  const label = th.fg("accent", "✎ steer");
447
515
  lines.push(row(label + " ".repeat(Math.max(1, innerW - visibleWidth(label) - visibleWidth(hint))) + hint));
448
516
  } else if (this.searchMode) {
@@ -865,17 +933,39 @@ export class ConversationViewer implements Component {
865
933
  private openComposer(): void {
866
934
  const input = new Input();
867
935
  input.focused = true;
936
+ this.steerHistoryIndex = -1;
937
+ this.steerHistoryDraft = "";
868
938
  input.onSubmit = (value: string) => {
869
939
  const message = value.trim();
870
940
  this.composer = undefined;
871
- if (message) this.onSteer?.(message);
941
+ this.steerHistoryIndex = -1;
942
+ this.steerHistoryDraft = "";
943
+ if (message) {
944
+ if (this.steerHistory[0] !== message) this.steerHistory.unshift(message);
945
+ if (this.steerHistory.length > 100) this.steerHistory.pop();
946
+ this.onSteer?.(message);
947
+ }
872
948
  this.tui.requestRender();
873
949
  };
874
950
  input.onEscape = () => {
875
951
  this.composer = undefined;
952
+ this.steerHistoryIndex = -1;
953
+ this.steerHistoryDraft = "";
876
954
  this.tui.requestRender();
877
955
  };
878
956
  this.composer = input;
879
957
  this.tui.requestRender();
880
958
  }
959
+
960
+ /** Recall the newest prior steer, preserving the draft for future editing. */
961
+ private recallSteerDraft(): void {
962
+ if (!this.composer || this.steerHistory.length === 0) return;
963
+ if (this.steerHistoryIndex < 0) {
964
+ this.steerHistoryDraft = this.composer.getValue();
965
+ this.steerHistoryIndex = 0;
966
+ } else if (this.steerHistoryIndex < this.steerHistory.length - 1) {
967
+ this.steerHistoryIndex++;
968
+ }
969
+ this.composer.setValue(this.steerHistory[this.steerHistoryIndex] ?? this.steerHistoryDraft);
970
+ }
881
971
  }