lonny-agent 0.1.2 → 0.1.8

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 (216) hide show
  1. package/.kilo/plans/1780064105789-mighty-pixel.md +171 -0
  2. package/.lonny/fix-new-command-session-cleanup.md +65 -0
  3. package/.lonny/tui-autocomplete-above.md +62 -0
  4. package/dist/agent/__tests__/compaction.test.js +6 -4
  5. package/dist/agent/__tests__/compaction.test.js.map +1 -1
  6. package/dist/agent/llm.d.ts +1 -1
  7. package/dist/agent/llm.d.ts.map +1 -1
  8. package/dist/agent/prompt-builder.d.ts.map +1 -1
  9. package/dist/agent/prompt-builder.js +26 -8
  10. package/dist/agent/prompt-builder.js.map +1 -1
  11. package/dist/agent/providers/anthropic.d.ts +1 -1
  12. package/dist/agent/providers/anthropic.d.ts.map +1 -1
  13. package/dist/agent/providers/anthropic.js +2 -2
  14. package/dist/agent/providers/anthropic.js.map +1 -1
  15. package/dist/agent/providers/google.d.ts +1 -1
  16. package/dist/agent/providers/google.d.ts.map +1 -1
  17. package/dist/agent/providers/google.js +2 -1
  18. package/dist/agent/providers/google.js.map +1 -1
  19. package/dist/agent/providers/ollama.d.ts +1 -1
  20. package/dist/agent/providers/ollama.d.ts.map +1 -1
  21. package/dist/agent/providers/ollama.js +2 -1
  22. package/dist/agent/providers/ollama.js.map +1 -1
  23. package/dist/agent/providers/openai.d.ts +1 -1
  24. package/dist/agent/providers/openai.d.ts.map +1 -1
  25. package/dist/agent/providers/openai.js +2 -2
  26. package/dist/agent/providers/openai.js.map +1 -1
  27. package/dist/agent/session.d.ts +1 -0
  28. package/dist/agent/session.d.ts.map +1 -1
  29. package/dist/agent/session.js +8 -2
  30. package/dist/agent/session.js.map +1 -1
  31. package/dist/pi-tui/autocomplete.d.ts +54 -0
  32. package/dist/pi-tui/autocomplete.d.ts.map +1 -0
  33. package/dist/pi-tui/autocomplete.js +636 -0
  34. package/dist/pi-tui/autocomplete.js.map +1 -0
  35. package/dist/pi-tui/components/box.d.ts +22 -0
  36. package/dist/pi-tui/components/box.d.ts.map +1 -0
  37. package/dist/pi-tui/components/box.js +104 -0
  38. package/dist/pi-tui/components/box.js.map +1 -0
  39. package/dist/pi-tui/components/cancellable-loader.d.ts +22 -0
  40. package/dist/pi-tui/components/cancellable-loader.d.ts.map +1 -0
  41. package/dist/pi-tui/components/cancellable-loader.js +35 -0
  42. package/dist/pi-tui/components/cancellable-loader.js.map +1 -0
  43. package/dist/pi-tui/components/editor.d.ts +249 -0
  44. package/dist/pi-tui/components/editor.d.ts.map +1 -0
  45. package/dist/pi-tui/components/editor.js +1883 -0
  46. package/dist/pi-tui/components/editor.js.map +1 -0
  47. package/dist/pi-tui/components/image.d.ts +28 -0
  48. package/dist/pi-tui/components/image.d.ts.map +1 -0
  49. package/dist/pi-tui/components/image.js +90 -0
  50. package/dist/pi-tui/components/image.js.map +1 -0
  51. package/dist/pi-tui/components/input.d.ts +37 -0
  52. package/dist/pi-tui/components/input.d.ts.map +1 -0
  53. package/dist/pi-tui/components/input.js +382 -0
  54. package/dist/pi-tui/components/input.js.map +1 -0
  55. package/dist/pi-tui/components/loader.d.ts +31 -0
  56. package/dist/pi-tui/components/loader.d.ts.map +1 -0
  57. package/dist/pi-tui/components/loader.js +70 -0
  58. package/dist/pi-tui/components/loader.js.map +1 -0
  59. package/dist/pi-tui/components/markdown.d.ts +96 -0
  60. package/dist/pi-tui/components/markdown.d.ts.map +1 -0
  61. package/dist/pi-tui/components/markdown.js +647 -0
  62. package/dist/pi-tui/components/markdown.js.map +1 -0
  63. package/dist/pi-tui/components/select-list.d.ts +50 -0
  64. package/dist/pi-tui/components/select-list.d.ts.map +1 -0
  65. package/dist/pi-tui/components/select-list.js +167 -0
  66. package/dist/pi-tui/components/select-list.js.map +1 -0
  67. package/dist/pi-tui/components/settings-list.d.ts +50 -0
  68. package/dist/pi-tui/components/settings-list.d.ts.map +1 -0
  69. package/dist/pi-tui/components/settings-list.js +189 -0
  70. package/dist/pi-tui/components/settings-list.js.map +1 -0
  71. package/dist/pi-tui/components/spacer.d.ts +12 -0
  72. package/dist/pi-tui/components/spacer.d.ts.map +1 -0
  73. package/dist/pi-tui/components/spacer.js +23 -0
  74. package/dist/pi-tui/components/spacer.js.map +1 -0
  75. package/dist/pi-tui/components/text.d.ts +19 -0
  76. package/dist/pi-tui/components/text.d.ts.map +1 -0
  77. package/dist/pi-tui/components/text.js +91 -0
  78. package/dist/pi-tui/components/text.js.map +1 -0
  79. package/dist/pi-tui/components/truncated-text.d.ts +13 -0
  80. package/dist/pi-tui/components/truncated-text.d.ts.map +1 -0
  81. package/dist/pi-tui/components/truncated-text.js +51 -0
  82. package/dist/pi-tui/components/truncated-text.js.map +1 -0
  83. package/dist/pi-tui/editor-component.d.ts +39 -0
  84. package/dist/pi-tui/editor-component.d.ts.map +1 -0
  85. package/dist/pi-tui/editor-component.js +2 -0
  86. package/dist/pi-tui/editor-component.js.map +1 -0
  87. package/dist/pi-tui/fuzzy.d.ts +16 -0
  88. package/dist/pi-tui/fuzzy.d.ts.map +1 -0
  89. package/dist/pi-tui/fuzzy.js +110 -0
  90. package/dist/pi-tui/fuzzy.js.map +1 -0
  91. package/dist/pi-tui/index.d.ts +23 -0
  92. package/dist/pi-tui/index.d.ts.map +1 -0
  93. package/dist/pi-tui/index.js +32 -0
  94. package/dist/pi-tui/index.js.map +1 -0
  95. package/dist/pi-tui/keybindings.d.ts +193 -0
  96. package/dist/pi-tui/keybindings.d.ts.map +1 -0
  97. package/dist/pi-tui/keybindings.js +174 -0
  98. package/dist/pi-tui/keybindings.js.map +1 -0
  99. package/dist/pi-tui/keys.d.ts +184 -0
  100. package/dist/pi-tui/keys.d.ts.map +1 -0
  101. package/dist/pi-tui/keys.js +1182 -0
  102. package/dist/pi-tui/keys.js.map +1 -0
  103. package/dist/pi-tui/kill-ring.d.ts +28 -0
  104. package/dist/pi-tui/kill-ring.d.ts.map +1 -0
  105. package/dist/pi-tui/kill-ring.js +44 -0
  106. package/dist/pi-tui/kill-ring.js.map +1 -0
  107. package/dist/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
  108. package/dist/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  109. package/dist/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  110. package/dist/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  111. package/dist/pi-tui/native-modifiers.d.ts +3 -0
  112. package/dist/pi-tui/native-modifiers.d.ts.map +1 -0
  113. package/dist/pi-tui/native-modifiers.js +53 -0
  114. package/dist/pi-tui/native-modifiers.js.map +1 -0
  115. package/dist/pi-tui/stdin-buffer.d.ts +50 -0
  116. package/dist/pi-tui/stdin-buffer.d.ts.map +1 -0
  117. package/dist/pi-tui/stdin-buffer.js +361 -0
  118. package/dist/pi-tui/stdin-buffer.js.map +1 -0
  119. package/dist/pi-tui/terminal-image.d.ts +90 -0
  120. package/dist/pi-tui/terminal-image.d.ts.map +1 -0
  121. package/dist/pi-tui/terminal-image.js +343 -0
  122. package/dist/pi-tui/terminal-image.js.map +1 -0
  123. package/dist/pi-tui/terminal.d.ts +113 -0
  124. package/dist/pi-tui/terminal.d.ts.map +1 -0
  125. package/dist/pi-tui/terminal.js +478 -0
  126. package/dist/pi-tui/terminal.js.map +1 -0
  127. package/dist/pi-tui/tui.d.ts +227 -0
  128. package/dist/pi-tui/tui.d.ts.map +1 -0
  129. package/dist/pi-tui/tui.js +1091 -0
  130. package/dist/pi-tui/tui.js.map +1 -0
  131. package/dist/pi-tui/undo-stack.d.ts +17 -0
  132. package/dist/pi-tui/undo-stack.d.ts.map +1 -0
  133. package/dist/pi-tui/undo-stack.js +25 -0
  134. package/dist/pi-tui/undo-stack.js.map +1 -0
  135. package/dist/pi-tui/utils.d.ts +84 -0
  136. package/dist/pi-tui/utils.d.ts.map +1 -0
  137. package/dist/pi-tui/utils.js +1024 -0
  138. package/dist/pi-tui/utils.js.map +1 -0
  139. package/dist/pi-tui/word-navigation.d.ts +25 -0
  140. package/dist/pi-tui/word-navigation.d.ts.map +1 -0
  141. package/dist/pi-tui/word-navigation.js +98 -0
  142. package/dist/pi-tui/word-navigation.js.map +1 -0
  143. package/dist/tools/__tests__/edit.test.js +85 -0
  144. package/dist/tools/__tests__/edit.test.js.map +1 -1
  145. package/dist/tools/edit.d.ts.map +1 -1
  146. package/dist/tools/edit.js +23 -1
  147. package/dist/tools/edit.js.map +1 -1
  148. package/dist/tui/components.d.ts +1 -1
  149. package/dist/tui/components.d.ts.map +1 -1
  150. package/dist/tui/components.js +1 -1
  151. package/dist/tui/components.js.map +1 -1
  152. package/dist/tui/editor-patch.d.ts +13 -0
  153. package/dist/tui/editor-patch.d.ts.map +1 -0
  154. package/dist/tui/editor-patch.js +41 -0
  155. package/dist/tui/editor-patch.js.map +1 -0
  156. package/dist/tui/index.d.ts.map +1 -1
  157. package/dist/tui/index.js +15 -3
  158. package/dist/tui/index.js.map +1 -1
  159. package/dist/web/index.d.ts.map +1 -1
  160. package/dist/web/index.js +9 -2
  161. package/dist/web/index.js.map +1 -1
  162. package/dist/web/public/app.js +65 -4
  163. package/dist/web/public/style.css +49 -0
  164. package/dist/web/session-bridge.d.ts +0 -4
  165. package/dist/web/session-bridge.d.ts.map +1 -1
  166. package/dist/web/session-bridge.js +16 -2
  167. package/dist/web/session-bridge.js.map +1 -1
  168. package/package.json +5 -3
  169. package/src/agent/__tests__/compaction.test.ts +6 -4
  170. package/src/agent/llm.ts +5 -1
  171. package/src/agent/prompt-builder.ts +27 -8
  172. package/src/agent/providers/anthropic.ts +15 -8
  173. package/src/agent/providers/google.ts +6 -1
  174. package/src/agent/providers/ollama.ts +6 -1
  175. package/src/agent/providers/openai.ts +21 -12
  176. package/src/agent/session.ts +12 -2
  177. package/src/pi-tui/autocomplete.ts +808 -0
  178. package/src/pi-tui/components/box.ts +137 -0
  179. package/src/pi-tui/components/cancellable-loader.ts +40 -0
  180. package/src/pi-tui/components/editor.ts +2321 -0
  181. package/src/pi-tui/components/image.ts +130 -0
  182. package/src/pi-tui/components/input.ts +456 -0
  183. package/src/pi-tui/components/loader.ts +93 -0
  184. package/src/pi-tui/components/markdown.ts +839 -0
  185. package/src/pi-tui/components/select-list.ts +263 -0
  186. package/src/pi-tui/components/settings-list.ts +264 -0
  187. package/src/pi-tui/components/spacer.ts +28 -0
  188. package/src/pi-tui/components/text.ts +113 -0
  189. package/src/pi-tui/components/truncated-text.ts +65 -0
  190. package/src/pi-tui/editor-component.ts +74 -0
  191. package/src/pi-tui/fuzzy.ts +137 -0
  192. package/src/pi-tui/index.ts +115 -0
  193. package/src/pi-tui/keybindings.ts +245 -0
  194. package/src/pi-tui/keys.ts +1440 -0
  195. package/src/pi-tui/kill-ring.ts +46 -0
  196. package/src/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
  197. package/src/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  198. package/src/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  199. package/src/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  200. package/src/pi-tui/native-modifiers.ts +65 -0
  201. package/src/pi-tui/stdin-buffer.ts +434 -0
  202. package/src/pi-tui/terminal-image.ts +469 -0
  203. package/src/pi-tui/terminal.ts +594 -0
  204. package/src/pi-tui/tui.ts +1376 -0
  205. package/src/pi-tui/undo-stack.ts +28 -0
  206. package/src/pi-tui/utils.ts +1176 -0
  207. package/src/pi-tui/word-navigation.ts +127 -0
  208. package/src/tools/__tests__/edit.test.ts +91 -0
  209. package/src/tools/edit.ts +27 -1
  210. package/src/tui/components.ts +2 -2
  211. package/src/tui/index.ts +24 -10
  212. package/src/web/index.ts +9 -2
  213. package/src/web/public/app.js +65 -4
  214. package/src/web/public/style.css +49 -0
  215. package/src/web/session-bridge.ts +13 -2
  216. package/tsconfig.json +2 -2
