termcast 1.3.30 → 1.3.32

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 (294) hide show
  1. package/dist/apis/cache.d.ts.map +1 -1
  2. package/dist/apis/cache.js +4 -39
  3. package/dist/apis/cache.js.map +1 -1
  4. package/dist/apis/hud.d.ts.map +1 -1
  5. package/dist/apis/hud.js +13 -31
  6. package/dist/apis/hud.js.map +1 -1
  7. package/dist/apis/localstorage.d.ts.map +1 -1
  8. package/dist/apis/localstorage.js +3 -27
  9. package/dist/apis/localstorage.js.map +1 -1
  10. package/dist/apis/toast.d.ts +16 -43
  11. package/dist/apis/toast.d.ts.map +1 -1
  12. package/dist/apis/toast.js +78 -177
  13. package/dist/apis/toast.js.map +1 -1
  14. package/dist/build.d.ts +3 -1
  15. package/dist/build.d.ts.map +1 -1
  16. package/dist/build.js +52 -2
  17. package/dist/build.js.map +1 -1
  18. package/dist/cli.d.ts +1 -0
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +206 -25
  21. package/dist/cli.js.map +1 -1
  22. package/dist/colors.d.ts.map +1 -1
  23. package/dist/colors.js +1 -0
  24. package/dist/colors.js.map +1 -1
  25. package/dist/compile.d.ts +0 -1
  26. package/dist/compile.d.ts.map +1 -1
  27. package/dist/compile.js +18 -23
  28. package/dist/compile.js.map +1 -1
  29. package/dist/components/actions.d.ts.map +1 -1
  30. package/dist/components/actions.js +30 -15
  31. package/dist/components/actions.js.map +1 -1
  32. package/dist/components/animation-tick.d.ts +12 -0
  33. package/dist/components/animation-tick.d.ts.map +1 -0
  34. package/dist/components/animation-tick.js +63 -0
  35. package/dist/components/animation-tick.js.map +1 -0
  36. package/dist/components/detail.d.ts.map +1 -1
  37. package/dist/components/detail.js +10 -13
  38. package/dist/components/detail.js.map +1 -1
  39. package/dist/components/dropdown.d.ts +1 -0
  40. package/dist/components/dropdown.d.ts.map +1 -1
  41. package/dist/components/dropdown.js +27 -26
  42. package/dist/components/dropdown.js.map +1 -1
  43. package/dist/components/extension-preferences.d.ts.map +1 -1
  44. package/dist/components/extension-preferences.js +15 -10
  45. package/dist/components/extension-preferences.js.map +1 -1
  46. package/dist/components/footer.d.ts +13 -0
  47. package/dist/components/footer.d.ts.map +1 -0
  48. package/dist/components/footer.js +106 -0
  49. package/dist/components/footer.js.map +1 -0
  50. package/dist/components/form/file-autocomplete.d.ts +19 -4
  51. package/dist/components/form/file-autocomplete.d.ts.map +1 -1
  52. package/dist/components/form/file-autocomplete.js +56 -55
  53. package/dist/components/form/file-autocomplete.js.map +1 -1
  54. package/dist/components/form/file-picker.d.ts.map +1 -1
  55. package/dist/components/form/file-picker.js +26 -15
  56. package/dist/components/form/file-picker.js.map +1 -1
  57. package/dist/components/form/index.d.ts.map +1 -1
  58. package/dist/components/form/index.js +17 -15
  59. package/dist/components/form/index.js.map +1 -1
  60. package/dist/components/form/with-left-border.d.ts.map +1 -1
  61. package/dist/components/form/with-left-border.js +4 -12
  62. package/dist/components/form/with-left-border.js.map +1 -1
  63. package/dist/components/list.d.ts.map +1 -1
  64. package/dist/components/list.js +126 -86
  65. package/dist/components/list.js.map +1 -1
  66. package/dist/components/loading-bar.d.ts.map +1 -1
  67. package/dist/components/loading-bar.js +5 -22
  68. package/dist/components/loading-bar.js.map +1 -1
  69. package/dist/components/loading-text.d.ts.map +1 -1
  70. package/dist/components/loading-text.js +3 -22
  71. package/dist/components/loading-text.js.map +1 -1
  72. package/dist/components/theme-picker.d.ts +2 -0
  73. package/dist/components/theme-picker.d.ts.map +1 -0
  74. package/dist/components/theme-picker.js +37 -0
  75. package/dist/components/theme-picker.js.map +1 -0
  76. package/dist/descendants.d.ts +6 -0
  77. package/dist/descendants.d.ts.map +1 -1
  78. package/dist/descendants.js +74 -8
  79. package/dist/descendants.js.map +1 -1
  80. package/dist/examples/internal/descendants-rerender.d.ts +14 -0
  81. package/dist/examples/internal/descendants-rerender.d.ts.map +1 -0
  82. package/dist/examples/internal/descendants-rerender.js +145 -0
  83. package/dist/examples/internal/descendants-rerender.js.map +1 -0
  84. package/dist/examples/internal/simple-dialog.js +4 -1
  85. package/dist/examples/internal/simple-dialog.js.map +1 -1
  86. package/dist/examples/internal/simple-scrollbox.js +1 -1
  87. package/dist/examples/internal/simple-scrollbox.js.map +1 -1
  88. package/dist/examples/list-with-dropdown.js +1 -1
  89. package/dist/examples/list-with-dropdown.js.map +1 -1
  90. package/dist/examples/miscellaneous.js +1 -1
  91. package/dist/examples/miscellaneous.js.map +1 -1
  92. package/dist/examples/toast-action.d.ts +2 -0
  93. package/dist/examples/toast-action.d.ts.map +1 -0
  94. package/dist/examples/toast-action.js +76 -0
  95. package/dist/examples/toast-action.js.map +1 -0
  96. package/dist/examples/toast-variations.js +38 -36
  97. package/dist/examples/toast-variations.js.map +1 -1
  98. package/dist/extensions/dev.d.ts +1 -1
  99. package/dist/extensions/dev.d.ts.map +1 -1
  100. package/dist/extensions/dev.js +62 -30
  101. package/dist/extensions/dev.js.map +1 -1
  102. package/dist/extensions/home.d.ts.map +1 -1
  103. package/dist/extensions/home.js +4 -3
  104. package/dist/extensions/home.js.map +1 -1
  105. package/dist/extensions/react-refresh-init.d.ts +5 -0
  106. package/dist/extensions/react-refresh-init.d.ts.map +1 -0
  107. package/dist/extensions/react-refresh-init.js +52 -0
  108. package/dist/extensions/react-refresh-init.js.map +1 -0
  109. package/dist/internal/date-picker-widget.js +1 -1
  110. package/dist/internal/date-picker-widget.js.map +1 -1
  111. package/dist/internal/dialog.d.ts +8 -3
  112. package/dist/internal/dialog.d.ts.map +1 -1
  113. package/dist/internal/dialog.js +37 -53
  114. package/dist/internal/dialog.js.map +1 -1
  115. package/dist/internal/navigation.d.ts +1 -0
  116. package/dist/internal/navigation.d.ts.map +1 -1
  117. package/dist/internal/navigation.js +25 -1
  118. package/dist/internal/navigation.js.map +1 -1
  119. package/dist/internal/providers.d.ts.map +1 -1
  120. package/dist/internal/providers.js +9 -197
  121. package/dist/internal/providers.js.map +1 -1
  122. package/dist/internal/scrollbox.d.ts.map +1 -1
  123. package/dist/internal/scrollbox.js +1 -0
  124. package/dist/internal/scrollbox.js.map +1 -1
  125. package/dist/release.d.ts +1 -0
  126. package/dist/release.d.ts.map +1 -1
  127. package/dist/release.js +16 -9
  128. package/dist/release.js.map +1 -1
  129. package/dist/state.d.ts +27 -1
  130. package/dist/state.d.ts.map +1 -1
  131. package/dist/state.js +6 -0
  132. package/dist/state.js.map +1 -1
  133. package/dist/theme.d.ts +6 -19
  134. package/dist/theme.d.ts.map +1 -1
  135. package/dist/theme.js +76 -45
  136. package/dist/theme.js.map +1 -1
  137. package/dist/themes/aura.json +69 -0
  138. package/dist/themes/ayu.json +80 -0
  139. package/dist/themes/catppuccin-frappe.json +233 -0
  140. package/dist/themes/catppuccin-macchiato.json +233 -0
  141. package/dist/themes/catppuccin.json +112 -0
  142. package/dist/themes/cobalt2.json +228 -0
  143. package/dist/themes/cursor.json +249 -0
  144. package/dist/themes/dracula.json +219 -0
  145. package/dist/themes/everforest.json +241 -0
  146. package/dist/themes/flexoki.json +237 -0
  147. package/dist/themes/github-light.json +56 -0
  148. package/dist/themes/github.json +241 -0
  149. package/dist/themes/gruvbox.json +95 -0
  150. package/dist/themes/kanagawa.json +77 -0
  151. package/dist/themes/lucent-orng.json +227 -0
  152. package/dist/themes/material.json +235 -0
  153. package/dist/themes/matrix.json +77 -0
  154. package/dist/themes/mercury.json +245 -0
  155. package/dist/themes/monokai.json +221 -0
  156. package/dist/themes/nightowl.json +221 -0
  157. package/dist/themes/nord.json +223 -0
  158. package/dist/themes/one-dark.json +84 -0
  159. package/dist/themes/opencode-light.json +62 -0
  160. package/dist/themes/opencode.json +245 -0
  161. package/dist/themes/orng.json +245 -0
  162. package/dist/themes/palenight.json +222 -0
  163. package/dist/themes/rosepine.json +234 -0
  164. package/dist/themes/solarized.json +223 -0
  165. package/dist/themes/synthwave84.json +226 -0
  166. package/dist/themes/termcast.json +226 -0
  167. package/dist/themes/tokyonight.json +243 -0
  168. package/dist/themes/vercel.json +255 -0
  169. package/dist/themes/vesper.json +218 -0
  170. package/dist/themes/zenburn.json +223 -0
  171. package/dist/themes.d.ts +57 -0
  172. package/dist/themes.d.ts.map +1 -0
  173. package/dist/themes.js +181 -0
  174. package/dist/themes.js.map +1 -0
  175. package/dist/utils/run-command.d.ts +2 -1
  176. package/dist/utils/run-command.d.ts.map +1 -1
  177. package/dist/utils/run-command.js +20 -10
  178. package/dist/utils/run-command.js.map +1 -1
  179. package/dist/utils.d.ts +2 -1
  180. package/dist/utils.d.ts.map +1 -1
  181. package/dist/utils.js +90 -17
  182. package/dist/utils.js.map +1 -1
  183. package/dist/watcher.d.ts +3 -0
  184. package/dist/watcher.d.ts.map +1 -0
  185. package/dist/watcher.js +16 -0
  186. package/dist/watcher.js.map +1 -0
  187. package/package.json +16 -10
  188. package/src/apis/cache.tsx +5 -44
  189. package/src/apis/hud.tsx +17 -62
  190. package/src/apis/localstorage.tsx +3 -32
  191. package/src/apis/toast.tsx +91 -275
  192. package/src/build.test.tsx +10 -0
  193. package/src/build.tsx +61 -1
  194. package/src/cli.tsx +365 -103
  195. package/src/colors.tsx +1 -0
  196. package/src/compile.tsx +21 -29
  197. package/src/compile.vitest.tsx +300 -0
  198. package/src/components/actions.tsx +64 -45
  199. package/src/components/animation-tick.tsx +85 -0
  200. package/src/components/detail.tsx +31 -35
  201. package/src/components/dropdown.tsx +32 -21
  202. package/src/components/extension-preferences.tsx +14 -10
  203. package/src/components/footer.tsx +241 -0
  204. package/src/components/form/file-autocomplete.tsx +80 -60
  205. package/src/components/form/file-picker.tsx +37 -25
  206. package/src/components/form/index.tsx +45 -41
  207. package/src/components/form/with-left-border.tsx +4 -14
  208. package/src/components/list.tsx +181 -121
  209. package/src/components/loading-bar.tsx +5 -25
  210. package/src/components/loading-text.tsx +4 -23
  211. package/src/components/theme-picker.tsx +57 -0
  212. package/src/descendants.tsx +98 -9
  213. package/src/examples/actions-dialog-layout.vitest.tsx +112 -0
  214. package/src/examples/file-autocomplete.vitest.tsx +131 -122
  215. package/src/examples/form-basic.vitest.tsx +463 -644
  216. package/src/examples/form-dropdown.vitest.tsx +553 -571
  217. package/src/examples/form-scroll.vitest.tsx +112 -102
  218. package/src/examples/form-tagpicker.vitest.tsx +364 -338
  219. package/src/examples/internal/descendants-rerender.tsx +273 -0
  220. package/src/examples/internal/descendants-rerender.vitest.tsx +194 -0
  221. package/src/examples/internal/simple-dialog.tsx +4 -4
  222. package/src/examples/internal/simple-scrollbox.tsx +2 -2
  223. package/src/examples/internal/simple-scrollbox.vitest.tsx +43 -31
  224. package/src/examples/list-detail-metadata.vitest.tsx +34 -30
  225. package/src/examples/list-dropdown-default.vitest.tsx +84 -72
  226. package/src/examples/list-empty-view.vitest.tsx +93 -0
  227. package/src/examples/list-fetch-data.vitest.tsx +36 -30
  228. package/src/examples/list-scrollbox.vitest.tsx +59 -39
  229. package/src/examples/list-with-detail.vitest.tsx +339 -314
  230. package/src/examples/list-with-dropdown.tsx +1 -0
  231. package/src/examples/list-with-dropdown.vitest.tsx +176 -150
  232. package/src/examples/list-with-sections.vitest.tsx +289 -270
  233. package/src/examples/list-with-toast.vitest.tsx +44 -44
  234. package/src/examples/miscellaneous.tsx +10 -0
  235. package/src/examples/simple-file-picker.vitest.tsx +90 -86
  236. package/src/examples/simple-grid.vitest.tsx +275 -249
  237. package/src/examples/simple-navigation.vitest.tsx +192 -168
  238. package/src/examples/store.vitest.tsx +6 -4
  239. package/src/examples/swift-extension.vitest.tsx +31 -19
  240. package/src/examples/synonyms.vitest.tsx +93 -83
  241. package/src/examples/toast-action.tsx +160 -0
  242. package/src/examples/toast-action.vitest.tsx +404 -0
  243. package/src/examples/toast-variations.tsx +58 -57
  244. package/src/examples/toast-variations.vitest.tsx +186 -166
  245. package/src/extensions/dev.tsx +74 -33
  246. package/src/extensions/dev.vitest.tsx +162 -69
  247. package/src/extensions/home.tsx +5 -6
  248. package/src/extensions/react-refresh-init.tsx +59 -0
  249. package/src/internal/date-picker-widget.tsx +1 -1
  250. package/src/internal/dialog.tsx +59 -83
  251. package/src/internal/navigation.tsx +37 -4
  252. package/src/internal/providers.tsx +27 -315
  253. package/src/internal/scrollbox.tsx +1 -0
  254. package/src/release.tsx +16 -10
  255. package/src/state.tsx +36 -3
  256. package/src/theme.tsx +82 -51
  257. package/src/themes/aura.json +69 -0
  258. package/src/themes/ayu.json +80 -0
  259. package/src/themes/catppuccin-frappe.json +233 -0
  260. package/src/themes/catppuccin-macchiato.json +233 -0
  261. package/src/themes/catppuccin.json +112 -0
  262. package/src/themes/cobalt2.json +228 -0
  263. package/src/themes/cursor.json +249 -0
  264. package/src/themes/dracula.json +219 -0
  265. package/src/themes/everforest.json +241 -0
  266. package/src/themes/flexoki.json +237 -0
  267. package/src/themes/github-light.json +56 -0
  268. package/src/themes/github.json +241 -0
  269. package/src/themes/gruvbox.json +95 -0
  270. package/src/themes/kanagawa.json +77 -0
  271. package/src/themes/lucent-orng.json +227 -0
  272. package/src/themes/material.json +235 -0
  273. package/src/themes/matrix.json +77 -0
  274. package/src/themes/mercury.json +252 -0
  275. package/src/themes/monokai.json +221 -0
  276. package/src/themes/nightowl.json +221 -0
  277. package/src/themes/nord.json +223 -0
  278. package/src/themes/one-dark.json +84 -0
  279. package/src/themes/opencode-light.json +62 -0
  280. package/src/themes/opencode.json +245 -0
  281. package/src/themes/orng.json +245 -0
  282. package/src/themes/palenight.json +222 -0
  283. package/src/themes/rosepine.json +234 -0
  284. package/src/themes/solarized.json +223 -0
  285. package/src/themes/synthwave84.json +226 -0
  286. package/src/themes/termcast.json +227 -0
  287. package/src/themes/tokyonight.json +243 -0
  288. package/src/themes/vercel.json +255 -0
  289. package/src/themes/vesper.json +218 -0
  290. package/src/themes/zenburn.json +223 -0
  291. package/src/themes.ts +291 -0
  292. package/src/utils/run-command.tsx +23 -12
  293. package/src/utils.tsx +115 -18
  294. package/src/watcher.tsx +19 -0
