termcast 1.3.16 → 1.3.19

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 (53) hide show
  1. package/dist/apis/environment.d.ts +7 -0
  2. package/dist/apis/environment.d.ts.map +1 -1
  3. package/dist/apis/environment.js +38 -0
  4. package/dist/apis/environment.js.map +1 -1
  5. package/dist/cli.js +80 -1
  6. package/dist/cli.js.map +1 -1
  7. package/dist/components/actions.d.ts +5 -4
  8. package/dist/components/actions.d.ts.map +1 -1
  9. package/dist/components/actions.js.map +1 -1
  10. package/dist/components/list.d.ts.map +1 -1
  11. package/dist/components/list.js +11 -0
  12. package/dist/components/list.js.map +1 -1
  13. package/dist/examples/list-with-toast.d.ts +2 -0
  14. package/dist/examples/list-with-toast.d.ts.map +1 -0
  15. package/dist/examples/list-with-toast.js +24 -0
  16. package/dist/examples/list-with-toast.js.map +1 -0
  17. package/dist/extensions/dev.js +1 -1
  18. package/dist/extensions/dev.js.map +1 -1
  19. package/dist/index.d.ts +4 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +4 -2
  22. package/dist/index.js.map +1 -1
  23. package/dist/internal/dialog.d.ts.map +1 -1
  24. package/dist/internal/dialog.js +1 -1
  25. package/dist/internal/dialog.js.map +1 -1
  26. package/dist/internal/navigation.d.ts +2 -0
  27. package/dist/internal/navigation.d.ts.map +1 -1
  28. package/dist/internal/navigation.js +28 -1
  29. package/dist/internal/navigation.js.map +1 -1
  30. package/dist/keyboard.d.ts +38 -0
  31. package/dist/keyboard.d.ts.map +1 -0
  32. package/dist/keyboard.js +25 -0
  33. package/dist/keyboard.js.map +1 -0
  34. package/dist/release.d.ts.map +1 -1
  35. package/dist/release.js +5 -0
  36. package/dist/release.js.map +1 -1
  37. package/dist/utils/run-command.d.ts.map +1 -1
  38. package/dist/utils/run-command.js +9 -0
  39. package/dist/utils/run-command.js.map +1 -1
  40. package/package.json +3 -2
  41. package/src/apis/environment.tsx +60 -0
  42. package/src/cli.tsx +90 -2
  43. package/src/components/actions.tsx +11 -6
  44. package/src/components/list.tsx +13 -0
  45. package/src/examples/list-with-toast.tsx +35 -0
  46. package/src/examples/list-with-toast.vitest.tsx +134 -0
  47. package/src/extensions/dev.tsx +1 -1
  48. package/src/index.tsx +12 -0
  49. package/src/internal/dialog.tsx +3 -1
  50. package/src/internal/navigation.tsx +33 -1
  51. package/src/keyboard.tsx +118 -0
  52. package/src/release.tsx +5 -0
  53. package/src/utils/run-command.tsx +10 -0
package/src/cli.tsx CHANGED
@@ -138,9 +138,10 @@ cli
138
138
  .on('add', rebuild)
139
139
  .on('unlink', rebuild)
140
140
  .on('error', (error) => logger.error('Watcher error:', error))
141
- } catch (e) {
141
+ } catch (e: any) {
142
+ console.error('Failed to start dev mode:', e?.message || e)
142
143
  logger.error(e)
143
- logger.log(`failed to start dev`, e)
144
+ process.exit(1)
144
145
  }
145
146
  })
146
147
 
@@ -364,6 +365,93 @@ cli
364
365
  }
365
366
  })
366
367
 