@@ -0,0 +1,65 @@
1
+ import type { Component } from '../tui.js'
2
+ import { truncateToWidth, visibleWidth } from '../utils.js'
3
+
4
+ /**
5
+ * Text component that truncates to fit viewport width
6
+ */
7
+ export class TruncatedText implements Component {
8
+ private text: string
9
+ private paddingX: number
10
+ private paddingY: number
11
+
12
+ constructor(text: string, paddingX: number = 0, paddingY: number = 0) {
13
+ this.text = text
14
+ this.paddingX = paddingX
15
+ this.paddingY = paddingY
16
+ }
17
+
18
+ invalidate(): void {
19
+ // No cached state to invalidate currently
20
+ }
21
+
22
+ render(width: number): string[] {
23
+ const result: string[] = []
24
+
25
+ // Empty line padded to width
26
+ const emptyLine = ' '.repeat(width)
27
+
28
+ // Add vertical padding above
29
+ for (let i = 0; i < this.paddingY; i++) {
30
+ result.push(emptyLine)
31
+ }
32
+
33
+ // Calculate available width after horizontal padding
34
+ const availableWidth = Math.max(1, width - this.paddingX * 2)
35
+
36
+ // Take only the first line (stop at newline)
37
+ let singleLineText = this.text
38
+ const newlineIndex = this.text.indexOf('\n')
39
+ if (newlineIndex !== -1) {
40
+ singleLineText = this.text.substring(0, newlineIndex)
41
+ }
42
+
43
+ // Truncate text if needed (accounting for ANSI codes)
44
+ const displayText = truncateToWidth(singleLineText, availableWidth)
45
+
46
+ // Add horizontal padding
47
+ const leftPadding = ' '.repeat(this.paddingX)
48
+ const rightPadding = ' '.repeat(this.paddingX)
49
+ const lineWithPadding = leftPadding + displayText + rightPadding
50
+
51
+ // Pad line to exactly width characters
52
+ const lineVisibleWidth = visibleWidth(lineWithPadding)
53
+ const paddingNeeded = Math.max(0, width - lineVisibleWidth)
54
+ const finalLine = lineWithPadding + ' '.repeat(paddingNeeded)
55
+
56
+ result.push(finalLine)
57
+
58
+ // Add vertical padding below
59
+ for (let i = 0; i < this.paddingY; i++) {
60
+ result.push(emptyLine)
61
+ }
62
+
63
+ return result
64
+ }
65
+ }
@@ -0,0 +1,74 @@
1
+ import type { AutocompleteProvider } from './autocomplete.js'
2
+ import type { Component } from './tui.js'
3
+
4
+ /**
5
+ * Interface for custom editor components.
6
+ *
7
+ * This allows extensions to provide their own editor implementation
8
+ * (e.g., vim mode, emacs mode, custom keybindings) while maintaining
9
+ * compatibility with the core application.
10
+ */
11
+ export interface EditorComponent extends Component {
12
+ // =========================================================================
13
+ // Core text access (required)
14
+ // =========================================================================
15
+
16
+ /** Get the current text content */
17
+ getText(): string
18
+
19
+ /** Set the text content */
20
+ setText(text: string): void
21
+
22
+ /** Handle raw terminal input (key presses, paste sequences, etc.) */
23
+ handleInput(data: string): void
24
+
25
+ // =========================================================================
26
+ // Callbacks (required)
27
+ // =========================================================================
28
+
29
+ /** Called when user submits (e.g., Enter key) */
30
+ onSubmit?: (text: string) => void
31
+
32
+ /** Called when text changes */
33
+ onChange?: (text: string) => void
34
+
35
+ // =========================================================================
36
+ // History support (optional)
37
+ // =========================================================================
38
+
39
+ /** Add text to history for up/down navigation */
40
+ addToHistory?(text: string): void
41
+
42
+ // =========================================================================
43
+ // Advanced text manipulation (optional)
44
+ // =========================================================================
45
+
46
+ /** Insert text at current cursor position */
47
+ insertTextAtCursor?(text: string): void
48
+
49
+ /**
50
+ * Get text with any markers expanded (e.g., paste markers).
51
+ * Falls back to getText() if not implemented.
52
+ */
53
+ getExpandedText?(): string
54
+
55
+ // =========================================================================
56
+ // Autocomplete support (optional)
57
+ // =========================================================================
58
+
59
+ /** Set the autocomplete provider */
60
+ setAutocompleteProvider?(provider: AutocompleteProvider): void
61
+
62
+ // =========================================================================
63
+ // Appearance (optional)
64
+ // =========================================================================
65
+
66
+ /** Border color function */
67
+ borderColor?: (str: string) => string
68
+
69
+ /** Set horizontal padding */
70
+ setPaddingX?(padding: number): void
71
+
72
+ /** Set max visible items in autocomplete dropdown */
73
+ setAutocompleteMaxVisible?(maxVisible: number): void
74
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Fuzzy matching utilities.
3
+ * Matches if all query characters appear in order (not necessarily consecutive).
4
+ * Lower score = better match.
5
+ */
6
+
7
+ export interface FuzzyMatch {
8
+ matches: boolean
9
+ score: number
10
+ }
11
+
12
+ export function fuzzyMatch(query: string, text: string): FuzzyMatch {
13
+ const queryLower = query.toLowerCase()
14
+ const textLower = text.toLowerCase()
15
+
16
+ const matchQuery = (normalizedQuery: string): FuzzyMatch => {
17
+ if (normalizedQuery.length === 0) {
18
+ return { matches: true, score: 0 }
19
+ }
20
+
21
+ if (normalizedQuery.length > textLower.length) {
22
+ return { matches: false, score: 0 }
23
+ }
24
+
25
+ let queryIndex = 0
26
+ let score = 0
27
+ let lastMatchIndex = -1
28
+ let consecutiveMatches = 0
29
+
30
+ for (let i = 0; i < textLower.length && queryIndex < normalizedQuery.length; i++) {
31
+ if (textLower[i] === normalizedQuery[queryIndex]) {
32
+ const isWordBoundary = i === 0 || /[\s\-_./:]/.test(textLower[i - 1]!)
33
+
34
+ // Reward consecutive matches
35
+ if (lastMatchIndex === i - 1) {
36
+ consecutiveMatches++
37
+ score -= consecutiveMatches * 5
38
+ } else {
39
+ consecutiveMatches = 0
40
+ // Penalize gaps
41
+ if (lastMatchIndex >= 0) {
42
+ score += (i - lastMatchIndex - 1) * 2
43
+ }
44
+ }
45
+
46
+ // Reward word boundary matches
47
+ if (isWordBoundary) {
48
+ score -= 10
49
+ }
50
+
51
+ // Slight penalty for later matches
52
+ score += i * 0.1
53
+
54
+ lastMatchIndex = i
55
+ queryIndex++
56
+ }
57
+ }
58
+
59
+ if (queryIndex < normalizedQuery.length) {
60
+ return { matches: false, score: 0 }
61
+ }
62
+
63
+ if (normalizedQuery === textLower) {
64
+ score -= 100
65
+ }
66
+
67
+ return { matches: true, score }
68
+ }
69
+
70
+ const primaryMatch = matchQuery(queryLower)
71
+ if (primaryMatch.matches) {
72
+ return primaryMatch
73
+ }
74
+
75
+ const alphaNumericMatch = queryLower.match(/^(?<letters>[a-z]+)(?<digits>[0-9]+)$/)
76
+ const numericAlphaMatch = queryLower.match(/^(?<digits>[0-9]+)(?<letters>[a-z]+)$/)
77
+ const swappedQuery = alphaNumericMatch
78
+ ? `${alphaNumericMatch.groups?.digits ?? ''}${alphaNumericMatch.groups?.letters ?? ''}`
79
+ : numericAlphaMatch
80
+ ? `${numericAlphaMatch.groups?.letters ?? ''}${numericAlphaMatch.groups?.digits ?? ''}`
81
+ : ''
82
+
83
+ if (!swappedQuery) {
84
+ return primaryMatch
85
+ }
86
+
87
+ const swappedMatch = matchQuery(swappedQuery)
88
+ if (!swappedMatch.matches) {
89
+ return primaryMatch
90
+ }
91
+
92
+ return { matches: true, score: swappedMatch.score + 5 }
93
+ }
94
+
95
+ /**
96
+ * Filter and sort items by fuzzy match quality (best matches first).
97
+ * Supports space-separated tokens: all tokens must match.
98
+ */
99
+ export function fuzzyFilter<T>(items: T[], query: string, getText: (item: T) => string): T[] {
100
+ if (!query.trim()) {
101
+ return items
102
+ }
103
+
104
+ const tokens = query
105
+ .trim()
106
+ .split(/\s+/)
107
+ .filter(t => t.length > 0)
108
+
109
+ if (tokens.length === 0) {
110
+ return items
111
+ }
112
+
113
+ const results: { item: T; totalScore: number }[] = []
114
+
115
+ for (const item of items) {
116
+ const text = getText(item)
117
+ let totalScore = 0
118
+ let allMatch = true
119
+
120
+ for (const token of tokens) {
121
+ const match = fuzzyMatch(token, text)
122
+ if (match.matches) {
123
+ totalScore += match.score
124
+ } else {
125
+ allMatch = false
126
+ break
127
+ }
128
+ }
129
+
130
+ if (allMatch) {
131
+ results.push({ item, totalScore })
132
+ }
133
+ }
134
+
135
+ results.sort((a, b) => a.totalScore - b.totalScore)
136
+ return results.map(r => r.item)
137
+ }
@@ -0,0 +1,115 @@
1
+ // Core TUI interfaces and classes
2
+
3
+ // Autocomplete support
4
+ export {
5
+ type AutocompleteItem,
6
+ type AutocompleteProvider,
7
+ type AutocompleteSuggestions,
8
+ CombinedAutocompleteProvider,
9
+ type SlashCommand,
10
+ } from './autocomplete.js'
11
+ // Components
12
+ export { Box } from './components/box.js'
13
+ export { CancellableLoader } from './components/cancellable-loader.js'
14
+ export { Editor, type EditorOptions, type EditorTheme } from './components/editor.js'
15
+ export { Image, type ImageOptions, type ImageTheme } from './components/image.js'
16
+ export { Input } from './components/input.js'
17
+ export { Loader, type LoaderIndicatorOptions } from './components/loader.js'
18
+ export {
19
+ type DefaultTextStyle,
20
+ Markdown,
21
+ type MarkdownOptions,
22
+ type MarkdownTheme,
23
+ } from './components/markdown.js'
24
+ export {
25
+ type SelectItem,
26
+ SelectList,
27
+ type SelectListLayoutOptions,
28
+ type SelectListTheme,
29
+ type SelectListTruncatePrimaryContext,
30
+ } from './components/select-list.js'
31
+ export {
32
+ type SettingItem,
33
+ SettingsList,
34
+ type SettingsListTheme,
35
+ } from './components/settings-list.js'
36
+ export { Spacer } from './components/spacer.js'
37
+ export { Text } from './components/text.js'
38
+ export { TruncatedText } from './components/truncated-text.js'
39
+ // Editor component interface (for custom editors)
40
+ export type { EditorComponent } from './editor-component.js'
41
+ // Fuzzy matching
42
+ export { type FuzzyMatch, fuzzyFilter, fuzzyMatch } from './fuzzy.js'
43
+ // Keybindings
44
+ export {
45
+ getKeybindings,
46
+ type Keybinding,
47
+ type KeybindingConflict,
48
+ type KeybindingDefinition,
49
+ type KeybindingDefinitions,
50
+ type Keybindings,
51
+ type KeybindingsConfig,
52
+ KeybindingsManager,
53
+ setKeybindings,
54
+ TUI_KEYBINDINGS,
55
+ } from './keybindings.js'
56
+ // Keyboard input handling
57
+ export {
58
+ decodeKittyPrintable,
59
+ isKeyRelease,
60
+ isKeyRepeat,
61
+ isKittyProtocolActive,
62
+ Key,
63
+ type KeyEventType,
64
+ type KeyId,
65
+ matchesKey,
66
+ parseKey,
67
+ setKittyProtocolActive,
68
+ } from './keys.js'
69
+ // Input buffering for batch splitting
70
+ export { StdinBuffer, type StdinBufferEventMap, type StdinBufferOptions } from './stdin-buffer.js'
71
+ // Terminal interface and implementations
72
+ export { ProcessTerminal, type Terminal } from './terminal.js'
73
+ // Terminal image support
74
+ export {
75
+ allocateImageId,
76
+ type CellDimensions,
77
+ calculateImageRows,
78
+ deleteAllKittyImages,
79
+ deleteKittyImage,
80
+ detectCapabilities,
81
+ encodeITerm2,
82
+ encodeKitty,
83
+ getCapabilities,
84
+ getCellDimensions,
85
+ getGifDimensions,
86
+ getImageDimensions,
87
+ getJpegDimensions,
88
+ getPngDimensions,
89
+ getWebpDimensions,
90
+ hyperlink,
91
+ type ImageDimensions,
92
+ type ImageProtocol,
93
+ type ImageRenderOptions,
94
+ imageFallback,
95
+ renderImage,
96
+ resetCapabilitiesCache,
97
+ setCapabilities,
98
+ setCellDimensions,
99
+ type TerminalCapabilities,
100
+ } from './terminal-image.js'
101
+ export {
102
+ type Component,
103
+ Container,
104
+ CURSOR_MARKER,
105
+ type Focusable,
106
+ isFocusable,
107
+ type OverlayAnchor,
108
+ type OverlayHandle,
109
+ type OverlayMargin,
110
+ type OverlayOptions,
111
+ type SizeValue,
112
+ TUI,
113
+ } from './tui.js'
114
+ // Utilities
115
+ export { truncateToWidth, visibleWidth, wrapTextWithAnsi } from './utils.js'
@@ -0,0 +1,245 @@
1
+ import { type KeyId, matchesKey } from './keys.js'
2
+
3
+ /**
4
+ * Global keybinding registry.
5
+ * Downstream packages can add keybindings via declaration merging.
6
+ */
7
+ export interface Keybindings {
8
+ // Editor navigation and editing
9
+ 'tui.editor.cursorUp': true
10
+ 'tui.editor.cursorDown': true
11
+ 'tui.editor.cursorLeft': true
12
+ 'tui.editor.cursorRight': true
13
+ 'tui.editor.cursorWordLeft': true
14
+ 'tui.editor.cursorWordRight': true
15
+ 'tui.editor.cursorLineStart': true
16
+ 'tui.editor.cursorLineEnd': true
17
+ 'tui.editor.jumpForward': true
18
+ 'tui.editor.jumpBackward': true
19
+ 'tui.editor.pageUp': true
20
+ 'tui.editor.pageDown': true
21
+ 'tui.editor.deleteCharBackward': true
22
+ 'tui.editor.deleteCharForward': true
23
+ 'tui.editor.deleteWordBackward': true
24
+ 'tui.editor.deleteWordForward': true
25
+ 'tui.editor.deleteToLineStart': true
26
+ 'tui.editor.deleteToLineEnd': true
27
+ 'tui.editor.yank': true
28
+ 'tui.editor.yankPop': true
29
+ 'tui.editor.undo': true
30
+ // Generic input actions
31
+ 'tui.input.newLine': true
32
+ 'tui.input.submit': true
33
+ 'tui.input.tab': true
34
+ 'tui.input.copy': true
35
+ // Generic selection actions
36
+ 'tui.select.up': true
37
+ 'tui.select.down': true
38
+ 'tui.select.pageUp': true
39
+ 'tui.select.pageDown': true
40
+ 'tui.select.confirm': true
41
+ 'tui.select.cancel': true
42
+ }
43
+
44
+ export type Keybinding = keyof Keybindings
45
+
46
+ export interface KeybindingDefinition {
47
+ defaultKeys: KeyId | KeyId[]
48
+ description?: string
49
+ }
50
+
51
+ export type KeybindingDefinitions = Record<string, KeybindingDefinition>
52
+ export type KeybindingsConfig = Record<string, KeyId | KeyId[] | undefined>
53
+
54
+ export const TUI_KEYBINDINGS = {
55
+ 'tui.editor.cursorUp': { defaultKeys: 'up', description: 'Move cursor up' },
56
+ 'tui.editor.cursorDown': { defaultKeys: 'down', description: 'Move cursor down' },
57
+ 'tui.editor.cursorLeft': {
58
+ defaultKeys: ['left', 'ctrl+b'],
59
+ description: 'Move cursor left',
60
+ },
61
+ 'tui.editor.cursorRight': {
62
+ defaultKeys: ['right', 'ctrl+f'],
63
+ description: 'Move cursor right',
64
+ },
65
+ 'tui.editor.cursorWordLeft': {
66
+ defaultKeys: ['alt+left', 'ctrl+left', 'alt+b'],
67
+ description: 'Move cursor word left',
68
+ },
69
+ 'tui.editor.cursorWordRight': {
70
+ defaultKeys: ['alt+right', 'ctrl+right', 'alt+f'],
71
+ description: 'Move cursor word right',
72
+ },
73
+ 'tui.editor.cursorLineStart': {
74
+ defaultKeys: ['home', 'ctrl+a'],
75
+ description: 'Move to line start',
76
+ },
77
+ 'tui.editor.cursorLineEnd': {
78
+ defaultKeys: ['end', 'ctrl+e'],
79
+ description: 'Move to line end',
80
+ },
81
+ 'tui.editor.jumpForward': {
82
+ defaultKeys: 'ctrl+]',
83
+ description: 'Jump forward to character',
84
+ },
85
+ 'tui.editor.jumpBackward': {
86
+ defaultKeys: 'ctrl+alt+]',
87
+ description: 'Jump backward to character',
88
+ },
89
+ 'tui.editor.pageUp': { defaultKeys: 'pageUp', description: 'Page up' },
90
+ 'tui.editor.pageDown': { defaultKeys: 'pageDown', description: 'Page down' },
91
+ 'tui.editor.deleteCharBackward': {
92
+ defaultKeys: 'backspace',
93
+ description: 'Delete character backward',
94
+ },
95
+ 'tui.editor.deleteCharForward': {
96
+ defaultKeys: ['delete', 'ctrl+d'],
97
+ description: 'Delete character forward',
98
+ },
99
+ 'tui.editor.deleteWordBackward': {
100
+ defaultKeys: ['ctrl+w', 'alt+backspace'],
101
+ description: 'Delete word backward',
102
+ },
103
+ 'tui.editor.deleteWordForward': {
104
+ defaultKeys: ['alt+d', 'alt+delete'],
105
+ description: 'Delete word forward',
106
+ },
107
+ 'tui.editor.deleteToLineStart': {
108
+ defaultKeys: 'ctrl+u',
109
+ description: 'Delete to line start',
110
+ },
111
+ 'tui.editor.deleteToLineEnd': {
112
+ defaultKeys: 'ctrl+k',
113
+ description: 'Delete to line end',
114
+ },
115
+ 'tui.editor.yank': { defaultKeys: 'ctrl+y', description: 'Yank' },
116
+ 'tui.editor.yankPop': { defaultKeys: 'alt+y', description: 'Yank pop' },
117
+ 'tui.editor.undo': { defaultKeys: 'ctrl+-', description: 'Undo' },
118
+ 'tui.input.newLine': { defaultKeys: 'shift+enter', description: 'Insert newline' },
119
+ 'tui.input.submit': { defaultKeys: 'enter', description: 'Submit input' },
120
+ 'tui.input.tab': { defaultKeys: 'tab', description: 'Tab / autocomplete' },
121
+ 'tui.input.copy': { defaultKeys: 'ctrl+c', description: 'Copy selection' },
122
+ 'tui.select.up': { defaultKeys: 'up', description: 'Move selection up' },
123
+ 'tui.select.down': { defaultKeys: 'down', description: 'Move selection down' },
124
+ 'tui.select.pageUp': { defaultKeys: 'pageUp', description: 'Selection page up' },
125
+ 'tui.select.pageDown': {
126
+ defaultKeys: 'pageDown',
127
+ description: 'Selection page down',
128
+ },
129
+ 'tui.select.confirm': { defaultKeys: 'enter', description: 'Confirm selection' },
130
+ 'tui.select.cancel': {
131
+ defaultKeys: ['escape', 'ctrl+c'],
132
+ description: 'Cancel selection',
133
+ },
134
+ } as const satisfies KeybindingDefinitions
135
+
136
+ export interface KeybindingConflict {
137
+ key: KeyId
138
+ keybindings: string[]
139
+ }
140
+
141
+ function normalizeKeys(keys: KeyId | KeyId[] | undefined): KeyId[] {
142
+ if (keys === undefined) return []
143
+ const keyList = Array.isArray(keys) ? keys : [keys]
144
+ const seen = new Set<KeyId>()
145
+ const result: KeyId[] = []
146
+ for (const key of keyList) {
147
+ if (!seen.has(key)) {
148
+ seen.add(key)
149
+ result.push(key)
150
+ }
151
+ }
152
+ return result
153
+ }
154
+
155
+ export class KeybindingsManager {
156
+ private definitions: KeybindingDefinitions
157
+ private userBindings: KeybindingsConfig
158
+ private keysById = new Map<Keybinding, KeyId[]>()
159
+ private conflicts: KeybindingConflict[] = []
160
+
161
+ constructor(definitions: KeybindingDefinitions, userBindings: KeybindingsConfig = {}) {
162
+ this.definitions = definitions
163
+ this.userBindings = userBindings
164
+ this.rebuild()
165
+ }
166
+
167
+ private rebuild(): void {
168
+ this.keysById.clear()
169
+ this.conflicts = []
170
+
171
+ const userClaims = new Map<KeyId, Set<Keybinding>>()
172
+ for (const [keybinding, keys] of Object.entries(this.userBindings)) {
173
+ if (!(keybinding in this.definitions)) continue
174
+ for (const key of normalizeKeys(keys)) {
175
+ const claimants = userClaims.get(key) ?? new Set<Keybinding>()
176
+ claimants.add(keybinding as Keybinding)
177
+ userClaims.set(key, claimants)
178
+ }
179
+ }
180
+
181
+ for (const [key, keybindings] of userClaims) {
182
+ if (keybindings.size > 1) {
183
+ this.conflicts.push({ key, keybindings: [...keybindings] })
184
+ }
185
+ }
186
+
187
+ for (const [id, definition] of Object.entries(this.definitions)) {
188
+ const userKeys = this.userBindings[id]
189
+ const keys =
190
+ userKeys === undefined ? normalizeKeys(definition.defaultKeys) : normalizeKeys(userKeys)
191
+ this.keysById.set(id as Keybinding, keys)
192
+ }
193
+ }
194
+
195
+ matches(data: string, keybinding: Keybinding): boolean {
196
+ const keys = this.keysById.get(keybinding) ?? []
197
+ for (const key of keys) {
198
+ if (matchesKey(data, key)) return true
199
+ }
200
+ return false
201
+ }
202
+
203
+ getKeys(keybinding: Keybinding): KeyId[] {
204
+ return [...(this.keysById.get(keybinding) ?? [])]
205
+ }
206
+
207
+ getDefinition(keybinding: Keybinding): KeybindingDefinition {
208
+ return this.definitions[keybinding]
209
+ }
210
+
211
+ getConflicts(): KeybindingConflict[] {
212
+ return this.conflicts.map(conflict => ({ ...conflict, keybindings: [...conflict.keybindings] }))
213
+ }
214
+
215
+ setUserBindings(userBindings: KeybindingsConfig): void {
216
+ this.userBindings = userBindings
217
+ this.rebuild()
218
+ }
219
+
220
+ getUserBindings(): KeybindingsConfig {
221
+ return { ...this.userBindings }
222
+ }
223
+
224
+ getResolvedBindings(): KeybindingsConfig {
225
+ const resolved: KeybindingsConfig = {}
226
+ for (const id of Object.keys(this.definitions)) {
227
+ const keys = this.keysById.get(id as Keybinding) ?? []
228
+ resolved[id] = keys.length === 1 ? keys[0]! : [...keys]
229
+ }
230
+ return resolved
231
+ }
232
+ }
233
+
234
+ let globalKeybindings: KeybindingsManager | null = null
235
+
236
+ export function setKeybindings(keybindings: KeybindingsManager): void {
237
+ globalKeybindings = keybindings
238
+ }
239
+
240
+ export function getKeybindings(): KeybindingsManager {
241
+ if (!globalKeybindings) {
242
+ globalKeybindings = new KeybindingsManager(TUI_KEYBINDINGS)
243
+ }
244
+ return globalKeybindings
245
+ }