@agentprojectcontext/apx 1.15.6 → 1.16.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.
Files changed (221) hide show
  1. package/package.json +40 -5
  2. package/src/cli/commands/log.js +113 -0
  3. package/src/cli/commands/overlay.js +253 -0
  4. package/src/cli/commands/sys.js +88 -16
  5. package/src/cli/index.js +23 -1
  6. package/src/cli/terminal-chat/renderer.js +71 -56
  7. package/src/cli-ts/commands/agent.ts +173 -0
  8. package/src/cli-ts/commands/chat.ts +119 -0
  9. package/src/cli-ts/commands/daemon.ts +112 -0
  10. package/src/cli-ts/commands/exec.ts +109 -0
  11. package/src/cli-ts/commands/mcp.ts +235 -0
  12. package/src/cli-ts/commands/session.ts +224 -0
  13. package/src/cli-ts/commands/status.ts +61 -0
  14. package/src/cli-ts/http.ts +36 -0
  15. package/src/cli-ts/index.ts +73 -0
  16. package/src/cli-ts/ui.ts +107 -0
  17. package/src/core/logging.js +81 -0
  18. package/src/daemon/api.js +58 -0
  19. package/src/daemon/engines/anthropic.js +60 -1
  20. package/src/daemon/engines/index.js +2 -1
  21. package/src/daemon/engines/ollama.js +70 -3
  22. package/src/daemon/index.js +58 -0
  23. package/src/daemon/overlay-ws.js +40 -0
  24. package/src/daemon/plugins/index.js +2 -1
  25. package/src/daemon/plugins/overlay.js +177 -0
  26. package/src/daemon/plugins/telegram.js +15 -3
  27. package/src/daemon/super-agent.js +102 -19
  28. package/src/daemon/transcription.js +262 -59
  29. package/src/daemon/whisper-server.py +57 -6
  30. package/src/overlay/index.html +44 -0
  31. package/src/overlay/main.js +480 -0
  32. package/src/overlay/package.json +3 -0
  33. package/src/overlay/preload.js +34 -0
  34. package/src/overlay/renderer.js +371 -0
  35. package/src/overlay/style.css +250 -0
  36. package/src/tui/_shims/cli-error.ts +6 -0
  37. package/src/tui/_shims/cli-logo.ts +18 -0
  38. package/src/tui/_shims/cli-ui.ts +1 -0
  39. package/src/tui/_shims/config-console-state.ts +7 -0
  40. package/src/tui/_shims/core-any.ts +30 -0
  41. package/src/tui/_shims/core-binary.ts +13 -0
  42. package/src/tui/_shims/core-flag.ts +3 -0
  43. package/src/tui/_shims/core-log.ts +14 -0
  44. package/src/tui/_shims/lsp-language.ts +1 -0
  45. package/src/tui/_shims/opencode-any.ts +135 -0
  46. package/src/tui/_shims/opencode-sdk-v2.ts +48 -0
  47. package/src/tui/_shims/plugin-tui.ts +13 -0
  48. package/src/tui/_shims/provider-provider.ts +10 -0
  49. package/src/tui/_shims/session-retry.ts +1 -0
  50. package/src/tui/_shims/session-schema.ts +15 -0
  51. package/src/tui/_shims/session-session.ts +3 -0
  52. package/src/tui/_shims/snapshot.ts +4 -0
  53. package/src/tui/_shims/tool-any.ts +18 -0
  54. package/src/tui/_shims/util-error.ts +7 -0
  55. package/src/tui/_shims/util-filesystem.ts +79 -0
  56. package/src/tui/_shims/util-format.ts +7 -0
  57. package/src/tui/_shims/util-iife.ts +3 -0
  58. package/src/tui/_shims/util-locale.ts +10 -0
  59. package/src/tui/_shims/util-process.ts +38 -0
  60. package/src/tui/app.tsx +783 -0
  61. package/src/tui/asset/charge.wav +0 -0
  62. package/src/tui/asset/pulse-a.wav +0 -0
  63. package/src/tui/asset/pulse-b.wav +0 -0
  64. package/src/tui/asset/pulse-c.wav +0 -0
  65. package/src/tui/attach.ts +100 -0
  66. package/src/tui/component/bg-pulse-render.ts +436 -0
  67. package/src/tui/component/bg-pulse.tsx +99 -0
  68. package/src/tui/component/border.tsx +21 -0
  69. package/src/tui/component/dialog-agent.tsx +31 -0
  70. package/src/tui/component/dialog-console-org.tsx +103 -0
  71. package/src/tui/component/dialog-mcp.tsx +85 -0
  72. package/src/tui/component/dialog-model.tsx +175 -0
  73. package/src/tui/component/dialog-provider.tsx +456 -0
  74. package/src/tui/component/dialog-retry-action.tsx +160 -0
  75. package/src/tui/component/dialog-session-delete-failed.tsx +99 -0
  76. package/src/tui/component/dialog-session-list.tsx +323 -0
  77. package/src/tui/component/dialog-session-rename.tsx +31 -0
  78. package/src/tui/component/dialog-skill.tsx +36 -0
  79. package/src/tui/component/dialog-stash.tsx +87 -0
  80. package/src/tui/component/dialog-status.tsx +168 -0
  81. package/src/tui/component/dialog-tag.tsx +44 -0
  82. package/src/tui/component/dialog-theme-list.tsx +50 -0
  83. package/src/tui/component/dialog-variant.tsx +39 -0
  84. package/src/tui/component/dialog-workspace-create.tsx +302 -0
  85. package/src/tui/component/dialog-workspace-file-changes.tsx +138 -0
  86. package/src/tui/component/dialog-workspace-unavailable.tsx +69 -0
  87. package/src/tui/component/error-component.tsx +92 -0
  88. package/src/tui/component/logo.tsx +896 -0
  89. package/src/tui/component/plugin-route-missing.tsx +14 -0
  90. package/src/tui/component/prompt/autocomplete.tsx +869 -0
  91. package/src/tui/component/prompt/cwd.ts +0 -0
  92. package/src/tui/component/prompt/frecency.tsx +90 -0
  93. package/src/tui/component/prompt/history.tsx +108 -0
  94. package/src/tui/component/prompt/index.tsx +1809 -0
  95. package/src/tui/component/prompt/part.ts +16 -0
  96. package/src/tui/component/prompt/stash.tsx +101 -0
  97. package/src/tui/component/prompt/traits.ts +35 -0
  98. package/src/tui/component/spinner.tsx +24 -0
  99. package/src/tui/component/startup-loading.tsx +63 -0
  100. package/src/tui/component/todo-item.tsx +32 -0
  101. package/src/tui/component/use-connected.tsx +9 -0
  102. package/src/tui/component/workspace-label.tsx +19 -0
  103. package/src/tui/config/cwd.ts +5 -0
  104. package/src/tui/config/keybind.ts +432 -0
  105. package/src/tui/config/tui-migrate.ts +154 -0
  106. package/src/tui/config/tui-schema.ts +34 -0
  107. package/src/tui/config/tui.ts +46 -0
  108. package/src/tui/context/aggregate-failures.ts +34 -0
  109. package/src/tui/context/args.tsx +15 -0
  110. package/src/tui/context/command-palette.tsx +163 -0
  111. package/src/tui/context/directory.ts +15 -0
  112. package/src/tui/context/editor-zed.ts +283 -0
  113. package/src/tui/context/editor.ts +468 -0
  114. package/src/tui/context/event-apx.ts +22 -0
  115. package/src/tui/context/event.ts +6 -0
  116. package/src/tui/context/exit.tsx +60 -0
  117. package/src/tui/context/helper.tsx +25 -0
  118. package/src/tui/context/kv.tsx +81 -0
  119. package/src/tui/context/local.tsx +608 -0
  120. package/src/tui/context/path-format.tsx +39 -0
  121. package/src/tui/context/project-apx.tsx +48 -0
  122. package/src/tui/context/project.tsx +7 -0
  123. package/src/tui/context/prompt.tsx +18 -0
  124. package/src/tui/context/route.tsx +52 -0
  125. package/src/tui/context/sdk-apx.tsx +185 -0
  126. package/src/tui/context/sdk.tsx +6 -0
  127. package/src/tui/context/sync-apx.tsx +178 -0
  128. package/src/tui/context/sync-v2.tsx +16 -0
  129. package/src/tui/context/sync.tsx +118 -0
  130. package/src/tui/context/theme/aura.json +69 -0
  131. package/src/tui/context/theme/ayu.json +80 -0
  132. package/src/tui/context/theme/carbonfox.json +248 -0
  133. package/src/tui/context/theme/catppuccin-frappe.json +230 -0
  134. package/src/tui/context/theme/catppuccin-macchiato.json +230 -0
  135. package/src/tui/context/theme/catppuccin.json +112 -0
  136. package/src/tui/context/theme/cobalt2.json +225 -0
  137. package/src/tui/context/theme/cursor.json +249 -0
  138. package/src/tui/context/theme/dracula.json +219 -0
  139. package/src/tui/context/theme/everforest.json +241 -0
  140. package/src/tui/context/theme/flexoki.json +237 -0
  141. package/src/tui/context/theme/github.json +233 -0
  142. package/src/tui/context/theme/gruvbox.json +242 -0
  143. package/src/tui/context/theme/kanagawa.json +77 -0
  144. package/src/tui/context/theme/lucent-orng.json +234 -0
  145. package/src/tui/context/theme/material.json +235 -0
  146. package/src/tui/context/theme/matrix.json +77 -0
  147. package/src/tui/context/theme/mercury.json +252 -0
  148. package/src/tui/context/theme/monokai.json +221 -0
  149. package/src/tui/context/theme/nightowl.json +221 -0
  150. package/src/tui/context/theme/nord.json +223 -0
  151. package/src/tui/context/theme/one-dark.json +84 -0
  152. package/src/tui/context/theme/opencode.json +245 -0
  153. package/src/tui/context/theme/orng.json +249 -0
  154. package/src/tui/context/theme/osaka-jade.json +93 -0
  155. package/src/tui/context/theme/palenight.json +222 -0
  156. package/src/tui/context/theme/rosepine.json +234 -0
  157. package/src/tui/context/theme/solarized.json +223 -0
  158. package/src/tui/context/theme/synthwave84.json +226 -0
  159. package/src/tui/context/theme/tokyonight.json +243 -0
  160. package/src/tui/context/theme/vercel.json +245 -0
  161. package/src/tui/context/theme/vesper.json +218 -0
  162. package/src/tui/context/theme/zenburn.json +223 -0
  163. package/src/tui/context/theme.tsx +1247 -0
  164. package/src/tui/context/tui-config.tsx +9 -0
  165. package/src/tui/event.ts +16 -0
  166. package/src/tui/feature-plugins/home/footer.tsx +94 -0
  167. package/src/tui/feature-plugins/home/tips-view.tsx +166 -0
  168. package/src/tui/feature-plugins/home/tips.tsx +59 -0
  169. package/src/tui/feature-plugins/sidebar/context.tsx +65 -0
  170. package/src/tui/feature-plugins/sidebar/files.tsx +63 -0
  171. package/src/tui/feature-plugins/sidebar/footer.tsx +94 -0
  172. package/src/tui/feature-plugins/sidebar/lsp.tsx +65 -0
  173. package/src/tui/feature-plugins/sidebar/mcp.tsx +97 -0
  174. package/src/tui/feature-plugins/sidebar/todo.tsx +49 -0
  175. package/src/tui/feature-plugins/system/plugins.tsx +269 -0
  176. package/src/tui/feature-plugins/system/session-v2.tsx +1143 -0
  177. package/src/tui/feature-plugins/system/which-key.tsx +608 -0
  178. package/src/tui/keymap.tsx +166 -0
  179. package/src/tui/layer.ts +6 -0
  180. package/src/tui/plugin/api.tsx +381 -0
  181. package/src/tui/plugin/command-shim.ts +109 -0
  182. package/src/tui/plugin/internal.ts +33 -0
  183. package/src/tui/plugin/runtime.ts +1069 -0
  184. package/src/tui/plugin/slots.tsx +60 -0
  185. package/src/tui/routes/home.tsx +96 -0
  186. package/src/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
  187. package/src/tui/routes/session/dialog-message.tsx +108 -0
  188. package/src/tui/routes/session/dialog-subagent.tsx +26 -0
  189. package/src/tui/routes/session/dialog-timeline.tsx +47 -0
  190. package/src/tui/routes/session/footer.tsx +91 -0
  191. package/src/tui/routes/session/index.tsx +188 -0
  192. package/src/tui/routes/session/permission.tsx +722 -0
  193. package/src/tui/routes/session/question.tsx +490 -0
  194. package/src/tui/routes/session/sidebar.tsx +102 -0
  195. package/src/tui/routes/session/subagent-footer.tsx +133 -0
  196. package/src/tui/run.ts +84 -0
  197. package/src/tui/thread.ts +261 -0
  198. package/src/tui/tsconfig.json +40 -0
  199. package/src/tui/ui/dialog-alert.tsx +66 -0
  200. package/src/tui/ui/dialog-confirm.tsx +108 -0
  201. package/src/tui/ui/dialog-export-options.tsx +217 -0
  202. package/src/tui/ui/dialog-help.tsx +40 -0
  203. package/src/tui/ui/dialog-prompt.tsx +101 -0
  204. package/src/tui/ui/dialog-select.tsx +553 -0
  205. package/src/tui/ui/dialog.tsx +211 -0
  206. package/src/tui/ui/link.tsx +34 -0
  207. package/src/tui/ui/spinner.ts +368 -0
  208. package/src/tui/ui/toast.tsx +111 -0
  209. package/src/tui/util/clipboard.ts +217 -0
  210. package/src/tui/util/editor.ts +37 -0
  211. package/src/tui/util/model.ts +23 -0
  212. package/src/tui/util/provider-origin.ts +7 -0
  213. package/src/tui/util/revert-diff.ts +18 -0
  214. package/src/tui/util/scroll.ts +25 -0
  215. package/src/tui/util/selection.ts +65 -0
  216. package/src/tui/util/signal.ts +41 -0
  217. package/src/tui/util/sound.ts +156 -0
  218. package/src/tui/util/transcript.ts +112 -0
  219. package/src/tui/validate-session.ts +29 -0
  220. package/src/tui/win32.ts +130 -0
  221. package/src/tui/worker.ts +104 -0