@@ -1,30 +1,11 @@
1
- import { useKeyboard, useTerminalDimensions } from '@opentui/react'
1
+ import { useKeyboard } from '@opentui/react'
2
2
  import React, { type ReactNode, useRef, useContext } from 'react'
3
3
  import { Theme } from 'termcast/src/theme'
4
4
  import { InFocus, useIsInFocus } from 'termcast/src/internal/focus-context'
5
5
  import { CommonProps } from 'termcast/src/utils'
6
- import {
7
- useStore,
8
- type DialogPosition,
9
- type DialogStackItem,
10
- } from 'termcast/src/state'
11
- import { logger } from '../logger'
12
- import { ToastOverlay } from 'termcast/src/apis/toast'
13
- import { NavigationContext } from 'termcast/src/internal/navigation'
6
+ import { useStore, type DialogPosition } from 'termcast/src/state'
14
7
 
15
- const Border = {
16
- topLeft: '┏',
17
- topRight: '┓',
18
- bottomLeft: '',
19
- bottomRight: '',
20
- horizontal: '━',
21
- vertical: '┃',
22
- topT: '+',
23
- bottomT: '+',
24
- leftT: '+',
25
- rightT: '+',
26
- cross: '+',
27
- }
8
+ import { NavigationContext } from 'termcast/src/internal/navigation'
28
9
 
