@shanepadgett/tau-tui 0.33.0 → 0.34.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shanepadgett/tau-tui",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "Shared TUI components and helpers for Tau Agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { editorTheme } from "./editor-theme.ts";
2
2
  export { bindingHint, bindingsHint, rawHint, type ToolKeyHint } from "./key-hints.ts";
3
3
  export { Marker, type MarkerState } from "./marker.ts";
4
+ export { ScrollableMarkdown } from "./scrollable-markdown.ts";
4
5
  export {
5
6
  SelectableList,
6
7
  type SelectableListActionResult,
@@ -0,0 +1,37 @@
1
+ import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
2
+ import { type Component, Markdown, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
3
+
4
+ /**
5
+ * Scrolling markdown body for a `ToolPanel`. The panel owns key handling and
6
+ * calls `scroll`; this component owns the viewport window.
7
+ */
8
+ export class ScrollableMarkdown implements Component {
9
+ private readonly tui: TUI;
10
+ private readonly markdown: Markdown;
11
+ private readonly chromeLines: number;
12
+ private offset = 0;
13
+ private maxOffset = 0;
14
+
15
+ constructor(tui: TUI, content: string, chromeLines: number) {
16
+ this.tui = tui;
17
+ this.markdown = new Markdown(content, 0, 0, getMarkdownTheme());
18
+ this.chromeLines = chromeLines;
19
+ }
20
+
21
+ scroll(delta: number): void {
22
+ this.offset = Math.max(0, Math.min(this.maxOffset, this.offset + delta));
23
+ this.tui.requestRender();
24
+ }
25
+
26
+ render(width: number): string[] {
27
+ const lines = this.markdown.render(width);
28
+ const height = Math.max(4, Math.floor(this.tui.terminal.rows * 0.9) - this.chromeLines);
29
+ this.maxOffset = Math.max(0, lines.length - height);
30
+ this.offset = Math.min(this.offset, this.maxOffset);
31
+ return lines.slice(this.offset, this.offset + height).map((line) => truncateToWidth(line, width, ""));
32
+ }
33
+
34
+ invalidate(): void {
35
+ this.markdown.invalidate();
36
+ }
37
+ }
@@ -183,61 +183,62 @@ export class SelectableList<T extends SelectableListItem> implements Component,
183
183
  ];
184
184
  }
185
185
 
186
- handleInput(data: string): void {
186
+ private handleMultiFilterKey(data: string): boolean {
187
+ if (this.config.selection.kind !== "multi" || !this.filterActive) return false;
187
188
  const keybindings = getKeybindings();
188
- if (this.config.selection.kind === "multi" && this.filterActive && matchesKey(data, Key.ctrl("c"))) {
189
+ if (matchesKey(data, Key.ctrl("c"))) {
189
190
  this.clearFilter();
190
- return;
191
+ return true;
191
192
  }
192
- if (
193
- this.config.selection.kind === "multi" &&
194
- this.filterActive &&
195
- keybindings.matches(data, "tui.select.cancel")
196
- ) {
193
+ if (keybindings.matches(data, "tui.select.cancel")) {
197
194
  this.clearFilterFocus();
198
- return;
195
+ return true;
199
196
  }
200
- if (
201
- this.config.selection.kind === "multi" &&
202
- this.filterActive &&
203
- keybindings.matches(data, "tui.select.confirm")
204
- ) {
197
+ if (keybindings.matches(data, "tui.select.confirm")) {
205
198
  this.blurFilter();
206
- return;
199
+ return true;
200
+ }
201
+ return false;
202
+ }
203
+
204
+ private handleMultiFilterMode(data: string): boolean {
205
+ if (this.config.selection.kind !== "multi") return false;
206
+ if (this.config.filter && !this.filterActive && matchesKey(data, Key.slash)) {
207
+ this.activateFilter();
208
+ return true;
209
+ }
210
+ if (this.filterActive) {
211
+ this.handleFilterInput(data);
212
+ return true;
213
+ }
214
+ return false;
215
+ }
216
+
217
+ private handleConfiguredAction(data: string): boolean {
218
+ for (const action of this.config.actions) {
219
+ if (!matchesKey(data, action.key)) continue;
220
+ this.runAction(action);
221
+ return true;
207
222
  }
223
+ return false;
224
+ }
225
+
226
+ handleInput(data: string): void {
227
+ if (this.handleMultiFilterKey(data)) return;
228
+
229
+ const keybindings = getKeybindings();
208
230
  if (keybindings.matches(data, "tui.select.cancel")) {
209
231
  this.config.onResult({ kind: "cancel" });
210
232
  return;
211
233
  }
212
-
213
234
  if (keybindings.matches(data, "tui.select.confirm")) {
214
235
  this.primary();
215
236
  return;
216
237
  }
217
-
218
238
  if (this.handleListKey(data)) return;
219
239
  if (this.config.selection.kind === "multi" && this.handleMultiSelectKey(data)) return;
220
- if (
221
- this.config.selection.kind === "multi" &&
222
- this.config.filter &&
223
- !this.filterActive &&
224
- matchesKey(data, Key.slash)
225
- ) {
226
- this.activateFilter();
227
- return;
228
- }
229
- if (this.config.selection.kind === "multi" && this.filterActive) {
230
- this.handleFilterInput(data);
231
- return;
232
- }
233
-
234
- for (const action of this.config.actions) {
235
- if (matchesKey(data, action.key)) {
236
- this.runAction(action);
237
- return;
238
- }
239
- }
240
-
240
+ if (this.handleMultiFilterMode(data)) return;
241
+ if (this.handleConfiguredAction(data)) return;
241
242
  if (this.config.filter && this.config.selection.kind === "single") this.handleFilterInput(data);
242
243
  }
243
244