@@ -0,0 +1,553 @@
1
+ import {
2
+ InputRenderable,
3
+ RGBA,
4
+ ScrollBoxRenderable,
5
+ TextAttributes,
6
+ type KeyEvent,
7
+ type Renderable,
8
+ } from "@opentui/core"
9
+ import type { Binding } from "@opentui/keymap"
10
+ import { useTheme, selectedForeground } from "@tui/context/theme"
11
+ import { entries, filter, flatMap, groupBy, pipe } from "remeda"
12
+ import { batch, createEffect, createMemo, For, Show, type JSX, on } from "solid-js"
13
+ import { createStore } from "solid-js/store"
14
+ import { useTerminalDimensions } from "@opentui/solid"
15
+ import * as fuzzysort from "fuzzysort"
16
+ import { isDeepEqual } from "remeda"
17
+ import { useDialog, type DialogContext } from "@tui/ui/dialog"
18
+ import { Locale } from "@/util/locale"
19
+ import { getScrollAcceleration } from "../util/scroll"
20
+ import { useTuiConfig } from "../context/tui-config"
21
+ import { formatKeyBindings, useBindings, useKeymapSelector } from "../keymap"
22
+
23
+ export interface DialogSelectProps<T> {
24
+ title: string
25
+ placeholder?: string
26
+ options: DialogSelectOption<T>[]
27
+ flat?: boolean
28
+ ref?: (ref: DialogSelectRef<T>) => void
29
+ onMove?: (option: DialogSelectOption<T>) => void
30
+ onFilter?: (query: string) => void
31
+ onSelect?: (option: DialogSelectOption<T>) => void
32
+ skipFilter?: boolean
33
+ renderFilter?: boolean
34
+ actions?: {
35
+ command: string
36
+ title: string
37
+ side?: "left" | "right"
38
+ disabled?: boolean
39
+ onTrigger: (option: DialogSelectOption<T>) => void
40
+ }[]
41
+ bindings?: readonly Binding<Renderable, KeyEvent>[]
42
+ current?: T
43
+ }
44
+
45
+ export interface DialogSelectOption<T = any> {
46
+ title: string
47
+ value: T
48
+ description?: string
49
+ footer?: JSX.Element | string
50
+ category?: string
51
+ categoryView?: JSX.Element
52
+ disabled?: boolean
53
+ bg?: RGBA
54
+ gutter?: () => JSX.Element
55
+ margin?: JSX.Element
56
+ onSelect?: (ctx: DialogContext) => void
57
+ }
58
+
59
+ export type DialogSelectRef<T> = {
60
+ filter: string
61
+ filtered: DialogSelectOption<T>[]
62
+ }
63
+
64
+ export function DialogSelect<T>(props: DialogSelectProps<T>) {
65
+ const dialog = useDialog()
66
+ const { theme } = useTheme()
67
+ const tuiConfig = useTuiConfig()
68
+ const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
69
+
70
+ const [store, setStore] = createStore({
71
+ selected: 0,
72
+ filter: "",
73
+ input: "keyboard" as "keyboard" | "mouse",
74
+ })
75
+
76
+ createEffect(
77
+ on(
78
+ () => props.current,
79
+ (current) => {
80
+ if (current) {
81
+ const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, current))
82
+ if (currentIndex >= 0) {
83
+ setStore("selected", currentIndex)
84
+ }
85
+ }
86
+ },
87
+ ),
88
+ )
89
+
90
+ let input: InputRenderable
91
+
92
+ const actions = createMemo(() => props.actions ?? [])
93
+ const actionBindings = useKeymapSelector((keymap) =>
94
+ keymap.getCommandBindings({
95
+ visibility: "registered",
96
+ commands: actions().map((item) => item.command),
97
+ }),
98
+ )
99
+
100
+ const actionLabels = createMemo(() => {
101
+ const labels = new Map<string, string>()
102
+
103
+ for (const action of actions()) {
104
+ const label = formatKeyBindings(actionBindings().get(action.command), tuiConfig)
105
+ if (label) labels.set(action.command, label)
106
+ }
107
+
108
+ return labels
109
+ })
110
+
111
+ const filtered = createMemo(() => {
112
+ if (props.skipFilter || props.renderFilter === false) return props.options.filter((x) => x.disabled !== true)
113
+ const needle = store.filter.toLowerCase()
114
+ const options = pipe(
115
+ props.options,
116
+ filter((x) => x.disabled !== true),
117
+ )
118
+ if (!needle) return options
119
+
120
+ // prioritize title matches (weight: 2) over category matches (weight: 1).
121
+ // users typically search by the item name, and not its category.
122
+ const result = fuzzysort
123
+ .go(needle, options, {
124
+ keys: ["title", "category"],
125
+ scoreFn: (r) => r[0].score * 2 + r[1].score,
126
+ })
127
+ .map((x) => x.obj)
128
+
129
+ return result
130
+ })
131
+
132
+ // When the filter changes due to how TUI works, the mousemove might still be triggered
133
+ // via a synthetic event as the layout moves underneath the cursor. This is a workaround to make sure the input mode remains keyboard
134
+ // that the mouseover event doesn't trigger when filtering.
135
+ createEffect(() => {
136
+ filtered()
137
+ setStore("input", "keyboard")
138
+ })
139
+
140
+ const flatten = createMemo(() => props.flat && store.filter.length > 0)
141
+
142
+ const grouped = createMemo<[string, DialogSelectOption<T>[]][]>(() => {
143
+ if (flatten()) return [["", filtered()]]
144
+ const result = pipe(
145
+ filtered(),
146
+ groupBy((x) => x.category ?? ""),
147
+ // mapValues((x) => x.sort((a, b) => a.title.localeCompare(b.title))),
148
+ entries(),
149
+ )
150
+ return result
151
+ })
152
+
153
+ const flat = createMemo(() => {
154
+ return pipe(
155
+ grouped(),
156
+ flatMap(([_, options]) => options),
157
+ )
158
+ })
159
+
160
+ const rows = createMemo(() => {
161
+ const headers = grouped().reduce((acc, [category], i) => {
162
+ if (!category) return acc
163
+ return acc + (i > 0 ? 2 : 1)
164
+ }, 0)
165
+ return flat().length + headers
166
+ })
167
+
168
+ const dimensions = useTerminalDimensions()
169
+ const height = createMemo(() => Math.min(rows(), Math.floor(dimensions().height / 2) - 6))
170
+
171
+ const selected = createMemo(() => flat()[store.selected])
172
+
173
+ createEffect(
174
+ on([() => store.filter, () => props.current], ([filter, current]) => {
175
+ setTimeout(() => {
176
+ if (filter.length > 0) {
177
+ moveTo(0, true)
178
+ } else if (current) {
179
+ const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, current))
180
+ if (currentIndex >= 0) {
181
+ moveTo(currentIndex, true)
182
+ }
183
+ }
184
+ }, 0)
185
+ }),
186
+ )
187
+
188
+ function move(direction: number) {
189
+ if (flat().length === 0) return
190
+ let next = store.selected + direction
191
+ if (next < 0) next = flat().length - 1
192
+ if (next >= flat().length) next = 0
193
+ moveTo(next, true)
194
+ }
195
+
196
+ function moveTo(next: number, center = false) {
197
+ setStore("selected", next)
198
+ const option = selected()
199
+ if (option) props.onMove?.(option)
200
+ if (!scroll) return
201
+ const target = scroll.getChildren().find((child: { id?: string }) => {
202
+ return child.id === JSON.stringify(selected()?.value)
203
+ })
204
+ if (!target) return
205
+ const y = target.y - scroll.y
206
+ if (center) {
207
+ const centerOffset = Math.floor(scroll.height / 2)
208
+ scroll.scrollBy(y - centerOffset)
209
+ } else {
210
+ if (y >= scroll.height) {
211
+ scroll.scrollBy(y - scroll.height + 1)
212
+ }
213
+ if (y < 0) {
214
+ scroll.scrollBy(y)
215
+ if (isDeepEqual(flat()[0].value, selected()?.value)) {
216
+ scroll.scrollTo(0)
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ function submit() {
223
+ setStore("input", "keyboard")
224
+ const option = selected()
225
+ if (!option) return
226
+ option.onSelect?.(dialog)
227
+ props.onSelect?.(option)
228
+ }
229
+
230
+ useBindings(() => {
231
+ const enabledActions = actions().filter((item) => !item.disabled)
232
+
233
+ return {
234
+ commands: [
235
+ {
236
+ name: "dialog.select.prev",
237
+ title: "Previous item",
238
+ category: "Dialog",
239
+ run() {
240
+ setStore("input", "keyboard")
241
+ move(-1)
242
+ },
243
+ },
244
+ {
245
+ name: "dialog.select.next",
246
+ title: "Next item",
247
+ category: "Dialog",
248
+ run() {
249
+ setStore("input", "keyboard")
250
+ move(1)
251
+ },
252
+ },
253
+ {
254
+ name: "dialog.select.page_up",
255
+ title: "Page up",
256
+ category: "Dialog",
257
+ run() {
258
+ setStore("input", "keyboard")
259
+ move(-10)
260
+ },
261
+ },
262
+ {
263
+ name: "dialog.select.page_down",
264
+ title: "Page down",
265
+ category: "Dialog",
266
+ run() {
267
+ setStore("input", "keyboard")
268
+ move(10)
269
+ },
270
+ },
271
+ {
272
+ name: "dialog.select.home",
273
+ title: "First item",
274
+ category: "Dialog",
275
+ run() {
276
+ setStore("input", "keyboard")
277
+ moveTo(0)
278
+ },
279
+ },
280
+ {
281
+ name: "dialog.select.end",
282
+ title: "Last item",
283
+ category: "Dialog",
284
+ run() {
285
+ setStore("input", "keyboard")
286
+ moveTo(flat().length - 1)
287
+ },
288
+ },
289
+ {
290
+ name: "dialog.select.submit",
291
+ title: "Select item",
292
+ category: "Dialog",
293
+ run: submit,
294
+ },
295
+ ...enabledActions.map((item) => ({
296
+ name: item.command,
297
+ title: item.title,
298
+ category: "Dialog",
299
+ run() {
300
+ setStore("input", "keyboard")
301
+ const option = selected()
302
+ if (!option) return
303
+ item.onTrigger(option)
304
+ },
305
+ })),
306
+ ],
307
+ bindings: [
308
+ ...tuiConfig.keybinds.gather("dialog.select", [
309
+ "dialog.select.prev",
310
+ "dialog.select.next",
311
+ "dialog.select.page_up",
312
+ "dialog.select.page_down",
313
+ "dialog.select.home",
314
+ "dialog.select.end",
315
+ "dialog.select.submit",
316
+ ]),
317
+ ...enabledActions.flatMap((item) => tuiConfig.keybinds.get(item.command)),
318
+ ...(props.bindings ?? []).filter((binding) => {
319
+ if (typeof binding.cmd !== "string") return true
320
+ return enabledActions.some((item) => item.command === binding.cmd)
321
+ }),
322
+ ],
323
+ }
324
+ })
325
+
326
+ let scroll: ScrollBoxRenderable | undefined
327
+ const ref: DialogSelectRef<T> = {
328
+ get filter() {
329
+ return store.filter
330
+ },
331
+ get filtered() {
332
+ return filtered()
333
+ },
334
+ }
335
+ props.ref?.(ref)
336
+
337
+ const visibleActions = createMemo(() =>
338
+ actions()
339
+ .map((item) => ({ ...item, label: actionLabels().get(item.command) ?? "" }))
340
+ .filter((item) => !item.disabled && item.label),
341
+ )
342
+ const left = createMemo(() => visibleActions().filter((item) => item.side !== "right"))
343
+ const right = createMemo(() => visibleActions().filter((item) => item.side === "right"))
344
+
345
+ return (
346
+ <box gap={1} paddingBottom={1}>
347
+ <box paddingLeft={4} paddingRight={4}>
348
+ <box flexDirection="row" justifyContent="space-between">
349
+ <text fg={theme.text} attributes={TextAttributes.BOLD}>
350
+ {props.title}
351
+ </text>
352
+ <text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
353
+ esc
354
+ </text>
355
+ </box>
356
+ <Show when={props.renderFilter !== false}>
357
+ <box paddingTop={1}>
358
+ <input
359
+ onInput={(e) => {
360
+ batch(() => {
361
+ setStore("filter", e)
362
+ props.onFilter?.(e)
363
+ })
364
+ }}
365
+ focusedBackgroundColor={theme.backgroundPanel}
366
+ cursorColor={theme.primary}
367
+ focusedTextColor={theme.textMuted}
368
+ ref={(r) => {
369
+ input = r
370
+ input.traits = { status: "FILTER" }
371
+ setTimeout(() => {
372
+ if (!input) return
373
+ if (input.isDestroyed) return
374
+ input.focus()
375
+ }, 1)
376
+ }}
377
+ placeholder={props.placeholder ?? "Search"}
378
+ placeholderColor={theme.textMuted}
379
+ />
380
+ </box>
381
+ </Show>
382
+ </box>
383
+ <Show
384
+ when={grouped().length > 0}
385
+ fallback={
386
+ <box paddingLeft={4} paddingRight={4} paddingTop={1}>
387
+ <text fg={theme.textMuted}>No results found</text>
388
+ </box>
389
+ }
390
+ >
391
+ <scrollbox
392
+ paddingLeft={1}
393
+ paddingRight={1}
394
+ scrollbarOptions={{ visible: false }}
395
+ scrollAcceleration={scrollAcceleration()}
396
+ ref={(r: ScrollBoxRenderable) => (scroll = r)}
397
+ maxHeight={height()}
398
+ >
399
+ <For each={grouped()}>
400
+ {([category, options], index) => (
401
+ <>
402
+ <Show when={category}>
403
+ <box paddingTop={index() > 0 ? 1 : 0} paddingLeft={3}>
404
+ <Show
405
+ when={options[0]?.categoryView}
406
+ fallback={
407
+ <text fg={theme.accent} attributes={TextAttributes.BOLD}>
408
+ {category}
409
+ </text>
410
+ }
411
+ >
412
+ {options[0]?.categoryView}
413
+ </Show>
414
+ </box>
415
+ </Show>
416
+ <For each={options}>
417
+ {(option) => {
418
+ const active = createMemo(() => isDeepEqual(option.value, selected()?.value))
419
+ const current = createMemo(() => isDeepEqual(option.value, props.current))
420
+ return (
421
+ <box
422
+ id={JSON.stringify(option.value)}
423
+ flexDirection="row"
424
+ position="relative"
425
+ onMouseMove={() => {
426
+ setStore("input", "mouse")
427
+ }}
428
+ onMouseUp={() => {
429
+ option.onSelect?.(dialog)
430
+ props.onSelect?.(option)
431
+ }}
432
+ onMouseOver={() => {
433
+ if (store.input !== "mouse") return
434
+ const index = flat().findIndex((x) => isDeepEqual(x.value, option.value))
435
+ if (index === -1) return
436
+ moveTo(index)
437
+ }}
438
+ onMouseDown={() => {
439
+ const index = flat().findIndex((x) => isDeepEqual(x.value, option.value))
440
+ if (index === -1) return
441
+ moveTo(index)
442
+ }}
443
+ backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)}
444
+ paddingLeft={current() || option.gutter ? 1 : 3}
445
+ paddingRight={3}
446
+ gap={1}
447
+ >
448
+ <Show when={!current() && option.margin}>
449
+ <box position="absolute" left={1} flexShrink={0}>
450
+ {option.margin}
451
+ </box>
452
+ </Show>
453
+ <Option
454
+ title={option.title}
455
+ footer={flatten() ? (option.category ?? option.footer) : option.footer}
456
+ description={option.description !== category ? option.description : undefined}
457
+ active={active()}
458
+ current={current()}
459
+ gutter={option.gutter}
460
+ />
461
+ </box>
462
+ )
463
+ }}
464
+ </For>
465
+ </>
466
+ )}
467
+ </For>
468
+ </scrollbox>
469
+ </Show>
470
+ <Show when={visibleActions().length} fallback={<box flexShrink={0} />}>
471
+ <box
472
+ paddingRight={2}
473
+ paddingLeft={4}
474
+ flexDirection="row"
475
+ justifyContent="space-between"
476
+ flexShrink={0}
477
+ paddingTop={1}
478
+ >
479
+ <box flexDirection="row" gap={2}>
480
+ <For each={left()}>
481
+ {(item) => (
482
+ <text>
483
+ <span style={{ fg: theme.text }}>
484
+ <b>{item.title}</b>{" "}
485
+ </span>
486
+ <span style={{ fg: theme.textMuted }}>{item.label}</span>
487
+ </text>
488
+ )}
489
+ </For>
490
+ </box>
491
+ <box flexDirection="row" gap={2}>
492
+ <For each={right()}>
493
+ {(item) => (
494
+ <text>
495
+ <span style={{ fg: theme.text }}>
496
+ <b>{item.title}</b>{" "}
497
+ </span>
498
+ <span style={{ fg: theme.textMuted }}>{item.label}</span>
499
+ </text>
500
+ )}
501
+ </For>
502
+ </box>
503
+ </box>
504
+ </Show>
505
+ </box>
506
+ )
507
+ }
508
+
509
+ function Option(props: {
510
+ title: string
511
+ description?: string
512
+ active?: boolean
513
+ current?: boolean
514
+ footer?: JSX.Element | string
515
+ gutter?: () => JSX.Element
516
+ onMouseOver?: () => void
517
+ }) {
518
+ const { theme } = useTheme()
519
+ const fg = selectedForeground(theme)
520
+
521
+ return (
522
+ <>
523
+ <Show when={props.current}>
524
+ <text flexShrink={0} fg={props.active ? fg : props.current ? theme.primary : theme.text} marginRight={0}>
525
+
526
+ </text>
527
+ </Show>
528
+ <Show when={!props.current && props.gutter}>
529
+ <box flexShrink={0} marginRight={0}>
530
+ {props.gutter?.()}
531
+ </box>
532
+ </Show>
533
+ <text
534
+ flexGrow={1}
535
+ fg={props.active ? fg : props.current ? theme.primary : theme.text}
536
+ attributes={props.active ? TextAttributes.BOLD : undefined}
537
+ overflow="hidden"
538
+ wrapMode="none"
539
+ paddingLeft={3}
540
+ >
541
+ {Locale.truncate(props.title, 61)}
542
+ <Show when={props.description}>
543
+ <span style={{ fg: props.active ? fg : theme.textMuted }}> {props.description}</span>
544
+ </Show>
545
+ </text>
546
+ <Show when={props.footer}>
547
+ <box flexShrink={0}>
548
+ <text fg={props.active ? fg : theme.textMuted}>{props.footer}</text>
549
+ </box>
550
+ </Show>
551
+ </>
552
+ )
553
+ }