29
10
  export type { DialogPosition } from 'termcast/src/state'
30
11
 
@@ -39,7 +20,6 @@ export function Dialog({
39
20
  position = 'center',
40
21
  onClickOutside,
41
22
  }: DialogProps): any {
42
- const dimensions = useTerminalDimensions()
43
23
  const inFocus = useIsInFocus()
44
24
  const clickedInsideDialog = useRef(false)
45
25
 
@@ -61,30 +41,20 @@ export function Dialog({
61
41
  return {
62
42
  alignItems: 'flex-end' as const,
63
43
  justifyContent: 'flex-start' as const,
64
- paddingTop: 2,
65
- paddingRight: 2,
66
- paddingBottom: undefined,
67
- paddingLeft: undefined,
44
+ padding: 2,
68
45
  }
69
46
  case 'bottom-right':
70
- // TODO show in center for now. easier to read
71
- // return {
72
- // alignItems: 'flex-end' as const,
73
- // justifyContent: 'flex-end' as const,
74
- // paddingTop: undefined,
75
- // paddingBottom: 2,
76
- // paddingRight: 2,
77
- // paddingLeft: undefined
78
- // }
47
+ return {
48
+ alignItems: 'flex-end' as const,
49
+ justifyContent: 'flex-end' as const,
50
+ padding: 2,
51
+ }
79
52
  case 'center':
80
53
  default:
81
54
  return {
82
55
  alignItems: 'center' as const,
83
- justifyContent: 'flex-start' as const,
84
- paddingTop: Math.floor(dimensions.height / 4),
85
- paddingBottom: undefined,
86
- paddingLeft: undefined,
87
- paddingRight: undefined,
56
+ justifyContent: 'center' as const,
57
+ padding: 0,
88
58
  }
89
59
  }
90
60
  }
@@ -94,26 +64,21 @@ export function Dialog({
94
64
  return (
95
65
  <box
96
66
  border={false}
97
- width={dimensions.width}
98
- height={dimensions.height}
67
+ flexGrow={1}
68
+ left={-2}
99
69
  alignItems={positionStyles.alignItems}
100
70
  justifyContent={positionStyles.justifyContent}
101
- position='absolute'
102
- paddingTop={positionStyles.paddingTop}
103
- paddingBottom={positionStyles.paddingBottom}
104
- paddingLeft={positionStyles.paddingLeft}
105
- paddingRight={positionStyles.paddingRight}
106
- left={0}
107
- top={0}
71
+ padding={positionStyles.padding}
72
+ // backgroundColor={Theme.background}
108
73
  onMouseDown={handleBackdropClick}
109
74
  >
110
75
  <box
111
76
  border
112
- customBorderChars={Border}
77
+ borderStyle='rounded'
113
78
  width={76}
114
- maxWidth={dimensions.width - 2}
79
+ maxWidth='95%'
115
80
  backgroundColor={Theme.backgroundPanel}
116
- borderColor={Theme.border}
81
+ borderColor={Theme.accent}
117
82
  paddingTop={1}
118
83
  onMouseDown={handleDialogClick}
119
84
  >
@@ -143,12 +108,13 @@ export function DialogProvider(props: DialogProviderProps): any {
143
108
  }
144
109
  })
145
110
 
111
+ // Children lose focus only when there's a dialog (toast uses unique shortcuts so no focus stealing needed)
112
+ const childrenInFocus = !dialogStack?.length
113
+
146
114
  return (
147
115
  <>
148
- <InFocus inFocus={!dialogStack?.length}>{props.children}</InFocus>
149
- <InFocus inFocus={false}>
150
- <ToastOverlay />
151
- </InFocus>
116
+ <InFocus inFocus={childrenInFocus}>{props.children}</InFocus>
117
+
152
118
  </>
153
119
  )
154
120
  }
@@ -161,31 +127,29 @@ export function DialogOverlay(): any {
161
127
  return null
162
128
  }
163
129
 
130
+ // Only render the topmost dialog
131
+ const topIndex = dialogStack.length - 1
132
+ const item = dialogStack[topIndex]
133
+
164
134
  return (
165
- <box position='absolute'>
166
- {dialogStack.map((item, index) => {
167
- const isLastItem = index === dialogStack.length - 1
168
- return (
169
- <InFocus key={'dialog' + String(index)} inFocus={isLastItem}>
170
- <Dialog
171
- position={item.position}
172
- onClickOutside={() => {
173
- if (!isLastItem) return
174
- const state = useStore.getState()
175
- if (state.dialogStack.length > 0) {
176
- useStore.setState({
177
- dialogStack: state.dialogStack.slice(0, -1),
178
- })
179
- }
180
- }}
181
- >
182
- <NavigationContext.Provider value={navContext}>
183
- {item.element}
184
- </NavigationContext.Provider>
185
- </Dialog>
186
- </InFocus>
187
- )
188
- })}
135
+ <box position='absolute' width='100%' height='100%' flexDirection='column'>
136
+ <InFocus inFocus={true}>
137
+ <Dialog
138
+ position={item.position}
139
+ onClickOutside={() => {
140
+ const state = useStore.getState()
141
+ if (state.dialogStack.length > 0) {
142
+ useStore.setState({
143
+ dialogStack: state.dialogStack.slice(0, -1),
144
+ })
145
+ }
146
+ }}
147
+ >
148
+ <NavigationContext.Provider value={navContext}>
149
+ {item.element}
150
+ </NavigationContext.Provider>
151
+ </Dialog>
152
+ </InFocus>
189
153
  </box>
190
154
  )
191
155
  }
