@shanepadgett/tau-tui 0.29.0 → 0.30.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.29.0",
3
+ "version": "0.30.0",
4
4
  "description": "Shared TUI components and helpers for Tau Agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,5 +1,13 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import { type Component, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
+ import {
3
+ type Component,
4
+ getKeybindings,
5
+ truncateToWidth,
6
+ type TUI,
7
+ visibleWidth,
8
+ wrapTextWithAnsi,
9
+ } from "@earendil-works/pi-tui";
10
+ import { bindingHint } from "./key-hints.ts";
3
11
  import { formatAge, preview } from "./text.ts";
4
12
  import {
5
13
  SelectableList,
@@ -7,7 +15,7 @@ import {
7
15
  type SelectableListItem,
8
16
  type SelectableListRowState,
9
17
  } from "./selectable-list.ts";
10
- import { ToolPanel } from "./tool-panel.ts";
18
+ import { ToolPanel, type ToolPanelConfig } from "./tool-panel.ts";
11
19
 
12
20
  export interface TextRecordSelectItem extends SelectableListItem {
13
21
  text: string;
@@ -19,27 +27,38 @@ export type TextRecordSelectResult<T extends TextRecordSelectItem> =
19
27
  | { kind: "primary"; item: T }
20
28
  | { kind: "action"; actionId: string; item: T };
21
29
 
22
- export interface TextRecordSelectPanelConfig {
30
+ export interface TextRecordDestructiveAction<T extends TextRecordSelectItem> extends SelectableListAction {
31
+ confirmLabel(item: T): string;
32
+ runningLabel: string;
33
+ onConfirm(item: T): Promise<readonly T[]>;
34
+ onError(error: unknown): void;
35
+ }
36
+
37
+ export interface TextRecordSelectPanelConfig<T extends TextRecordSelectItem> {
23
38
  title: string;
24
39
  path: string;
25
40
  emptyMessage: string;
26
41
  primaryLabel: string;
27
42
  actions: readonly SelectableListAction[];
43
+ destructiveAction: TextRecordDestructiveAction<T>;
28
44
  expandActiveItem: boolean;
29
45
  }
30
46
 
31
47
  export function createTextRecordSelectPanel<T extends TextRecordSelectItem>(
48
+ tui: TUI,
32
49
  theme: Theme,
33
50
  items: readonly T[],
34
- config: TextRecordSelectPanelConfig,
51
+ config: TextRecordSelectPanelConfig<T>,
35
52
  done: (result: TextRecordSelectResult<T>) => void,
36
53
  ): Component {
54
+ let currentItems = items;
55
+ let destructiveState: { item: T; running: boolean } | undefined;
37
56
  const list = new SelectableList(theme, {
38
57
  items,
39
58
  emptyMessage: config.emptyMessage,
40
59
  selection: { kind: "single", primaryLabel: config.primaryLabel },
41
60
  filter: { searchText: (item) => item.text },
42
- actions: config.actions,
61
+ actions: [...config.actions, config.destructiveAction],
43
62
  cancelLabel: "cancel",
44
63
  maxVisible: 12,
45
64
  renderItem: (item, state, width) => renderTextRecord(theme, item, state, config.expandActiveItem, width),
@@ -50,20 +69,69 @@ export function createTextRecordSelectPanel<T extends TextRecordSelectItem>(
50
69
  }
51
70
  const item = result.items[0];
52
71
  if (!item) return;
72
+ if (result.kind === "action" && result.actionId === config.destructiveAction.id) {
73
+ destructiveState = { item, running: false };
74
+ syncPanel();
75
+ return;
76
+ }
53
77
  done(
54
78
  result.kind === "primary" ? { kind: "primary", item } : { kind: "action", actionId: result.actionId, item },
55
79
  );
56
80
  },
57
81
  });
58
- const panel = new ToolPanel(theme, {
82
+ const panelConfig: ToolPanelConfig = {
59
83
  title: config.title,
60
84
  secondary: `${items.length} total · ${config.path}`,
61
85
  body: list,
62
86
  footer: { kind: "hints", hints: list.getKeyHints() },
63
- });
87
+ };
88
+ const panel = new ToolPanel(theme, panelConfig);
89
+
90
+ function syncPanel(): void {
91
+ panelConfig.secondary = `${currentItems.length} total · ${config.path}`;
92
+ panelConfig.footer = destructiveState
93
+ ? destructiveState.running
94
+ ? { kind: "infoAck", message: config.destructiveAction.runningLabel, hints: [] }
95
+ : {
96
+ kind: "destructiveAck",
97
+ message: config.destructiveAction.confirmLabel(destructiveState.item),
98
+ hints: [bindingHint("tui.select.confirm", "confirm"), bindingHint("tui.select.cancel", "cancel")],
99
+ }
100
+ : { kind: "hints", hints: list.getKeyHints() };
101
+ tui.requestRender();
102
+ }
103
+
104
+ async function confirmDestructive(item: T): Promise<void> {
105
+ try {
106
+ currentItems = await config.destructiveAction.onConfirm(item);
107
+ list.setItems(currentItems);
108
+ } catch (error) {
109
+ config.destructiveAction.onError(error);
110
+ }
111
+ destructiveState = undefined;
112
+ syncPanel();
113
+ }
64
114
 
65
115
  return {
66
- handleInput: (data) => list.handleInput(data),
116
+ handleInput: (data) => {
117
+ if (!destructiveState) {
118
+ list.handleInput(data);
119
+ return;
120
+ }
121
+ if (destructiveState.running) return;
122
+ const keybindings = getKeybindings();
123
+ if (keybindings.matches(data, "tui.select.confirm")) {
124
+ const item = destructiveState.item;
125
+ destructiveState.running = true;
126
+ syncPanel();
127
+ void confirmDestructive(item);
128
+ return;
129
+ }
130
+ if (keybindings.matches(data, "tui.select.cancel")) {
131
+ destructiveState = undefined;
132
+ syncPanel();
133
+ }
134
+ },
67
135
  render: (width) => panel.render(width),
68
136
  invalidate: () => panel.invalidate(),
69
137
  };