368
+ cli
369
+ .command('download <extensionName>', 'Download extension from Raycast extensions repo')
370
+ .option('-o, --output <path>', 'Output directory', { default: '.' })
371
+ .action(async (extensionName: string, options: { output: string }) => {
372
+ try {
373
+ const destPath = path.resolve(options.output)
374
+ const extensionDir = path.join(destPath, extensionName)
375
+
376
+ console.log(`Downloading extension '${extensionName}' from raycast/extensions...`)
377
+
378
+ if (fs.existsSync(extensionDir)) {
379
+ console.log(`Removing existing directory: ${extensionDir}`)
380
+ fs.rmSync(extensionDir, { recursive: true, force: true })
381
+ }
382
+
383
+ fs.mkdirSync(destPath, { recursive: true })
384
+
385
+ const repoUrl = 'https://github.com/raycast/extensions.git'
386
+ const cloneCmd = `git clone -n --depth=1 --filter=tree:0 "${repoUrl}" "${extensionName}"`
387
+ console.log(`Running: ${cloneCmd}`)
388
+ try {
389
+ execSync(cloneCmd, {
390
+ cwd: destPath,
391
+ stdio: 'inherit',
392
+ })
393
+ } catch (error) {
394
+ console.error(`Failed to clone repository`)
395
+ process.exit(1)
396
+ }
397
+
398
+ const sparseCmd = `git sparse-checkout set --no-cone "extensions/${extensionName}"`
399
+ console.log(`Running: ${sparseCmd}`)
400
+ try {
401
+ execSync(sparseCmd, {
402
+ cwd: extensionDir,
403
+ stdio: 'inherit',
404
+ })
405
+ } catch (error) {
406
+ console.error(`Failed to set sparse-checkout`)
407
+ process.exit(1)
408
+ }
409
+
410
+ const checkoutCmd = 'git checkout'
411
+ console.log(`Running: ${checkoutCmd}`)
412
+ try {
413
+ execSync(checkoutCmd, {
414
+ cwd: extensionDir,
415
+ stdio: 'inherit',
416
+ })
417
+ } catch (error) {
418
+ console.error(`Failed to checkout files`)
419
+ process.exit(1)
420
+ }
421
+
422
+ const extensionPath = path.join(extensionDir, 'extensions', extensionName)
423
+
424
+ if (!fs.existsSync(extensionPath)) {
425
+ console.error(`Extension '${extensionName}' not found in raycast/extensions repo`)
426
+ fs.rmSync(extensionDir, { recursive: true, force: true })
427
+ process.exit(1)
428
+ }
429
+
430
+ const filesToMove = fs.readdirSync(extensionPath)
431
+ for (const file of filesToMove) {
432
+ const src = path.join(extensionPath, file)
433
+ const dest = path.join(extensionDir, file)
434
+ fs.renameSync(src, dest)
435
+ }
436
+
437
+ fs.rmSync(path.join(extensionDir, 'extensions'), { recursive: true, force: true })
438
+ fs.rmSync(path.join(extensionDir, '.git'), { recursive: true, force: true })
439
+
440
+ console.log(`\nInstalling dependencies...`)
441
+ execSync('npm install', {
442
+ cwd: extensionDir,
443
+ stdio: 'inherit',
444
+ })
445
+
446
+ console.log(`\n✅ Extension downloaded successfully!`)
447
+ console.log(`📁 Path: ${extensionDir}`)
448
+ process.exit(0)
449
+ } catch (error) {
450
+ console.error('Error downloading extension:', error)
451
+ process.exit(1)
452
+ }
453
+ })
454
+
367
455
  cli.command('', 'List and run installed extensions').action(async () => {
368
456
  await runHomeCommand()
369
457
  })
@@ -20,6 +20,11 @@ import { useDialog } from 'termcast/src/internal/dialog'
20
20
  import { Dropdown } from 'termcast/src/components/dropdown'
21
21
  import { useIsInFocus } from 'termcast/src/internal/focus-context'
22
22
  import { CommonProps } from 'termcast/src/utils'
23
+ import type {
24
+ KeyboardShortcut,
25
+ KeyboardKeyEquivalent,
26
+ KeyboardKeyModifier,
27
+ } from 'termcast/src/keyboard'
23
28
  import { showToast, Toast } from 'termcast/src/apis/toast'
24
29
  import { createDescendants } from 'termcast/src/descendants'
25
30
  import { useFormSubmit } from 'termcast/src/components/form/index'