@@ -193,13 +157,24 @@ export function DialogOverlay(): any {
193
157
  export function useDialog() {
194
158
  const dialogStack = useStore((state) => state.dialogStack)
195
159
 
196
- const pushDialog = (element: ReactNode, position?: DialogPosition) => {
160
+ const pushDialog = (args: {
161
+ element: ReactNode
162
+ position?: DialogPosition
163
+ type?: 'actions'
164
+ }) => {
197
165
  const state = useStore.getState()
198
166
  useStore.setState({
199
- dialogStack: [...state.dialogStack, { element, position }],
167
+ dialogStack: [
168
+ ...state.dialogStack,
169
+ { element: args.element, position: args.position, type: args.type },
170
+ ],
200
171
  })
201
172
  }
202
173
 
174
+ const pushActions = (element: ReactNode, position: DialogPosition='center') => {
175
+ pushDialog({ element, position, type: 'actions' })
176
+ }
177
+
203
178
  const clearDialogs = () => {
204
179
  useStore.setState({ dialogStack: [] })
205
180
  }
@@ -210,6 +185,7 @@ export function useDialog() {
210
185
 
211
186
  return {
212
187
  push: pushDialog,
188
+ pushActions,
213
189
  clear: clearDialogs,
214
190
  replace: replaceDialog,
215
191
  stack: dialogStack,
@@ -16,6 +16,7 @@ import { logger } from '../logger'
16
16
 
17
17
  interface Navigation {
18
18
  push: (element: ReactNode, onPop?: () => void) => void
19
+ replace: (element: ReactNode, onPop?: () => void) => void
19
20
  pop: () => void
20
21
  popToRoot: () => void
21
22
  }
@@ -26,9 +27,9 @@ interface NavigationContextType {
26
27
  isPending: boolean
27
28
  }
28
29
 
29
- export const NavigationContext = createContext<NavigationContextType | undefined>(
30
- undefined,
31
- )
30
+ export const NavigationContext = createContext<
31
+ NavigationContextType | undefined
32
+ >(undefined)
32
33
 
33
34
  interface NavigationProviderProps extends CommonProps {
34
35
  children: ReactNode
@@ -63,6 +64,34 @@ export function NavigationProvider(props: NavigationProviderProps): any {
63
64
  useStore.setState({
64
65
  navigationStack: [...currentStack, { element, onPop }],
65
66
  dialogStack: [],
67
+ toast: null,
68
+ toastWithPrimaryAction: false,
69
+ })
70
+ })
71
+ }, [])
72
+
73
+ const replace = useCallback((element: any, onPop?: () => void) => {
74
+ if (!element) {
75
+ throw new Error(`cannot replace with falsy value ${element}`)
76
+ }
77
+
78
+ logger.log(
79
+ 'replacing',
80
+ (element as any)?.type?.name || (element as any)?.type,
81
+ )
82
+
83
+ const currentStack = useStore.getState().navigationStack
84
+ const newStack =
85
+ currentStack.length > 0
86
+ ? [...currentStack.slice(0, -1), { element, onPop }]
87
+ : [{ element, onPop }]
88
+
89
+ startNavigationTransition(() => {
90
+ useStore.setState({
91
+ navigationStack: newStack,
92
+ dialogStack: [],
93
+ toast: null,
94
+ toastWithPrimaryAction: false,
66
95
  })
67
96
  })
68
97
  }, [])
@@ -101,10 +130,11 @@ export function NavigationProvider(props: NavigationProviderProps): any {
101
130
  const navigation = React.useMemo(
102
131
  () => ({
103
132
  push,
133
+ replace,
104
134
  pop,
105
135
  popToRoot,
106
136
  }),
107
- [push, pop, popToRoot],
137
+ [push, replace, pop, popToRoot],
108
138
  )
109
139
 
110
140
  const value = React.useMemo(
@@ -123,6 +153,9 @@ export function NavigationProvider(props: NavigationProviderProps): any {
123
153
  useKeyboard((evt) => {
124
154
  if (!inFocus) return
125
155
  if (evt.name === 'escape') {
156
+ // If there's a toast visible, let toast handle escape first
157
+ if (useStore.getState().toast) return
158
+
126
159
  if (stack.length > 1) {
127
160
  logger.log(
128
161
  'popping navigation',
@@ -11,19 +11,19 @@ import { NavigationProvider } from 'termcast/src/internal/navigation'
11
11
  import { CommonProps } from 'termcast/src/utils'
12
12
  import { Cache } from 'termcast/src/apis/cache'
13
13
  import { logger } from 'termcast/src/logger'
14
- import { Theme } from 'termcast/src/theme'
15
- import { useKeyboard } from '@opentui/react'
16
- import { TextAttributes } from '@opentui/core'
14
+ import { Theme, initializeTheme } from 'termcast/src/theme'
17
15
  import { useStore } from 'termcast/src/state'
18
- import * as fs from 'fs'
19
- import * as path from 'path'
20
- import { exec } from 'child_process'
21
- import dedent from 'string-dedent'
16
+ import { useTerminalDimensions } from '@opentui/react'
22
17
  import { initializeErrorHandlers } from 'termcast/src/internal/error-handler'
23
18
 
19
+ import { InFocus } from './focus-context'
20
+
24
21
  // Initialize error handlers at module load time
25
22
  initializeErrorHandlers()
26
23
 
24
+ // Initialize theme from persisted storage
25
+ initializeTheme()
26
+
27
27
  const queryClient = new QueryClient({
28
28
  defaultOptions: {
29
29
  queries: {
@@ -75,9 +75,6 @@ class ErrorBoundaryClass extends Component<
75
75
  constructor(props: { children: ReactNode }) {
76
76
  super(props)
77
77
  this.state = { hasError: false, error: null }
78
-
79
- this.openGitHubIssue = this.openGitHubIssue.bind(this)
80
- this.getRecentLogs = this.getRecentLogs.bind(this)
81
78
  }
82
79
 
83
80
  static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
@@ -92,314 +89,19 @@ class ErrorBoundaryClass extends Component<
92
89
  })
93
90
  }
94
91
 
95
- getRecentLogs(): string {
96
- const LOG_FILE = path.join(process.cwd(), 'app.log')
97
- try {
98
- if (fs.existsSync(LOG_FILE)) {
99
- const content = fs.readFileSync(LOG_FILE, 'utf-8')
100
- const lines = content.split('\n').filter((line) => line.trim())
101
- // Get last 200 lines
102
- const recentLines = lines.slice(-200)
103
- return recentLines.join('\n')
104
- }
105
- } catch (err) {
106
- logger.error('Failed to read log file:', err)
107
- }
108
- return 'No logs available'
109
- }
110
-
111
- openGitHubIssue(): void {
112
- const error = this.state.error
113
- if (!error) return
114
-
115
- const logs = this.getRecentLogs()
116
-
117
- // Get navigation stack information for body
118
- const navigationStack = useStore.getState().navigationStack
119
- const navigationInfo = navigationStack
120
- .map((item, index) => {
121
- const element = item.element as any
122
- const componentName =
123
- element?.type?.displayName ||
124
- element?.type?.name ||
125
- element?.type ||
126
- 'Unknown'
127
- return `${index + 1}. ${componentName}`
128
- })
129
- .join('\n')
130
-
131
- // Get current extension/command info
132
- const extensionPackageJson = useStore.getState().extensionPackageJson
133
- const currentCommandName = useStore.getState().currentCommandName
134
- const extensionPath = useStore.getState().extensionPath
135
-
136
- const extensionName =
137
- extensionPackageJson?.name ||
138
- (extensionPath ? path.basename(extensionPath) : null)
139
-
140
- let contextInfo = ''
141
- let titlePrefix = ''
142
-
143
- if (extensionName) {
144
- contextInfo = `Extension: ${extensionName}`
145
- titlePrefix = `\`${extensionName}\``
146
- if (currentCommandName) {
147
- contextInfo += ` | Command: ${currentCommandName}`
148
- titlePrefix += ` > \`${currentCommandName}\``
149
- }
150
- }
151
-
152
- const title = encodeURIComponent(
153
- titlePrefix ? `${titlePrefix}: ${error.message}` : error.message,
154
- )
155
-
156
- const MAX_URL_LENGTH = 4096
157
- const baseUrl = 'https://github.com/remorses/termcast/issues/new?title='
158
- const titlePart = `${baseUrl}${title}&body=`
159
-
160
- // Calculate how much space we have for the body
161
- const availableBodyLength = MAX_URL_LENGTH - titlePart.length
162
-
163
- // Always create a minimal body
164
- const minimalBody = dedent`
165
- ## Error Details
166
-
167
- **Message:** ${error.message}
168
- **Context:** ${contextInfo || 'No extension loaded'}
169
- **Navigation:** \`${navigationStack
170
- .map((item) => {
171
- const element = item.element as any
172
- return (
173
- element?.type?.displayName ||
174
- element?.type?.name ||
175
- element?.type ||
176
- 'Unknown'
177
- )
178
- })
179
- .join(' > ')}\`
180
-
181
- ## Stack Trace
182
-
183
- \`\`\`\`
184
- ${error.stack || 'No stack trace available'}
185
- \`\`\`\`
186
-
187
- ## Environment
188
-
189
- - Platform: ${process.platform}
190
- - Node Version: ${process.version}
191
- - Date: ${new Date().toISOString()}
192
- `
193
-
194
- let body = encodeURIComponent(minimalBody)
195
-
196
- // If still too long, just truncate it
197
- if (body.length > availableBodyLength) {
198
- body = body.substring(0, availableBodyLength)
199
- }
200
-
201
- const url = `${titlePart}${body}`
202
-
203
- // Open in browser
204
- const openCmd =
205
- process.platform === 'darwin'
206
- ? 'open'
207
- : process.platform === 'win32'
208
- ? 'start'
209
- : 'xdg-open'
210
-
211
- exec(`${openCmd} "${url}"`, (err) => {
212
- if (err) {
213
- logger.error('Failed to open browser:', err)
214
- }
215
- })
216
- }
217
-
218
92
  render(): any {
219
93
  if (this.state.hasError) {
220
- return (
221
- <ErrorDisplay
222
- error={this.state.error}
223
- onOpenIssue={this.openGitHubIssue}
224
- getRecentLogs={this.getRecentLogs}
225
- />
226
- )
94
+ return <ErrorDisplay error={this.state.error} />
227
95
  }
228
96
 
229
97
  return this.props.children
230
98
  }
231
99
  }
232
100
 
233
- function ErrorDisplay({
234
- error,
235
- onOpenIssue,
236
- getRecentLogs,
237
- }: {
238
- error: Error | null
239
- onOpenIssue: () => void
240
- getRecentLogs: () => string
241
- }): any {
242
- const [isHovered, setIsHovered] = React.useState(false)
243
- const [showLogs, setShowLogs] = React.useState(false)
244
- const [focusedIndex, setFocusedIndex] = React.useState(0) // 0 = issue button, 1+ = stack trace lines
245
-
246
- // Parse stack trace to get file paths with line numbers
247
- const stackLines = React.useMemo(() => {
248
- if (!error?.stack) return []
249
-
250
- const lines = error.stack.split('\n')
251
- const parsedLines: Array<{ text: string; file?: string; line?: number }> =
252
- []
253
-
254
- for (const line of lines) {
255
- // Match file paths with line:column format
256
- // Common patterns: "at file:///path/to/file.ts:123:45" or "at /path/to/file.ts:123:45"
257
- const match = line.match(
258
- /at\s+(?:.*?\s+\()?(?:file:\/\/)?([^:\s]+(?:\.tsx?|\.jsx?)):(\d+)(?::\d+)?/,
259
- )
260
-
261
- if (match) {
262
- parsedLines.push({
263
- text: line,
264
- file: match[1],
265
- line: parseInt(match[2], 10),
266
- })
267
- } else {
268
- parsedLines.push({ text: line })
269
- }
270
- }
271
-
272
- return parsedLines
273
- }, [error?.stack])
274
-
275
- // Count of focusable stack lines (those with file paths)
276
- const focusableStackLines = stackLines.filter((line) => line.file).length
277
-
278
- useKeyboard(async (evt) => {
279
- if (evt.name === 'down') {
280
- setFocusedIndex((prev) => {
281
- const maxIndex = focusableStackLines // 0 for button + focusable stack lines
282
- return Math.min(prev + 1, maxIndex)
283
- })
284
- } else if (evt.name === 'up') {
285
- setFocusedIndex((prev) => Math.max(prev - 1, 0))
286
- } else if (evt.name === 'return') {
287
- if (focusedIndex === 0) {
288
- // Issue button is focused
289
- onOpenIssue()
290
- } else {
291
- // Stack trace line is focused - copy file path to clipboard
292
- let currentFocusableIndex = 0
293
- for (const line of stackLines) {
294
- if (line.file) {
295
- currentFocusableIndex++
296
- if (currentFocusableIndex === focusedIndex) {
297
- const filePathWithLine = `${line.file}:${line.line}`
298
- const { Clipboard } = await import(
299
- 'termcast/src/apis/clipboard'
300
- )
301
- await Clipboard.copy(filePathWithLine)
302
- logger.log(`📋 Copied to clipboard: ${filePathWithLine}`)
303
- break
304
- }
305
- }
306
- }
307
- }
308
- }
309
- })
310
-
101
+ function ErrorDisplay({ error }: { error: Error | null }): any {
311
102
  return (
312
- <box padding={2} flexDirection='column' gap={1}>
313
- <text fg={Theme.error} attributes={TextAttributes.BOLD}>
314
- ⚠️ An error occurred
315
- </text>
316
-
317
- <text fg={Theme.error}>
318
- {error?.message || 'An unexpected error occurred'}
319
- </text>
320
-
321
- <box
322
- paddingLeft={1}
323
- paddingRight={1}
324
- borderStyle='rounded'
325
- border={true}
326
- borderColor={
327
- focusedIndex === 0
328
- ? Theme.highlight
329
- : isHovered
330
- ? Theme.highlight
331
- : Theme.border
332
- }
333
- backgroundColor={
334
- focusedIndex === 0
335
- ? Theme.backgroundPanel
336
- : isHovered
337
- ? Theme.backgroundPanel
338
- : undefined
339
- }
340
- onMouseMove={() => setIsHovered(true)}
341
- onMouseOut={() => setIsHovered(false)}
342
- onMouseDown={onOpenIssue}
343
- marginTop={1}
344
- >
345
- <text>
346
- {focusedIndex === 0 ? '▶ ' : ' '}📝 Press Enter or click to report
347
- on GitHub
348
- </text>
349
- </box>
350
-
351
- <box flexDirection='column' marginTop={1}>
352
- <text fg={Theme.textMuted} attributes={TextAttributes.BOLD}>
353
- Stack Trace:
354
- </text>
355
- {stackLines.map((line, index) => {
356
- let currentFocusableIndex = 0
357
- let isFocused = false
358
-
359
- // Determine if this line is focused
360
- if (line.file) {
361
- currentFocusableIndex = stackLines
362
- .slice(0, index + 1)
363
- .filter((l) => l.file).length
364
- isFocused = focusedIndex === currentFocusableIndex
365
- }
366
-
367
- return (
368
- <box
369
- key={index}
370
- backgroundColor={isFocused ? Theme.backgroundPanel : undefined}
371
- >
372
- <text fg={isFocused ? Theme.highlight : Theme.error}>
373
- {isFocused ? '▶ ' : ' '}
374
- {line.text}
375
- </text>
376
- </box>
377
- )
378
- })}
379
- </box>
380
-
381
- <box marginTop={1} onMouseDown={() => setShowLogs(!showLogs)}>
382
- <text fg={Theme.textMuted} attributes={TextAttributes.UNDERLINE}>
383
- {showLogs ? '▼' : '▶'} Toggle logs (last 200 lines)
384
- </text>
385
- </box>
386
-
387
- {showLogs && (
388
- <box flexDirection='column' marginTop={1}>
389
- <text fg={Theme.textMuted} attributes={TextAttributes.BOLD}>
390
- Recent Logs:
391
- </text>
392
- <box
393
- padding={1}
394
- borderStyle='single'
395
- border={true}
396
- borderColor={Theme.border}
397
- maxHeight={20}
398
- >
399
- <text fg={Theme.textMuted}>{getRecentLogs()}</text>
400
- </box>
401
- </box>
402
- )}
103
+ <box padding={2}>
104
+ <text fg={Theme.error} wrapMode='none'>{error?.stack}</text>
403
105
  </box>
404
106
  )
405
107
  }
@@ -407,6 +109,8 @@ function ErrorDisplay({
407
109
  const ErrorBoundary = ErrorBoundaryClass as any
408
110
 
409
111
  export function TermcastProvider(props: ProvidersProps): any {
112
+
113
+
410
114
  return (
411
115
  <ErrorBoundary>
412
116
  <Suspense fallback={<LoadingFallback />}>
@@ -417,14 +121,22 @@ export function TermcastProvider(props: ProvidersProps): any {
417
121
  maxAge: 1000 * 60 * 60 * 24, // 24 hours
418
122
  }}
419
123
  >
420
- <DialogProvider>
124
+ <box
125
+ minHeight={'100%'}
126
+ justifyContent='flex-start'
127
+ backgroundColor={Theme.background}
128
+ // borderColor={Theme.border}
129
+ // fg={Theme.text}
130
+ >
421
131
  <box padding={2}>
422
- {/* NavigationProvider must be last to ensure parent providers remain in the tree when navigation changes */}
423
- <NavigationProvider overlay={<DialogOverlay />}>
424
- {props.children}
425
- </NavigationProvider>
132
+ <DialogProvider>
133
+ {/* NavigationProvider must be last to ensure parent providers remain in the tree when navigation changes */}
134
+ <NavigationProvider overlay={<DialogOverlay />}>
135
+ <box>{props.children}</box>
136
+ </NavigationProvider>
137
+ </DialogProvider>
426
138
  </box>
427
- </DialogProvider>
139
+ </box>
428
140
  </PersistQueryClientProvider>
429
141
  </Suspense>
430
142
  </ErrorBoundary>
@@ -34,6 +34,7 @@ export function ScrollBox({
34
34
  },
35
35
  contentOptions: {
36
36
  flexShrink: 0,
37
+ minHeight: 0, // let the scrollbox shrink with content
37
38
  ...(style?.contentOptions || {}),
38
39
  },
39
40
  scrollbarOptions: {