termcast 1.3.34 → 1.3.36

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 (57) hide show
  1. package/dist/build.d.ts.map +1 -1
  2. package/dist/build.js +25 -0
  3. package/dist/build.js.map +1 -1
  4. package/dist/components/footer.d.ts.map +1 -1
  5. package/dist/components/footer.js +1 -1
  6. package/dist/components/footer.js.map +1 -1
  7. package/dist/components/icon.d.ts.map +1 -1
  8. package/dist/components/icon.js +386 -23
  9. package/dist/components/icon.js.map +1 -1
  10. package/dist/components/list.d.ts.map +1 -1
  11. package/dist/components/list.js +70 -7
  12. package/dist/components/list.js.map +1 -1
  13. package/dist/extensions/home.js +1 -1
  14. package/dist/extensions/home.js.map +1 -1
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/internal/dialog.d.ts.map +1 -1
  19. package/dist/internal/dialog.js +4 -5
  20. package/dist/internal/dialog.js.map +1 -1
  21. package/dist/internal/providers.d.ts.map +1 -1
  22. package/dist/internal/providers.js +18 -5
  23. package/dist/internal/providers.js.map +1 -1
  24. package/dist/state.d.ts +1 -0
  25. package/dist/state.d.ts.map +1 -1
  26. package/dist/state.js.map +1 -1
  27. package/dist/theme.d.ts.map +1 -1
  28. package/dist/theme.js +6 -2
  29. package/dist/theme.js.map +1 -1
  30. package/dist/utils.d.ts +16 -1
  31. package/dist/utils.d.ts.map +1 -1
  32. package/dist/utils.js +28 -1
  33. package/dist/utils.js.map +1 -1
  34. package/package.json +8 -4
  35. package/src/build.tsx +28 -0
  36. package/src/compile.vitest.tsx +18 -18
  37. package/src/components/footer.tsx +4 -2
  38. package/src/components/icon.tsx +385 -23
  39. package/src/components/list.tsx +84 -13
  40. package/src/examples/github.vitest.tsx +36 -36
  41. package/src/examples/list-detail-metadata.vitest.tsx +1 -1
  42. package/src/examples/list-dropdown-default.vitest.tsx +9 -9
  43. package/src/examples/list-scrollbox.vitest.tsx +41 -41
  44. package/src/examples/list-with-detail.vitest.tsx +35 -36
  45. package/src/examples/list-with-sections.vitest.tsx +117 -117
  46. package/src/examples/simple-grid.vitest.tsx +44 -44
  47. package/src/examples/simple-navigation.vitest.tsx +43 -12
  48. package/src/examples/store.vitest.tsx +1 -1
  49. package/src/examples/swift-extension.vitest.tsx +3 -3
  50. package/src/extensions/dev.vitest.tsx +21 -21
  51. package/src/extensions/home.tsx +1 -1
  52. package/src/index.tsx +1 -0
  53. package/src/internal/dialog.tsx +21 -23
  54. package/src/internal/providers.tsx +18 -5
  55. package/src/state.tsx +1 -0
  56. package/src/theme.tsx +6 -2
  57. package/src/utils.tsx +40 -1