@@ -35,8 +40,8 @@ export interface ActionProps extends CommonProps {
35
40
  icon?: string | null
36
41
  style?: ActionStyle
37
42
  shortcut?: {
38
- modifiers?: string[]
39
- key: string
43
+ modifiers?: KeyboardKeyModifier[]
44
+ key: KeyboardKeyEquivalent
40
45
  } | null
41
46
  onAction?: () => void
42
47
  autoFocus?: boolean
@@ -148,7 +153,7 @@ interface PickDateProps extends Omit<ActionProps, 'onAction'> {
148
153
  // Create descendants for Actions - minimal fields needed
149
154
  interface ActionDescendant {
150
155
  title: string
151
- shortcut?: { modifiers?: string[]; key: string } | null
156
+ shortcut?: { modifiers?: KeyboardKeyModifier[]; key: KeyboardKeyEquivalent } | null
152
157
  execute: () => void
153
158
  }
154
159
 
@@ -586,14 +591,14 @@ interface ActionPanelSubmenuProps extends ActionPanelSectionProps {
586
591
  title: string
587
592
  icon?: string | null
588
593
  shortcut?: {
589
- modifiers?: string[]
590
- key: string
594
+ modifiers?: KeyboardKeyModifier[]
595
+ key: KeyboardKeyEquivalent
591
596
  } | null
592
597
  }
593
598
 
594
599
  // Helper function to format shortcuts for display
595
600
  function formatShortcut(
596
- shortcut?: { modifiers?: string[]; key: string } | null,
601
+ shortcut?: { modifiers?: KeyboardKeyModifier[]; key: KeyboardKeyEquivalent } | null,
597
602
  ): string {
598
603
  if (!shortcut) return ''
599
604
 
@@ -767,6 +767,19 @@ export const List: ListType = (props) => {
767
767
  }
768
768
  }, [selectedItemId])
769
769
 
770
+ // Call onSelectionChange when selection changes
771
+ useEffect(() => {
772
+ if (!onSelectionChange) return
773
+
774
+ const items = Object.values(descendantsContext.map.current)
775
+ .filter((item) => item.index !== -1)
776
+ .sort((a, b) => a.index - b.index)
777
+
778
+ const currentItem = items.find((item) => item.index === selectedIndex)
779
+ const selectedId = currentItem?.props?.id ?? null
780
+ onSelectionChange(selectedId)
781
+ }, [selectedIndex])
782
+
770
783
  const scrollToItem = (item: { props?: ListItemDescendant }) => {
771
784
  const scrollBox = scrollBoxRef.current
772
785
  const elementRef = item.props?.elementRef
@@ -0,0 +1,35 @@
1
+ import { renderWithProviders, List, showToast, Toast } from 'termcast'
2
+
3
+ const items = [
4
+ { id: '1', title: 'First Item' },
5
+ { id: '2', title: 'Second Item' },
6
+ { id: '3', title: 'Third Item' },
7
+ { id: '4', title: 'Fourth Item' },
8
+ { id: '5', title: 'Fifth Item' },
9
+ ]
10
+
11
+ function ListWithToast() {
12
+ const handleSelectionChange = async (id: string | null) => {
13
+ if (id) {
14
+ const item = items.find((i) => i.id === id)
15
+ await showToast({
16
+ style: Toast.Style.Success,
17
+ title: 'Selected',
18
+ message: item?.title,
19
+ })
20
+ }
21
+ }
22
+
23
+ return (
24
+ <List
25
+ navigationTitle='List With Toast'
26
+ onSelectionChange={handleSelectionChange}
27
+ >
28
+ {items.map((item) => (
29
+ <List.Item key={item.id} id={item.id} title={item.title} />
30
+ ))}
31
+ </List>
32
+ )
33
+ }
34
+
35
+ await renderWithProviders(<ListWithToast />)
@@ -0,0 +1,134 @@
1
+ import { test, expect, afterEach, beforeEach } from 'vitest'
2
+ import { launchTerminal, Session } from 'tuistory/src'
3
+
4
+ let session: Session
5
+
6
+ beforeEach(async () => {
7
+ session = await launchTerminal({
8
+ command: 'bun',
9
+ args: ['src/examples/list-with-toast.tsx'],
10
+ cols: 70,
11
+ rows: 20,
12
+ })
13
+ })
14
+
15
+ afterEach(() => {
16
+ session?.close()
17
+ })
18
+
19
+ test('list navigation works while toast is shown', async () => {
20
+ await session.text({
21
+ waitFor: (text) => {
22
+ return /First Item/i.test(text)
23
+ },
24
+ })
25
+
26
+ const initialSnapshot = await session.text()
27
+ expect(initialSnapshot).toMatchInlineSnapshot(`
28
+ "
29
+
30
+
31
+ List With Toast ────────────────────────────────────────────────
32
+
33
+ Search...
34
+
35
+ ›First Item
36
+ Second Item
37
+ Third Item
38
+ Fourth Item
39
+ Fifth Item
40
+
41
+
42
+
43
+
44
+
45
+
46
+ ↵ select ↑↓ naviga┌─────────────────────────┐
47
+ │ ✓ Selected - First Item │
48
+ └─────────────────────────┘"
49
+ `)
50
+
51
+ await session.press('down')
52
+ await new Promise((r) => setTimeout(r, 200))
53
+
54
+ const afterFirstDown = await session.text()
55
+ expect(afterFirstDown).toMatchInlineSnapshot(`
56
+ "
57
+
58
+
59
+ List With Toast ────────────────────────────────────────────────
60
+
61
+ Search...
62
+
63
+ First Item
64
+ ›Second Item
65
+ Third Item
66
+ Fourth Item
67
+ Fifth Item
68
+
69
+
70
+
71
+
72
+
73
+
74
+ ↵ select ↑↓ navig┌──────────────────────────┐
75
+ │ ✓ Selected - Second Item │
76
+ └──────────────────────────┘"
77
+ `)
78
+
79
+ await session.press('down')
80
+ await new Promise((r) => setTimeout(r, 200))
81
+
82
+ const afterSecondDown = await session.text()
83
+ expect(afterSecondDown).toMatchInlineSnapshot(`
84
+ "
85
+
86
+
87
+ List With Toast ────────────────────────────────────────────────
88
+
89
+ Search...
90
+
91
+ First Item
92
+ Second Item
93
+ ›Third Item
94
+ Fourth Item
95
+ Fifth Item
96
+
97
+
98
+
99
+
100
+
101
+
102
+ ↵ select ↑↓ naviga┌─────────────────────────┐
103
+ │ ✓ Selected - Third Item │
104
+ └─────────────────────────┘"
105
+ `)
106
+
107
+ await session.press('up')
108
+ await new Promise((r) => setTimeout(r, 200))
109
+
110
+ const afterUp = await session.text()
111
+ expect(afterUp).toMatchInlineSnapshot(`
112
+ "
113
+
114
+
115
+ List With Toast ────────────────────────────────────────────────
116
+
117
+ Search...
118
+
119
+ First Item
120
+ ›Second Item
121
+ Third Item
122
+ Fourth Item
123
+ Fifth Item
124
+
125
+
126
+
127
+
128
+
129
+
130
+ ↵ select ↑↓ navig┌──────────────────────────┐
131
+ │ ✓ Selected - Second Item │
132
+ └──────────────────────────┘"
133
+ `)
134
+ }, 10000)
@@ -68,7 +68,7 @@ function ExtensionCommandsList({
68
68
  searchBarPlaceholder='Search commands...'
69
69
  >
70
70
  <List.Section title='Commands'>
71
- {commands.map((command) => (
71
+ {commands.filter((cmd) => cmd.mode !== 'menu-bar').map((command) => (
72
72
  <List.Item
73
73
  key={command.name}
74
74
  id={command.name}
package/src/index.tsx CHANGED
@@ -133,7 +133,9 @@ export { Color } from 'termcast/src/colors'
133
133
  // Navigation
134
134
  export {
135
135
  useNavigation,
136
+ NavigationProvider,
136
137
  NavigationContainer,
138
+ popToRoot,
137
139
  } from 'termcast/src/internal/navigation'
138
140
 
139
141
  // Hooks
@@ -185,6 +187,7 @@ export {
185
187
  environment,
186
188
  getSelectedFinderItems,
187
189
  getSelectedText,
190
+ launchCommand,
188
191
  } from 'termcast/src/apis/environment'
189
192
  export type { Environment, LaunchProps } from 'termcast/src/apis/environment'
190
193
 
@@ -223,5 +226,14 @@ export { closeMainWindow, PopToRootType } from 'termcast/src/apis/window'
223
226
  // HUD
224
227
  export { showHUD } from 'termcast/src/apis/hud'
225
228
 
229
+ // Keyboard
230
+ export { Keyboard } from 'termcast/src/keyboard'
231
+ export type {
232
+ KeyboardKeyEquivalent,
233
+ KeyboardKeyModifier,
234
+ KeyboardShortcut,
235
+ KeyboardCrossPlatformShortcut,
236
+ } from 'termcast/src/keyboard'
237
+
226
238
  // Compile support
227
239
  export { startCompiledExtension } from 'termcast/src/extensions/dev'
@@ -170,7 +170,9 @@ export function DialogProvider(props: DialogProviderProps): any {
170
170
  })}
171
171
  </box>
172
172
  )}
173
- <ToastOverlay />
173
+ <InFocus inFocus={false}>
174
+ <ToastOverlay />
175
+ </InFocus>
174
176
  </>
175
177
  )
176
178
  }
@@ -17,6 +17,7 @@ import { logger } from '../logger'
17
17
  interface Navigation {
18
18
  push: (element: ReactNode, onPop?: () => void) => void
19
19
  pop: () => void
20
+ popToRoot: () => void
20
21
  }
21
22
 
22
23
  interface NavigationContextType {
@@ -80,12 +81,29 @@ export function NavigationProvider(props: NavigationProviderProps): any {
80
81
  })
81
82
  }, [])
82
83
 
84
+ const popToRoot = useCallback(() => {
85
+ const currentStack = useStore.getState().navigationStack
86
+ if (currentStack.length <= 1) return
87
+
88
+ const poppedItems = currentStack.slice(1)
89
+ for (const item of poppedItems.reverse()) {
90
+ if (item?.onPop) {
91
+ item.onPop()
92
+ }
93
+ }
94
+
95
+ startNavigationTransition(() => {
96
+ useStore.setState({ navigationStack: [currentStack[0]] })
97
+ })
98
+ }, [])
99
+
83
100
  const navigation = React.useMemo(
84
101
  () => ({
85
102
  push,
86
103
  pop,
104
+ popToRoot,
87
105
  }),
88
- [push, pop],
106
+ [push, pop, popToRoot],
89
107
  )
90
108
 
91
109
  const value = React.useMemo(
@@ -138,6 +156,20 @@ export function useNavigationPending(): boolean {
138
156
  return context?.isPending || false
139
157
  }
140
158
 
159
+ export async function popToRoot(): Promise<void> {
160
+ const currentStack = useStore.getState().navigationStack
161
+ if (currentStack.length <= 1) return
162
+
163
+ const poppedItems = currentStack.slice(1)
164
+ for (const item of poppedItems.reverse()) {
165
+ if (item?.onPop) {
166
+ item.onPop()
167
+ }
168
+ }
169
+
170
+ useStore.setState({ navigationStack: [currentStack[0]] })
171
+ }
172
+
141
173
  interface NavigationContainerProps extends CommonProps {
142
174
  children: ReactNode
143
175
  }
@@ -0,0 +1,118 @@
1
+ export type KeyEquivalent =
2
+ | 'a'
3
+ | 'b'
4
+ | 'c'
5
+ | 'd'
6
+ | 'e'
7
+ | 'f'
8
+ | 'g'
9
+ | 'h'
10
+ | 'i'
11
+ | 'j'
12
+ | 'k'
13
+ | 'l'
14
+ | 'm'
15
+ | 'n'
16
+ | 'o'
17
+ | 'p'
18
+ | 'q'
19
+ | 'r'
20
+ | 's'
21
+ | 't'
22
+ | 'u'
23
+ | 'v'
24
+ | 'w'
25
+ | 'x'
26
+ | 'y'
27
+ | 'z'
28
+ | '0'
29
+ | '1'
30
+ | '2'
31
+ | '3'
32
+ | '4'
33
+ | '5'
34
+ | '6'
35
+ | '7'
36
+ | '8'
37
+ | '9'
38
+ | '.'
39
+ | ','
40
+ | ';'
41
+ | '='
42
+ | '+'
43
+ | '-'
44
+ | '['
45
+ | ']'
46
+ | '{'
47
+ | '}'
48
+ | '«'
49
+ | '»'
50
+ | '('
51
+ | ')'
52
+ | '/'
53
+ | '\\'
54
+ | "'"
55
+ | '`'
56
+ | '§'
57
+ | '^'
58
+ | '@'
59
+ | '$'
60
+ | 'return'
61
+ | 'delete'
62
+ | 'deleteForward'
63
+ | 'tab'
64
+ | 'arrowUp'
65
+ | 'arrowDown'
66
+ | 'arrowLeft'
67
+ | 'arrowRight'
68
+ | 'pageUp'
69
+ | 'pageDown'
70
+ | 'home'
71
+ | 'end'
72
+ | 'space'
73
+ | 'escape'
74
+ | 'enter'
75
+ | 'backspace'
76
+
77
+ export type KeyModifier = 'cmd' | 'ctrl' | 'opt' | 'shift' | 'alt' | 'windows'
78
+
79
+ export interface Shortcut {
80
+ key: KeyEquivalent
81
+ modifiers: KeyModifier[]
82
+ }
83
+
84
+ export interface CrossPlatformShortcut {
85
+ macOS: Shortcut
86
+ Windows: Shortcut
87
+ }
88
+
89
+ const CommonShortcuts = {
90
+ Copy: { modifiers: ['cmd', 'shift'], key: 'c' } as Shortcut,
91
+ CopyDeeplink: { modifiers: ['cmd', 'shift'], key: 'c' } as Shortcut,
92
+ CopyName: { modifiers: ['cmd', 'shift'], key: '.' } as Shortcut,
93
+ CopyPath: { modifiers: ['cmd', 'shift'], key: ',' } as Shortcut,
94
+ Save: { modifiers: ['cmd'], key: 's' } as Shortcut,
95
+ Duplicate: { modifiers: ['cmd'], key: 'd' } as Shortcut,
96
+ Edit: { modifiers: ['cmd'], key: 'e' } as Shortcut,
97
+ MoveDown: { modifiers: ['cmd', 'shift'], key: 'arrowDown' } as Shortcut,
98
+ MoveUp: { modifiers: ['cmd', 'shift'], key: 'arrowUp' } as Shortcut,
99
+ New: { modifiers: ['cmd'], key: 'n' } as Shortcut,
100
+ Open: { modifiers: ['cmd'], key: 'o' } as Shortcut,
101
+ OpenWith: { modifiers: ['cmd', 'shift'], key: 'o' } as Shortcut,
102
+ Pin: { modifiers: ['cmd', 'shift'], key: 'p' } as Shortcut,
103
+ Refresh: { modifiers: ['cmd'], key: 'r' } as Shortcut,
104
+ Remove: { modifiers: ['ctrl'], key: 'x' } as Shortcut,
105
+ RemoveAll: { modifiers: ['ctrl', 'shift'], key: 'x' } as Shortcut,
106
+ ToggleQuickLook: { modifiers: ['cmd'], key: 'y' } as Shortcut,
107
+ } as const
108
+
109
+ export const Keyboard = {
110
+ Shortcut: {
111
+ Common: CommonShortcuts,
112
+ },
113
+ } as const
114
+
115
+ export type { KeyEquivalent as KeyboardKeyEquivalent }
116
+ export type { KeyModifier as KeyboardKeyModifier }
117
+ export type { Shortcut as KeyboardShortcut }
118
+ export type { CrossPlatformShortcut as KeyboardCrossPlatformShortcut }
package/src/release.tsx CHANGED
@@ -66,6 +66,11 @@ export async function releaseExtension({
66
66
  if (error.message?.includes('already exists')) {
67
67
  throw error
68
68
  }
69
+ const stderr = error.stderr?.toString() || ''
70
+ const isNotFound = stderr.includes('release not found') || stderr.includes('Not Found')
71
+ if (!isNotFound) {
72
+ throw new Error(`Failed to check release status: ${stderr || error.message}`)
73
+ }
69
74
  }
70
75
 
71
76
  // Install dependencies for all platforms
@@ -122,6 +122,16 @@ export async function runCommand(options: RunCommandOptions): Promise<void> {
122
122
  launchContext: undefined,
123
123
  }
124
124
 
125
+ // Menu bar commands are not supported in termcast
126
+ if (command.mode === 'menu-bar') {
127
+ await showToast({
128
+ style: Toast.Style.Failure,
129
+ title: 'Unsupported command type',
130
+ message: 'Menu bar commands are not supported',
131
+ })
132
+ return
133
+ }
134
+
125
135
  // Handle no-view commands (they export an async function, not a component)
126
136
  if (command.mode === 'no-view') {
127
137
  if (typeof CommandComponent === 'function') {