@shanepadgett/tau-agent 0.5.0 → 0.6.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.
@@ -1,7 +1,7 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { type Component, getKeybindings, Key, matchesKey, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
3
3
  import { bindingHint, rawHint, SelectableList, Tabs, ToolPanel, type ToolPanelConfig } from "@shanepadgett/tau-tui";
4
- import type { ContextEntry, ContextOperation } from "./definitions.ts";
4
+ import type { ContextEntry } from "./definitions.ts";
5
5
 
6
6
  type SelectableEntry = ContextEntry & { id: string };
7
7
 
@@ -15,6 +15,7 @@ export class ContextPanel implements Component {
15
15
  private readonly selected = new Map<string, readonly SelectableEntry[]>();
16
16
  private readonly done: (result: ContextEntry[] | undefined) => void;
17
17
  private current: ContextEntry;
18
+ private fileOffset = 0;
18
19
 
19
20
  constructor(
20
21
  tui: TUI,
@@ -72,6 +73,17 @@ export class ContextPanel implements Component {
72
73
  const list = this.activeList();
73
74
  if (!list?.isFilterFocused()) {
74
75
  const keys = getKeybindings();
76
+ const pageSize = this.filePageSize();
77
+ if (this.current.files.length > pageSize && matchesKey(data, Key.alt("up"))) {
78
+ this.fileOffset = Math.max(0, this.fileOffset - pageSize);
79
+ this.sync();
80
+ return;
81
+ }
82
+ if (this.current.files.length > pageSize && matchesKey(data, Key.alt("down"))) {
83
+ this.fileOffset = Math.min(this.current.files.length - pageSize, this.fileOffset + pageSize);
84
+ this.sync();
85
+ return;
86
+ }
75
87
  if (matchesKey(data, Key.ctrl("c"))) {
76
88
  for (const [tab, tabList] of this.lists) {
77
89
  tabList.setSelectedIds([]);
@@ -90,7 +102,9 @@ export class ContextPanel implements Component {
90
102
  }
91
103
  }
92
104
  this.tabs.handleInput(data);
93
- this.current = this.activeList()?.getCurrentItem() ?? this.current;
105
+ const current = this.activeList()?.getCurrentItem() ?? this.current;
106
+ if (current.id !== this.current.id) this.fileOffset = 0;
107
+ this.current = current;
94
108
  this.sync();
95
109
  }
96
110
 
@@ -106,14 +120,35 @@ export class ContextPanel implements Component {
106
120
  }
107
121
  private body(): Component {
108
122
  return {
109
- render: (width) => [
110
- ...this.tabs.render(width),
111
- "",
112
- ...this.current.files.map((file) => truncateToWidth(this.theme.fg("muted", `• ${file}`), width, "…")),
113
- ],
123
+ render: (width) => {
124
+ const tabs = this.tabs.render(width);
125
+ const pageSize = this.filePageSize(tabs.length);
126
+ const maxOffset = Math.max(0, this.current.files.length - pageSize);
127
+ this.fileOffset = Math.min(this.fileOffset, maxOffset);
128
+ const files = this.current.files.slice(this.fileOffset, this.fileOffset + pageSize);
129
+ const range =
130
+ this.current.files.length > pageSize
131
+ ? this.theme.fg(
132
+ "dim",
133
+ `${this.fileOffset + 1}-${this.fileOffset + files.length} of ${this.current.files.length} files`,
134
+ )
135
+ : undefined;
136
+ return [
137
+ ...tabs,
138
+ "",
139
+ ...(range ? [truncateToWidth(range, width, "…")] : []),
140
+ ...files.map((file) => truncateToWidth(this.theme.fg("muted", `• ${file}`), width, "…")),
141
+ ];
142
+ },
114
143
  invalidate: () => this.tabs.invalidate(),
115
144
  };
116
145
  }
146
+ private filePageSize(tabLines = this.tabs.render(this.tui.terminal.columns).length): number {
147
+ const overlayHeight = Math.floor(this.tui.terminal.rows * 0.8);
148
+ const available = Math.max(1, overlayHeight - tabLines - 7);
149
+ const pageSize = Math.min(8, available);
150
+ return this.current.files.length > pageSize ? Math.max(1, pageSize - 1) : pageSize;
151
+ }
117
152
  private secondary(): string {
118
153
  return `${[...this.selected.values()].reduce((sum, items) => sum + items.length, 0)} selected`;
119
154
  }
@@ -123,6 +158,7 @@ export class ContextPanel implements Component {
123
158
  ? list.getKeyHints()
124
159
  : [
125
160
  ...this.tabs.getKeyHints(),
161
+ ...(this.current.files.length > this.filePageSize() ? [rawHint("option+↑/↓", "scroll files")] : []),
126
162
  rawHint("ctrl+c", "clear all"),
127
163
  bindingHint("tui.select.confirm", "inject"),
128
164
  bindingHint("tui.select.cancel", "cancel"),
@@ -134,104 +170,3 @@ export class ContextPanel implements Component {
134
170
  this.tui.requestRender();
135
171
  }
136
172
  }
137
-
138
- export type ProposalReviewDecision =
139
- | { kind: "approve"; operations: ContextOperation[] }
140
- | { kind: "feedback"; scope: "batch"; selectedIds: string[] }
141
- | { kind: "feedback"; scope: "operation"; operationId: string; selectedIds: string[] }
142
- | { kind: "rejected" };
143
-
144
- export class ProposalPanel implements Component {
145
- private readonly list: SelectableList<ContextOperation>;
146
- private readonly panel: ToolPanel;
147
- private readonly done: (result: ProposalReviewDecision) => void;
148
- private selected: readonly ContextOperation[] = [];
149
- private current: ContextOperation;
150
- constructor(
151
- tui: TUI,
152
- theme: Theme,
153
- proposals: readonly ContextOperation[],
154
- done: (result: ProposalReviewDecision) => void,
155
- ) {
156
- const first = proposals[0];
157
- if (!first) throw new Error("Proposal panel requires at least one operation");
158
- this.done = done;
159
- this.current = first;
160
- const items = [...proposals];
161
- this.list = new SelectableList(theme, {
162
- items,
163
- emptyMessage: "No proposals.",
164
- selection: { kind: "multi", primaryLabel: "apply selected" },
165
- actions: [],
166
- cancelLabel: "cancel",
167
- maxVisible: 12,
168
- renderItem: (item, _state, width) => [
169
- truncateToWidth(
170
- `${theme.fg("accent", item.kind)} · ${item.tab}/${item.concept}/${item.entry} ${theme.fg("dim", item.reason)}`,
171
- width,
172
- "…",
173
- ),
174
- ],
175
- onResult: (result) =>
176
- done(
177
- result.kind === "primary" && result.items.length > 0
178
- ? { kind: "approve", operations: [...result.items] }
179
- : { kind: "rejected" },
180
- ),
181
- onSelectionChange: (selected) => {
182
- this.selected = selected;
183
- tui.requestRender();
184
- },
185
- });
186
- this.list.setSelectedIds(items.map((item) => item.id));
187
- this.selected = items;
188
- this.panel = new ToolPanel(theme, {
189
- title: "Maintain contexts",
190
- secondary: "Review proposed operations",
191
- body: {
192
- render: (width) => [
193
- ...this.list.render(width),
194
- "",
195
- ...operationPaths(this.current).map((path) =>
196
- truncateToWidth(theme.fg("muted", `• ${path}`), width, "…"),
197
- ),
198
- ],
199
- invalidate: () => this.list.invalidate(),
200
- },
201
- footer: {
202
- kind: "hints",
203
- hints: [
204
- ...this.list.getKeyHints(),
205
- rawHint("ctrl+b", "batch feedback"),
206
- rawHint("ctrl+i", "item feedback"),
207
- ],
208
- },
209
- });
210
- }
211
- handleInput(data: string): void {
212
- if (!this.list.isFilterFocused()) {
213
- const selectedIds = this.selected.map((item) => item.id);
214
- if (matchesKey(data, Key.ctrl("b"))) {
215
- this.done({ kind: "feedback", scope: "batch", selectedIds });
216
- return;
217
- }
218
- if (matchesKey(data, Key.ctrl("i"))) {
219
- const current = this.list.getCurrentItem();
220
- if (current) this.done({ kind: "feedback", scope: "operation", operationId: current.id, selectedIds });
221
- return;
222
- }
223
- }
224
- this.list.handleInput(data);
225
- this.current = this.list.getCurrentItem() ?? this.current;
226
- }
227
- render(width: number): string[] {
228
- return this.panel.render(width);
229
- }
230
- invalidate(): void {
231
- this.panel.invalidate();
232
- }
233
- }
234
-
235
- function operationPaths(operation: ContextOperation): string[] {
236
- return operation.kind === "replace-file" ? [`${operation.from} → ${operation.to}`] : operation.files;
237
- }