@@ -111,7 +111,7 @@ test('navigation between main and detail views', async () => {
111
111
  > Detail view - Press ESC to go back
112
112
 
113
113
  Details
114
- ›This is the detail view for Second Item Press Enter to go back
114
+ ›This is the detail view for Second Item Press Enter to go back o
115
115
 
116
116
 
117
117
 
@@ -153,8 +153,8 @@ test('navigation between main and detail views', async () => {
153
153
  > Main view
154
154
 
155
155
  Items
156
- First Item Navigate to first detail
157
- Second Item Navigate to second detail
156
+ First Item Navigate to first detail
157
+ Second Item Navigate to second detail
158
158
  Third Item Navigate to third detail
159
159
 
160
160
 
@@ -187,9 +187,9 @@ test('navigation between main and detail views', async () => {
187
187
  > Main view
188
188
 
189
189
  Items
190
- First Item Navigate to first detail
190
+ First Item Navigate to first detail
191
191
  Second Item Navigate to second detail
192
- Third Item Navigate to third detail
192
+ Third Item Navigate to third detail
193
193
 
194
194
 
195
195
 
@@ -215,12 +215,12 @@ test('navigation between main and detail views', async () => {
215
215
  "
216
216
 
217
217
 
218
- Detail: Third Item ─────────────────────────────────────────────
218
+ Detail: First Item ─────────────────────────────────────────────
219
219
 
220
220
  > Detail view - Press ESC to go back
221
221
 
222
222
  Details
223
- ›This is the detail view for Third Item Press Enter to go back o
223
+ ›This is the detail view for First Item Press Enter to go back or
224
224
 
225
225
 
226
226
 
@@ -412,7 +412,7 @@ test('navigation with actions panel', async () => {
412
412
  > Detail view - Press ESC to go back
413
413
 
414
414
  Details
415
- ›This is the detail view for Second Item Press Enter to go back
415
+ ›This is the detail view for Second Item Press Enter to go back o
416
416
 
417
417
 
418
418
 
@@ -482,8 +482,8 @@ test('navigation with actions panel', async () => {
482
482
  > Main view
483
483
 
484
484
  Items
485
- First Item Navigate to first detail
486
- Second Item Navigate to second detail
485
+ First Item Navigate to first detail
486
+ Second Item Navigate to second detail
487
487
  Third Item Navigate to third detail
488
488
 
489
489
 
@@ -606,7 +606,7 @@ test('search functionality in main and detail views', async () => {
606
606
  > Detail view - Press ESC to go back
607
607
 
608
608
  Details
609
- ›This is the detail view for First Item Press Enter to go back o
609
+ ›This is the detail view for First Item Press Enter to go back or
610
610
 
611
611
 
612
612
 
@@ -644,7 +644,7 @@ test('search functionality in main and detail views', async () => {
644
644
 
645
645
 
646
646
 
647
- No items found
647
+ No items found
648
648
 
649
649
 
650
650
 
@@ -700,3 +700,34 @@ test('search functionality in main and detail views', async () => {
700
700
  "
701
701
  `)
702
702
  }, 10000)
703
+
704
+ test('keeps selected item after opening detail and pressing esc', async () => {
705
+ await session.text({
706
+ waitFor: (text) => {
707
+ return /Navigation Example/i.test(text) && /First Item/i.test(text)
708
+ },
709
+ })
710
+
711
+ await session.press('down')
712
+ const selectedSecondSnapshot = await session.text()
713
+ expect(selectedSecondSnapshot).toContain('›Second Item')
714
+
715
+ await session.press('enter')
716
+ await session.text({
717
+ waitFor: (text) => {
718
+ return /Detail: Second Item/i.test(text)
719
+ },
720
+ timeout: 3000,
721
+ })
722
+
723
+ await session.press('esc')
724
+ const backSnapshot = await session.text({
725
+ waitFor: (text) => {
726
+ return /Navigation Example/i.test(text)
727
+ },
728
+ timeout: 3000,
729
+ })
730
+
731
+ expect(backSnapshot).toContain('›Second Item')
732
+ expect(backSnapshot).not.toContain('›First Item')
733
+ }, 10000)
@@ -50,7 +50,7 @@ test('Store extension - searching for spiceblow shows Database', async () => {
50
50
 
51
51
  > spiceblow
52
52
 
53
- ›Spiceblow - Sql Database Management Search, update, insert and delete row
53
+ ›Spiceblow - Sql Database Management Search, update, insert and delete rows
54
54
 
55
55
 
56
56
 
@@ -89,13 +89,14 @@ test.skipIf(isLinux)('swift extension dev mode shows command list', async () =>
89
89
  > Search commands...
90
90
 
91
91
  Commands
92
- ›List Items Displays a simple list with some items view
93
- Swift List Displays a list of items returned by a Swift f view
92
+ ›List Items Displays a simple list with some items view
93
+ Swift List Displays a list of items returned by a Swift fun view
94
94
 
95
95
 
96
96
 
97
97
 
98
98
 
99
+ ↵ run command ↑↓ navigate ^k actions
99
100
 
100
101
 
101
102
 
@@ -104,7 +105,6 @@ test.skipIf(isLinux)('swift extension dev mode shows command list', async () =>
104
105
 
105
106
 
106
107
 
107
- ↵ run command ↑↓ navigate ^k actions
108
108
 
109
109
  "
110
110
  `)
@@ -57,12 +57,12 @@ test('dev command shows extension commands list', async () => {
57
57
 
58
58
  > Search commands...
59
59
 
60
- Commands
61
- ›List Items Displays a simple list with some ite view
62
- Search Items Search and filter through a list o view
63
- Google Oauth view
64
- usePromise Demo Shows how to use the usePromise view
65
- Show State Shows the current application state view
60
+ Commands
61
+ ›List Items Displays a simple list with some items view
62
+ Search Items Search and filter through a list of view
63
+ Google Oauth view
64
+ usePromise Demo Shows how to use the usePromise h view
65
+ Show State Shows the current application state in view
66
66
 
67
67
 
68
68
  ↵ run command ↑↓ navigate ^k actions
@@ -93,12 +93,12 @@ test('selecting command with arguments shows arguments form', async () => {
93
93
 
94
94
  > Search commands...
95
95
 
96
- Google Oauth view
97
- usePromise Demo Shows how to use the usePromise view
98
- Show State Shows the current application state view
99
- ›With Arguments Demonstrates command arguments ( view
100
- Quick Action Copies current timestamp to cli no-view
101
- Throw Error Command that throws an error at roo view
96
+ Google Oauth view
97
+ usePromise Demo Shows how to use the usePromise h view
98
+ Show State Shows the current application state in view
99
+ ›With Arguments Demonstrates command arguments (te view
100
+ Quick Action Copies current timestamp to clipb no-view
101
+ Throw Error Command that throws an error at root view
102
102
 
103
103
 
104
104
  ↵ run command ↑↓ navigate ^k actions
@@ -204,9 +204,9 @@ test('can fill arguments and run command', async () => {
204
204
  > Search...
205
205
 
206
206
  Received Arguments
207
- ›▼ Search Query (empty)
208
- Secret Key (empty)
209
- Category (empty)
207
+ ›𝐓 Search Query (empty)
208
+ 𝐓 Secret Key (empty)
209
+ 𝐓 Category (empty)
210
210
 
211
211
 
212
212
 
@@ -242,12 +242,12 @@ test('can run simple view command without arguments', async () => {
242
242
 
243
243
  > Search...
244
244
 
245
- Items
246
- ›▲ First Item This is the first item
247
- Second Item This is the second item
248
- Third Item This is the third item
249
- Fourth Item This is the fourth item
250
- Fifth Item This is the fifth item
245
+ Items
246
+ ›○ First Item This is the first item
247
+ Second Item This is the second item
248
+ Third Item This is the third item
249
+ Fourth Item This is the fourth item
250
+ Fifth Item This is the fifth item
251
251
 
252
252
 
253
253
  ✓ Copied to Clipboard First Item
@@ -181,7 +181,7 @@ function ExtensionsList({
181
181
 
182
182
  export async function runHomeCommand(): Promise<void> {
183
183
  logger.log(`preparing to render the home command component`)
184
- await renderWithProviders(<Home />)
184
+ await renderWithProviders(<Home />, { extensionName: 'termcast-home' })
185
185
  logger.log(`rendered home command component`)
186
186
  }
187
187
 
package/src/index.tsx CHANGED
@@ -224,6 +224,7 @@ export { TermcastProvider } from 'termcast/src/internal/providers'
224
224
 
225
225
  // Helper function for rendering examples
226
226
  export { renderWithProviders } from 'termcast/src/utils'
227
+ export type { RenderWithProvidersOptions } from 'termcast/src/utils'
227
228
 
228
229
  // Window Management
229
230
  export { closeMainWindow, PopToRootType } from 'termcast/src/apis/window'
@@ -151,7 +151,6 @@ export function DialogOverlay(): any {
151
151
  const dialogStack = useStore((state) => state.dialogStack)
152
152
  const showActionsDialog = useStore((state) => state.showActionsDialog)
153
153
  const navContext = useContext(NavigationContext)
154
- const theme = useTheme()
155
154
 
156
155
  const setActionsPortalTargetRef = useCallback((node: any) => {
157
156
  if (!node) {
@@ -177,13 +176,33 @@ export function DialogOverlay(): any {
177
176
 
178
177
  return (
179
178
  <>
179
+ {item && (
180
+ <box position='absolute' width='100%' height='100%' flexDirection='column'>
181
+ <InFocus inFocus={true}>
182
+ <Dialog
183
+ position={item.position}
184
+ onClickOutside={() => {
185
+ const state = useStore.getState()
186
+ if (state.dialogStack.length > 0) {
187
+ useStore.setState({
188
+ dialogStack: state.dialogStack.slice(0, -1),
189
+ })
190
+ }
191
+ }}
192
+ >
193
+ <NavigationContext.Provider value={navContext}>
194
+ {item.element}
195
+ </NavigationContext.Provider>
196
+ </Dialog>
197
+ </InFocus>
198
+ </box>
199
+ )}
180
200
  {showActionsDialog && (
181
201
  <box
182
202
  position='absolute'
183
203
  width='100%'
184
204
  height='100%'
185
205
  flexDirection='column'
186
- backgroundColor={theme.background}
187
206
  >
188
207
  <InFocus inFocus={true}>
189
208
  <Dialog
@@ -201,27 +220,6 @@ export function DialogOverlay(): any {
201
220
  </InFocus>
202
221
  </box>
203
222
  )}
204
- {item && (
205
- <box position='absolute' width='100%' height='100%' flexDirection='column'>
206
- <InFocus inFocus={true}>
207
- <Dialog
208
- position={item.position}
209
- onClickOutside={() => {
210
- const state = useStore.getState()
211
- if (state.dialogStack.length > 0) {
212
- useStore.setState({
213
- dialogStack: state.dialogStack.slice(0, -1),
214
- })
215
- }
216
- }}
217
- >
218
- <NavigationContext.Provider value={navContext}>
219
- {item.element}
220
- </NavigationContext.Provider>
221
- </Dialog>
222
- </InFocus>
223
- </box>
224
- )}
225
223
  </>
226
224
  )
227
225
  }
@@ -35,20 +35,33 @@ const queryClient = new QueryClient({
35
35
  },
36
36
  })
37
37
 
38
- // Create a custom persister using the Cache class
39
- const queryCache = new Cache({ namespace: 'tanstack-query' })
38
+ // Lazy-initialized Cache for TanStack Query persistence.
39
+ // Must not be created at module load time because extensionPath may not be set yet
40
+ // (e.g. when renderWithProviders sets state right before rendering).
41
+ // Tracks the extensionPath it was created with so it can be recreated if the path changes.
42
+ let queryCache: Cache | null = null
43
+ let queryCachePath: string | null = null
44
+
45
+ function getQueryCache(): Cache {
46
+ const currentPath = useStore.getState().extensionPath
47
+ if (!queryCache || currentPath !== queryCachePath) {
48
+ queryCache = new Cache({ namespace: 'tanstack-query' })
49
+ queryCachePath = currentPath
50
+ }
51
+ return queryCache
52
+ }
40
53
 
41
54
  const persister = {
42
55
  persistClient: async (client: any) => {
43
56
  const serialized = JSON.stringify(client)
44
- queryCache.set('query-client-data', serialized)
57
+ getQueryCache().set('query-client-data', serialized)
45
58
  },
46
59
  restoreClient: async () => {
47
- const data = queryCache.get('query-client-data')
60
+ const data = getQueryCache().get('query-client-data')
48
61
  return data ? JSON.parse(data) : undefined
49
62
  },
50
63
  removeClient: async () => {
51
- queryCache.remove('query-client-data')
64
+ getQueryCache().remove('query-client-data')
52
65
  },
53
66
  }
54
67
 
package/src/state.tsx CHANGED
@@ -36,6 +36,7 @@ export interface DialogStackItem {
36
36
  export interface NavigationStackItem {
37
37
  element: ReactNode
38
38
  onPop?: () => void
39
+ selectedListIndex?: number
39
40
  }
40
41
 
41
42
  interface AppState {
package/src/theme.tsx CHANGED
@@ -3,12 +3,16 @@ import { getResolvedTheme, type ResolvedTheme, defaultThemeName, themeNames } fr
3
3
  import { useStore } from './state'
4
4
  import { Cache } from './apis/cache'
5
5
 
6
- // Global cache for theme persistence (no namespace = global storage)
6
+ // Global cache for theme persistence (no namespace = global storage).
7
+ // Tracks extensionPath so the cache is recreated if the path changes.
7
8
  let globalCache: Cache | null = null
9
+ let globalCachePath: string | null = null
8
10
 
9
11
  function getGlobalCache(): Cache {
10
- if (!globalCache) {
12
+ const currentPath = useStore.getState().extensionPath
13
+ if (!globalCache || currentPath !== globalCachePath) {
11
14
  globalCache = new Cache()
15
+ globalCachePath = currentPath
12
16
  }
13
17
  return globalCache
14
18
  }
package/src/utils.tsx CHANGED
@@ -12,8 +12,47 @@ import {
12
12
  type CommandWithFile,
13
13
  } from './package-json'
14
14
  import { logger } from './logger'
15
+ import { useStore } from './state'
16
+
17
+ export interface RenderWithProvidersOptions {
18
+ extensionName?: string
19
+ extensionPath?: string
20
+ packageJson?: RaycastPackageJson
21
+ }
22
+
23
+ /**
24
+ * Render a React element wrapped in TermcastProvider with all necessary state initialized.
25
+ *
26
+ * Sets up extensionPath (for LocalStorage, Cache, assets) and extensionPackageJson
27
+ * (for preferences, environment metadata, action panel config) before rendering.
28
+ *
29
+ * - extensionName defaults to 'termcast-app'
30
+ * - extensionPath defaults to ~/.termcast/compiled/{extensionName}
31
+ * - packageJson defaults to a minimal { name, title, commands: [] }
32
+ */
33
+ export async function renderWithProviders(
34
+ element: ReactNode,
35
+ options?: RenderWithProvidersOptions,
36
+ ): Promise<void> {
37
+ const extensionName = options?.extensionName || 'termcast-app'
38
+ const extensionPath =
39
+ options?.extensionPath ||
40
+ path.join(os.homedir(), '.termcast', 'compiled', extensionName)
41
+ const packageJson: RaycastPackageJson = options?.packageJson || {
42
+ name: extensionName,
43
+ title: extensionName,
44
+ description: '',
45
+ commands: [],
46
+ }
47
+
48
+ // Reset store to initial state then set extension fields, matching
49
+ // startDevMode/startCompiledExtension behavior to avoid stale navigation/dialog state
50
+ useStore.setState({
51
+ ...useStore.getInitialState(),
52
+ extensionPath,
53
+ extensionPackageJson: packageJson,
54
+ })
15
55
 
16
- export async function renderWithProviders(element: ReactNode): Promise<void> {
17
56
  const renderer = await createCliRenderer({
18
57
  onDestroy: () => {
19
58
  process.exit(0)