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.
- package/dist/apis/cache.d.ts.map +1 -1
- package/dist/apis/cache.js +4 -39
- package/dist/apis/cache.js.map +1 -1
- package/dist/apis/hud.d.ts.map +1 -1
- package/dist/apis/hud.js +13 -31
- package/dist/apis/hud.js.map +1 -1
- package/dist/apis/localstorage.d.ts.map +1 -1
- package/dist/apis/localstorage.js +3 -27
- package/dist/apis/localstorage.js.map +1 -1
- package/dist/apis/toast.d.ts +16 -43
- package/dist/apis/toast.d.ts.map +1 -1
- package/dist/apis/toast.js +78 -177
- package/dist/apis/toast.js.map +1 -1
- package/dist/build.d.ts +3 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +52 -2
- package/dist/build.js.map +1 -1
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +206 -25
- package/dist/cli.js.map +1 -1
- package/dist/colors.d.ts.map +1 -1
- package/dist/colors.js +1 -0
- package/dist/colors.js.map +1 -1
- package/dist/compile.d.ts +0 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +18 -23
- package/dist/compile.js.map +1 -1
- package/dist/components/actions.d.ts.map +1 -1
- package/dist/components/actions.js +30 -15
- package/dist/components/actions.js.map +1 -1
- package/dist/components/animation-tick.d.ts +12 -0
- package/dist/components/animation-tick.d.ts.map +1 -0
- package/dist/components/animation-tick.js +63 -0
- package/dist/components/animation-tick.js.map +1 -0
- package/dist/components/detail.d.ts.map +1 -1
- package/dist/components/detail.js +10 -13
- package/dist/components/detail.js.map +1 -1
- package/dist/components/dropdown.d.ts +1 -0
- package/dist/components/dropdown.d.ts.map +1 -1
- package/dist/components/dropdown.js +27 -26
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/extension-preferences.d.ts.map +1 -1
- package/dist/components/extension-preferences.js +15 -10
- package/dist/components/extension-preferences.js.map +1 -1
- package/dist/components/footer.d.ts +13 -0
- package/dist/components/footer.d.ts.map +1 -0
- package/dist/components/footer.js +106 -0
- package/dist/components/footer.js.map +1 -0
- package/dist/components/form/file-autocomplete.d.ts +19 -4
- package/dist/components/form/file-autocomplete.d.ts.map +1 -1
- package/dist/components/form/file-autocomplete.js +56 -55
- package/dist/components/form/file-autocomplete.js.map +1 -1
- package/dist/components/form/file-picker.d.ts.map +1 -1
- package/dist/components/form/file-picker.js +26 -15
- package/dist/components/form/file-picker.js.map +1 -1
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +17 -15
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/form/with-left-border.d.ts.map +1 -1
- package/dist/components/form/with-left-border.js +4 -12
- package/dist/components/form/with-left-border.js.map +1 -1
- package/dist/components/list.d.ts.map +1 -1
- package/dist/components/list.js +126 -86
- package/dist/components/list.js.map +1 -1
- package/dist/components/loading-bar.d.ts.map +1 -1
- package/dist/components/loading-bar.js +5 -22
- package/dist/components/loading-bar.js.map +1 -1
- package/dist/components/loading-text.d.ts.map +1 -1
- package/dist/components/loading-text.js +3 -22
- package/dist/components/loading-text.js.map +1 -1
- package/dist/components/theme-picker.d.ts +2 -0
- package/dist/components/theme-picker.d.ts.map +1 -0
- package/dist/components/theme-picker.js +37 -0
- package/dist/components/theme-picker.js.map +1 -0
- package/dist/descendants.d.ts +6 -0
- package/dist/descendants.d.ts.map +1 -1
- package/dist/descendants.js +74 -8
- package/dist/descendants.js.map +1 -1
- package/dist/examples/internal/descendants-rerender.d.ts +14 -0
- package/dist/examples/internal/descendants-rerender.d.ts.map +1 -0
- package/dist/examples/internal/descendants-rerender.js +145 -0
- package/dist/examples/internal/descendants-rerender.js.map +1 -0
- package/dist/examples/internal/simple-dialog.js +4 -1
- package/dist/examples/internal/simple-dialog.js.map +1 -1
- package/dist/examples/internal/simple-scrollbox.js +1 -1
- package/dist/examples/internal/simple-scrollbox.js.map +1 -1
- package/dist/examples/list-with-dropdown.js +1 -1
- package/dist/examples/list-with-dropdown.js.map +1 -1
- package/dist/examples/miscellaneous.js +1 -1
- package/dist/examples/miscellaneous.js.map +1 -1
- package/dist/examples/toast-action.d.ts +2 -0
- package/dist/examples/toast-action.d.ts.map +1 -0
- package/dist/examples/toast-action.js +76 -0
- package/dist/examples/toast-action.js.map +1 -0
- package/dist/examples/toast-variations.js +38 -36
- package/dist/examples/toast-variations.js.map +1 -1
- package/dist/extensions/dev.d.ts +1 -1
- package/dist/extensions/dev.d.ts.map +1 -1
- package/dist/extensions/dev.js +62 -30
- package/dist/extensions/dev.js.map +1 -1
- package/dist/extensions/home.d.ts.map +1 -1
- package/dist/extensions/home.js +4 -3
- package/dist/extensions/home.js.map +1 -1
- package/dist/extensions/react-refresh-init.d.ts +5 -0
- package/dist/extensions/react-refresh-init.d.ts.map +1 -0
- package/dist/extensions/react-refresh-init.js +52 -0
- package/dist/extensions/react-refresh-init.js.map +1 -0
- package/dist/internal/date-picker-widget.js +1 -1
- package/dist/internal/date-picker-widget.js.map +1 -1
- package/dist/internal/dialog.d.ts +8 -3
- package/dist/internal/dialog.d.ts.map +1 -1
- package/dist/internal/dialog.js +37 -53
- package/dist/internal/dialog.js.map +1 -1
- package/dist/internal/navigation.d.ts +1 -0
- package/dist/internal/navigation.d.ts.map +1 -1
- package/dist/internal/navigation.js +25 -1
- package/dist/internal/navigation.js.map +1 -1
- package/dist/internal/providers.d.ts.map +1 -1
- package/dist/internal/providers.js +9 -197
- package/dist/internal/providers.js.map +1 -1
- package/dist/internal/scrollbox.d.ts.map +1 -1
- package/dist/internal/scrollbox.js +1 -0
- package/dist/internal/scrollbox.js.map +1 -1
- package/dist/release.d.ts +1 -0
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +16 -9
- package/dist/release.js.map +1 -1
- package/dist/state.d.ts +27 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +6 -0
- package/dist/state.js.map +1 -1
- package/dist/theme.d.ts +6 -19
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +76 -45
- package/dist/theme.js.map +1 -1
- package/dist/themes/aura.json +69 -0
- package/dist/themes/ayu.json +80 -0
- package/dist/themes/catppuccin-frappe.json +233 -0
- package/dist/themes/catppuccin-macchiato.json +233 -0
- package/dist/themes/catppuccin.json +112 -0
- package/dist/themes/cobalt2.json +228 -0
- package/dist/themes/cursor.json +249 -0
- package/dist/themes/dracula.json +219 -0
- package/dist/themes/everforest.json +241 -0
- package/dist/themes/flexoki.json +237 -0
- package/dist/themes/github-light.json +56 -0
- package/dist/themes/github.json +241 -0
- package/dist/themes/gruvbox.json +95 -0
- package/dist/themes/kanagawa.json +77 -0
- package/dist/themes/lucent-orng.json +227 -0
- package/dist/themes/material.json +235 -0
- package/dist/themes/matrix.json +77 -0
- package/dist/themes/mercury.json +245 -0
- package/dist/themes/monokai.json +221 -0
- package/dist/themes/nightowl.json +221 -0
- package/dist/themes/nord.json +223 -0
- package/dist/themes/one-dark.json +84 -0
- package/dist/themes/opencode-light.json +62 -0
- package/dist/themes/opencode.json +245 -0
- package/dist/themes/orng.json +245 -0
- package/dist/themes/palenight.json +222 -0
- package/dist/themes/rosepine.json +234 -0
- package/dist/themes/solarized.json +223 -0
- package/dist/themes/synthwave84.json +226 -0
- package/dist/themes/termcast.json +226 -0
- package/dist/themes/tokyonight.json +243 -0
- package/dist/themes/vercel.json +255 -0
- package/dist/themes/vesper.json +218 -0
- package/dist/themes/zenburn.json +223 -0
- package/dist/themes.d.ts +57 -0
- package/dist/themes.d.ts.map +1 -0
- package/dist/themes.js +181 -0
- package/dist/themes.js.map +1 -0
- package/dist/utils/run-command.d.ts +2 -1
- package/dist/utils/run-command.d.ts.map +1 -1
- package/dist/utils/run-command.js +20 -10
- package/dist/utils/run-command.js.map +1 -1
- package/dist/utils.d.ts +2 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +90 -17
- package/dist/utils.js.map +1 -1
- package/dist/watcher.d.ts +3 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +16 -0
- package/dist/watcher.js.map +1 -0
- package/package.json +16 -10
- package/src/apis/cache.tsx +5 -44
- package/src/apis/hud.tsx +17 -62
- package/src/apis/localstorage.tsx +3 -32
- package/src/apis/toast.tsx +91 -275
- package/src/build.test.tsx +10 -0
- package/src/build.tsx +61 -1
- package/src/cli.tsx +365 -103
- package/src/colors.tsx +1 -0
- package/src/compile.tsx +21 -29
- package/src/compile.vitest.tsx +300 -0
- package/src/components/actions.tsx +64 -45
- package/src/components/animation-tick.tsx +85 -0
- package/src/components/detail.tsx +31 -35
- package/src/components/dropdown.tsx +32 -21
- package/src/components/extension-preferences.tsx +14 -10
- package/src/components/footer.tsx +241 -0
- package/src/components/form/file-autocomplete.tsx +80 -60
- package/src/components/form/file-picker.tsx +37 -25
- package/src/components/form/index.tsx +45 -41
- package/src/components/form/with-left-border.tsx +4 -14
- package/src/components/list.tsx +181 -121
- package/src/components/loading-bar.tsx +5 -25
- package/src/components/loading-text.tsx +4 -23
- package/src/components/theme-picker.tsx +57 -0
- package/src/descendants.tsx +98 -9
- package/src/examples/actions-dialog-layout.vitest.tsx +112 -0
- package/src/examples/file-autocomplete.vitest.tsx +131 -122
- package/src/examples/form-basic.vitest.tsx +463 -644
- package/src/examples/form-dropdown.vitest.tsx +553 -571
- package/src/examples/form-scroll.vitest.tsx +112 -102
- package/src/examples/form-tagpicker.vitest.tsx +364 -338
- package/src/examples/internal/descendants-rerender.tsx +273 -0
- package/src/examples/internal/descendants-rerender.vitest.tsx +194 -0
- package/src/examples/internal/simple-dialog.tsx +4 -4
- package/src/examples/internal/simple-scrollbox.tsx +2 -2
- package/src/examples/internal/simple-scrollbox.vitest.tsx +43 -31
- package/src/examples/list-detail-metadata.vitest.tsx +34 -30
- package/src/examples/list-dropdown-default.vitest.tsx +84 -72
- package/src/examples/list-empty-view.vitest.tsx +93 -0
- package/src/examples/list-fetch-data.vitest.tsx +36 -30
- package/src/examples/list-scrollbox.vitest.tsx +59 -39
- package/src/examples/list-with-detail.vitest.tsx +339 -314
- package/src/examples/list-with-dropdown.tsx +1 -0
- package/src/examples/list-with-dropdown.vitest.tsx +176 -150
- package/src/examples/list-with-sections.vitest.tsx +289 -270
- package/src/examples/list-with-toast.vitest.tsx +44 -44
- package/src/examples/miscellaneous.tsx +10 -0
- package/src/examples/simple-file-picker.vitest.tsx +90 -86
- package/src/examples/simple-grid.vitest.tsx +275 -249
- package/src/examples/simple-navigation.vitest.tsx +192 -168
- package/src/examples/store.vitest.tsx +6 -4
- package/src/examples/swift-extension.vitest.tsx +31 -19
- package/src/examples/synonyms.vitest.tsx +93 -83
- package/src/examples/toast-action.tsx +160 -0
- package/src/examples/toast-action.vitest.tsx +404 -0
- package/src/examples/toast-variations.tsx +58 -57
- package/src/examples/toast-variations.vitest.tsx +186 -166
- package/src/extensions/dev.tsx +74 -33
- package/src/extensions/dev.vitest.tsx +162 -69
- package/src/extensions/home.tsx +5 -6
- package/src/extensions/react-refresh-init.tsx +59 -0
- package/src/internal/date-picker-widget.tsx +1 -1
- package/src/internal/dialog.tsx +59 -83
- package/src/internal/navigation.tsx +37 -4
- package/src/internal/providers.tsx +27 -315
- package/src/internal/scrollbox.tsx +1 -0
- package/src/release.tsx +16 -10
- package/src/state.tsx +36 -3
- package/src/theme.tsx +82 -51
- package/src/themes/aura.json +69 -0
- package/src/themes/ayu.json +80 -0
- package/src/themes/catppuccin-frappe.json +233 -0
- package/src/themes/catppuccin-macchiato.json +233 -0
- package/src/themes/catppuccin.json +112 -0
- package/src/themes/cobalt2.json +228 -0
- package/src/themes/cursor.json +249 -0
- package/src/themes/dracula.json +219 -0
- package/src/themes/everforest.json +241 -0
- package/src/themes/flexoki.json +237 -0
- package/src/themes/github-light.json +56 -0
- package/src/themes/github.json +241 -0
- package/src/themes/gruvbox.json +95 -0
- package/src/themes/kanagawa.json +77 -0
- package/src/themes/lucent-orng.json +227 -0
- package/src/themes/material.json +235 -0
- package/src/themes/matrix.json +77 -0
- package/src/themes/mercury.json +252 -0
- package/src/themes/monokai.json +221 -0
- package/src/themes/nightowl.json +221 -0
- package/src/themes/nord.json +223 -0
- package/src/themes/one-dark.json +84 -0
- package/src/themes/opencode-light.json +62 -0
- package/src/themes/opencode.json +245 -0
- package/src/themes/orng.json +245 -0
- package/src/themes/palenight.json +222 -0
- package/src/themes/rosepine.json +234 -0
- package/src/themes/solarized.json +223 -0
- package/src/themes/synthwave84.json +226 -0
- package/src/themes/termcast.json +227 -0
- package/src/themes/tokyonight.json +243 -0
- package/src/themes/vercel.json +255 -0
- package/src/themes/vesper.json +218 -0
- package/src/themes/zenburn.json +223 -0
- package/src/themes.ts +291 -0
- package/src/utils/run-command.tsx +23 -12
- package/src/utils.tsx +115 -18
- package/src/watcher.tsx +19 -0
package/src/extensions/dev.tsx
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
// CRITICAL: Import react-refresh-init FIRST before @opentui/react!
|
|
2
|
+
// This ensures the devtools hook exists and can intercept injectIntoDevTools calls
|
|
3
|
+
import { RefreshRuntime, hasRefreshCapability, getRendererInternals } from './react-refresh-init'
|
|
4
|
+
|
|
1
5
|
import fs from 'node:fs'
|
|
2
6
|
import path from 'node:path'
|
|
7
|
+
import os from 'node:os'
|
|
3
8
|
import React from 'react'
|
|
4
9
|
import { createRoot } from '@opentui/react'
|
|
5
10
|
import { createCliRenderer } from '@opentui/core'
|
|
@@ -9,6 +14,8 @@ import { useNavigation } from 'termcast/src/internal/navigation'
|
|
|
9
14
|
import { TermcastProvider } from 'termcast/src/internal/providers'
|
|
10
15
|
import { showToast, Toast } from 'termcast/src/apis/toast'
|
|
11
16
|
import { Icon } from 'termcast'
|
|
17
|
+
import { Theme } from 'termcast/src/theme'
|
|
18
|
+
import { logger } from '../logger'
|
|
12
19
|
import { getCommandsWithFiles, CommandWithFile, RaycastPackageJson } from '../package-json'
|
|
13
20
|
import { buildExtensionCommands } from '../build'
|
|
14
21
|
import {
|
|
@@ -18,9 +25,17 @@ import {
|
|
|
18
25
|
handleHelpFlag,
|
|
19
26
|
} from '../utils/run-command'
|
|
20
27
|
|
|
28
|
+
function CommandLoadError({ error }: { error: Error }): any {
|
|
29
|
+
return (
|
|
30
|
+
<box padding={2}>
|
|
31
|
+
<text fg={Theme.error} wrapMode='none'>{error.stack}</text>
|
|
32
|
+
</box>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
interface BundledCommand extends CommandWithFile {
|
|
22
37
|
bundledPath: string
|
|
23
|
-
|
|
38
|
+
loadComponent?: () => Promise<(props: any) => any>
|
|
24
39
|
}
|
|
25
40
|
|
|
26
41
|
function ExtensionCommandsList({
|
|
@@ -32,19 +47,17 @@ function ExtensionCommandsList({
|
|
|
32
47
|
commands: BundledCommand[]
|
|
33
48
|
skipArgv?: number
|
|
34
49
|
}): any {
|
|
35
|
-
const { push } = useNavigation()
|
|
50
|
+
const { push, replace } = useNavigation()
|
|
51
|
+
const [loadError, setLoadError] = React.useState<Error | null>(null)
|
|
36
52
|
|
|
37
53
|
const visibleCommands = commands.filter((cmd) => cmd.mode !== 'menu-bar')
|
|
54
|
+
const isSingleCommand = visibleCommands.length === 1
|
|
38
55
|
|
|
39
|
-
const handleCommandSelect = async (command: BundledCommand) => {
|
|
56
|
+
const handleCommandSelect = async (command: BundledCommand, useReplace = false) => {
|
|
40
57
|
clearCommandArguments()
|
|
41
58
|
|
|
42
|
-
if (!command.bundledPath && !command.
|
|
43
|
-
|
|
44
|
-
style: Toast.Style.Failure,
|
|
45
|
-
title: 'Command not built',
|
|
46
|
-
message: `Command ${command.name} was not built successfully`,
|
|
47
|
-
})
|
|
59
|
+
if (!command.bundledPath && !command.loadComponent) {
|
|
60
|
+
setLoadError(new Error(`Command ${command.name} was not built successfully`))
|
|
48
61
|
return
|
|
49
62
|
}
|
|
50
63
|
|
|
@@ -54,15 +67,12 @@ function ExtensionCommandsList({
|
|
|
54
67
|
extensionName: packageJson.name,
|
|
55
68
|
packageJson,
|
|
56
69
|
bundledPath: command.bundledPath,
|
|
57
|
-
|
|
58
|
-
push,
|
|
70
|
+
loadComponent: command.loadComponent,
|
|
71
|
+
push: useReplace ? replace : push,
|
|
72
|
+
replace,
|
|
59
73
|
})
|
|
60
74
|
} catch (error: any) {
|
|
61
|
-
|
|
62
|
-
style: Toast.Style.Failure,
|
|
63
|
-
title: 'Failed to load command',
|
|
64
|
-
message: error.message || String(error),
|
|
65
|
-
})
|
|
75
|
+
setLoadError(error)
|
|
66
76
|
}
|
|
67
77
|
}
|
|
68
78
|
|
|
@@ -75,7 +85,8 @@ function ExtensionCommandsList({
|
|
|
75
85
|
if (commandName) {
|
|
76
86
|
const command = visibleCommands.find((cmd) => cmd.name === commandName)
|
|
77
87
|
if (command) {
|
|
78
|
-
|
|
88
|
+
// Use replace so ESC at root exits instead of going back to command list
|
|
89
|
+
handleCommandSelect(command, true)
|
|
79
90
|
} else {
|
|
80
91
|
showToast({
|
|
81
92
|
style: Toast.Style.Failure,
|
|
@@ -87,12 +98,18 @@ function ExtensionCommandsList({
|
|
|
87
98
|
}
|
|
88
99
|
}
|
|
89
100
|
|
|
90
|
-
if (
|
|
91
|
-
|
|
101
|
+
if (isSingleCommand) {
|
|
102
|
+
// Use replace so ESC at root exits instead of going back to command list
|
|
103
|
+
handleCommandSelect(visibleCommands[0], true)
|
|
92
104
|
}
|
|
93
105
|
}, [])
|
|
94
106
|
|
|
95
|
-
|
|
107
|
+
// Show error screen for single command that failed to load
|
|
108
|
+
if (loadError) {
|
|
109
|
+
return <CommandLoadError error={loadError} />
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (isSingleCommand) {
|
|
96
113
|
return null
|
|
97
114
|
}
|
|
98
115
|
|
|
@@ -113,7 +130,7 @@ function ExtensionCommandsList({
|
|
|
113
130
|
}
|
|
114
131
|
accessories={[
|
|
115
132
|
{ text: command.mode },
|
|
116
|
-
...(command.bundledPath
|
|
133
|
+
...(command.bundledPath || command.loadComponent
|
|
117
134
|
? []
|
|
118
135
|
: [
|
|
119
136
|
{
|
|
@@ -178,11 +195,12 @@ export async function startDevMode({
|
|
|
178
195
|
// Parse the package.json to get extension metadata
|
|
179
196
|
const { packageJson } = getCommandsWithFiles({ packageJsonPath })
|
|
180
197
|
|
|
181
|
-
// Build and set initial devElement
|
|
198
|
+
// Build and set initial devElement with hot reload support
|
|
182
199
|
const { commands } = await buildExtensionCommands({
|
|
183
200
|
extensionPath: resolvedPath,
|
|
184
201
|
format: 'esm',
|
|
185
202
|
target: 'bun',
|
|
203
|
+
hotReload: true,
|
|
186
204
|
})
|
|
187
205
|
|
|
188
206
|
// Handle --help before rendering
|
|
@@ -209,9 +227,9 @@ export async function startDevMode({
|
|
|
209
227
|
|
|
210
228
|
function App(): any {
|
|
211
229
|
const devElement = useStore((state) => state.devElement)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
return <TermcastProvider
|
|
230
|
+
// REMOVED: key={devRebuildCount} - we want to preserve the React tree!
|
|
231
|
+
// React Refresh will update components in-place without remounting
|
|
232
|
+
return <TermcastProvider>{devElement}</TermcastProvider>
|
|
215
233
|
}
|
|
216
234
|
|
|
217
235
|
const renderer = await createCliRenderer({
|
|
@@ -230,7 +248,7 @@ export async function startCompiledExtension({
|
|
|
230
248
|
packageJson: RaycastPackageJson
|
|
231
249
|
compiledCommands: Array<{
|
|
232
250
|
name: string
|
|
233
|
-
|
|
251
|
+
loadComponent: () => Promise<(props: any) => any>
|
|
234
252
|
}>
|
|
235
253
|
skipArgv?: number
|
|
236
254
|
}): Promise<void> {
|
|
@@ -248,7 +266,7 @@ export async function startCompiledExtension({
|
|
|
248
266
|
filePath: '',
|
|
249
267
|
exists: true,
|
|
250
268
|
bundledPath: '',
|
|
251
|
-
|
|
269
|
+
loadComponent: compiled.loadComponent,
|
|
252
270
|
}
|
|
253
271
|
})
|
|
254
272
|
|
|
@@ -263,9 +281,13 @@ export async function startCompiledExtension({
|
|
|
263
281
|
skipArgv,
|
|
264
282
|
})
|
|
265
283
|
|
|
284
|
+
// For compiled extensions, use ~/.termcast/compiled/{name} as extensionPath
|
|
285
|
+
// This is where data.db and cache will be stored
|
|
286
|
+
const compiledExtensionPath = path.join(os.homedir(), '.termcast', 'compiled', packageJson.name)
|
|
287
|
+
|
|
266
288
|
useStore.setState({
|
|
267
289
|
...useStore.getInitialState(),
|
|
268
|
-
extensionPath:
|
|
290
|
+
extensionPath: compiledExtensionPath,
|
|
269
291
|
extensionPackageJson: packageJson,
|
|
270
292
|
devElement: (
|
|
271
293
|
<ExtensionCommandsList packageJson={packageJson} commands={commands} skipArgv={skipArgv} />
|
|
@@ -296,28 +318,47 @@ export async function triggerRebuild({
|
|
|
296
318
|
extensionPath,
|
|
297
319
|
format: 'esm',
|
|
298
320
|
target: 'bun',
|
|
321
|
+
hotReload: true,
|
|
299
322
|
})
|
|
300
323
|
|
|
301
324
|
// Re-parse package.json in case it changed
|
|
302
325
|
const packageJsonPath = path.join(extensionPath, 'package.json')
|
|
303
326
|
const { packageJson } = getCommandsWithFiles({ packageJsonPath })
|
|
304
327
|
|
|
305
|
-
// Update the devElement with new commands and increment rebuild count
|
|
306
328
|
const state = useStore.getState()
|
|
329
|
+
const newRebuildCount = state.devRebuildCount + 1
|
|
330
|
+
|
|
331
|
+
// Re-import all command modules with cache bust
|
|
332
|
+
// This triggers $RefreshReg$ calls which register new component versions
|
|
333
|
+
// TODO maybe we can skip importing all command modules here. only the one being used instead
|
|
334
|
+
for (const cmd of commands) {
|
|
335
|
+
if (cmd.bundledPath) {
|
|
336
|
+
try {
|
|
337
|
+
await import(`${cmd.bundledPath}?v=${newRebuildCount}`)
|
|
338
|
+
} catch (err) {
|
|
339
|
+
logger.error(`Failed to reimport ${cmd.name}:`, err)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
307
343
|
|
|
344
|
+
// Trigger React Refresh - this updates components in-place!
|
|
345
|
+
// The reconciler will find all fibers using updated "families"
|
|
346
|
+
// and schedule re-renders with the new implementations
|
|
347
|
+
RefreshRuntime.performReactRefresh()
|
|
348
|
+
|
|
349
|
+
// Update state WITHOUT resetting navigation/dialog stacks
|
|
308
350
|
useStore.setState({
|
|
309
351
|
extensionPackageJson: packageJson,
|
|
352
|
+
devRebuildCount: newRebuildCount,
|
|
310
353
|
devElement: (
|
|
311
354
|
<ExtensionCommandsList
|
|
312
355
|
packageJson={packageJson}
|
|
313
356
|
commands={commands}
|
|
314
357
|
/>
|
|
315
358
|
),
|
|
316
|
-
devRebuildCount: state.devRebuildCount + 1,
|
|
317
|
-
navigationStack: [], // Reset navigation so NavigationProvider re-initializes with new devElement
|
|
318
|
-
dialogStack: [], // Clear any open dialogs/toasts on rebuild
|
|
319
|
-
toast: null,
|
|
320
359
|
})
|
|
360
|
+
|
|
361
|
+
// TODO show a green dot in the corner to notify HMR happened
|
|
321
362
|
} catch (error: any) {
|
|
322
363
|
await showToast({
|
|
323
364
|
style: Toast.Style.Failure,
|
|
@@ -30,18 +30,20 @@ test('dev command shows extension commands list', async () => {
|
|
|
30
30
|
"
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Simple Test Extension ────────────────────────────────
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
> Search commands...
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
Commands ▲
|
|
38
|
+
›List Items Displays a simple list with some ite view ▀
|
|
39
|
+
Search Items Search and filter through a list o view
|
|
40
|
+
Google Oauth view
|
|
41
|
+
usePromise Demo Shows how to use the usePromise view ▼
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
↵ run command ↑↓ navigate ^k actions
|
|
45
|
+
|
|
46
|
+
"
|
|
45
47
|
`)
|
|
46
48
|
}, 30000)
|
|
47
49
|
|
|
@@ -63,18 +65,20 @@ test('selecting command with arguments shows arguments form', async () => {
|
|
|
63
65
|
"
|
|
64
66
|
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
Simple Test Extension ────────────────────────────────
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
> Search commands...
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
usePromise Demo Shows how to use the usePromise view ▲
|
|
73
|
+
Show State Shows the current application state view
|
|
74
|
+
›With Arguments Demonstrates command arguments ( view
|
|
75
|
+
Quick Action Copies current timestamp to cli no-view ▀
|
|
76
|
+
Throw Error Command that throws an error at roo view ▼
|
|
75
77
|
|
|
76
78
|
|
|
77
|
-
|
|
79
|
+
↵ run command ↑↓ navigate ^k actions
|
|
80
|
+
|
|
81
|
+
"
|
|
78
82
|
`)
|
|
79
83
|
|
|
80
84
|
// Select the command to show arguments form (enter opens action panel, enter again runs)
|
|
@@ -91,19 +95,20 @@ test('selecting command with arguments shows arguments form', async () => {
|
|
|
91
95
|
"
|
|
92
96
|
|
|
93
97
|
|
|
94
|
-
■ With Arguments █
|
|
95
|
-
│ Enter the arguments to run this command.
|
|
96
|
-
│
|
|
97
|
-
◇ Search query
|
|
98
|
-
│ Search query
|
|
99
|
-
│
|
|
100
|
-
◇ Secret key
|
|
101
|
-
│ Secret key
|
|
102
|
-
◇ Category
|
|
103
|
-
│ Category
|
|
104
98
|
|
|
105
99
|
|
|
106
|
-
|
|
100
|
+
■ With Arguments ▀
|
|
101
|
+
│ Enter the arguments to run this command.
|
|
102
|
+
│
|
|
103
|
+
◇ Search query
|
|
104
|
+
│ Search query
|
|
105
|
+
│
|
|
106
|
+
◇ Secret key
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
ctrl ↵ submit tab navigate ^k actions
|
|
110
|
+
|
|
111
|
+
"
|
|
107
112
|
`)
|
|
108
113
|
}, 30000)
|
|
109
114
|
|
|
@@ -135,19 +140,20 @@ test('can fill arguments and run command', async () => {
|
|
|
135
140
|
"
|
|
136
141
|
|
|
137
142
|
|
|
138
|
-
■ With Arguments █
|
|
139
|
-
│ Enter the arguments to run this command.
|
|
140
|
-
│
|
|
141
|
-
◇ Search query
|
|
142
|
-
│ Search query
|
|
143
|
-
│
|
|
144
|
-
◇ Secret key
|
|
145
|
-
│ Secret key
|
|
146
|
-
◇ Category
|
|
147
|
-
│ Category
|
|
148
143
|
|
|
149
144
|
|
|
150
|
-
|
|
145
|
+
■ With Arguments ▀
|
|
146
|
+
│ Enter the arguments to run this command.
|
|
147
|
+
│
|
|
148
|
+
◇ Search query
|
|
149
|
+
│ Search query
|
|
150
|
+
│
|
|
151
|
+
◇ Secret key
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
ctrl ↵ submit tab navigate ^k actions
|
|
155
|
+
|
|
156
|
+
"
|
|
151
157
|
`)
|
|
152
158
|
|
|
153
159
|
// Submit the form with Alt+Enter (opens action panel), then Enter (selects submit)
|
|
@@ -164,20 +170,20 @@ test('can fill arguments and run command', async () => {
|
|
|
164
170
|
"
|
|
165
171
|
|
|
166
172
|
|
|
167
|
-
|
|
173
|
+
Command Arguments Demo ───────────────────────────────
|
|
168
174
|
|
|
169
|
-
|
|
175
|
+
> Search...
|
|
170
176
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
177
|
+
Received Arguments
|
|
178
|
+
›▼ Search Query (empty)
|
|
179
|
+
▼ Secret Key (empty)
|
|
180
|
+
▼ Category (empty)
|
|
175
181
|
|
|
176
182
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
183
|
+
|
|
184
|
+
✓ Copied to Clipboard (empty)
|
|
185
|
+
|
|
186
|
+
"
|
|
181
187
|
`)
|
|
182
188
|
}, 30000)
|
|
183
189
|
|
|
@@ -201,24 +207,28 @@ test('can run simple view command without arguments', async () => {
|
|
|
201
207
|
"
|
|
202
208
|
|
|
203
209
|
|
|
204
|
-
|
|
210
|
+
List Items ───────────────────────────────────────────
|
|
205
211
|
|
|
206
|
-
|
|
212
|
+
> Search...
|
|
207
213
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
Items ▲
|
|
215
|
+
›▲ First Item This is the first item █
|
|
216
|
+
▲ Second Item This is the second item
|
|
217
|
+
▲ Third Item This is the third item
|
|
218
|
+
▲ Fourth Item This is the fourth item ▼
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
✓ Copied to Clipboard First Item
|
|
222
|
+
|
|
223
|
+
"
|
|
218
224
|
`)
|
|
219
225
|
}, 30000)
|
|
220
226
|
|
|
221
227
|
test('hot reload updates TUI when source file changes', async () => {
|
|
228
|
+
// This test verifies that React Refresh hot reload is working.
|
|
229
|
+
// When a source file changes, the component content should update in-place
|
|
230
|
+
// without needing to manually exit and re-enter the command.
|
|
231
|
+
|
|
222
232
|
const hotReloadFixtureDir = path.resolve(__dirname, '../../fixtures/hot-reload-extension')
|
|
223
233
|
const sourceFilePath = path.join(hotReloadFixtureDir, 'src/detail-view.tsx')
|
|
224
234
|
const fs = await import('node:fs')
|
|
@@ -260,29 +270,112 @@ test('hot reload updates TUI when source file changes', async () => {
|
|
|
260
270
|
const newContent = originalContent.replace('MARKER_VALUE', `UPDATED_${randomNumber}`)
|
|
261
271
|
fs.writeFileSync(sourceFilePath, newContent)
|
|
262
272
|
|
|
263
|
-
// Wait for rebuild
|
|
273
|
+
// Wait for rebuild toast to appear
|
|
274
|
+
await hotReloadSession.waitIdle()
|
|
275
|
+
await new Promise((resolve) => setTimeout(resolve, 3000))
|
|
276
|
+
|
|
277
|
+
const afterReloadSnapshot = await hotReloadSession.text()
|
|
278
|
+
|
|
279
|
+
// React Refresh is working! The component content IS updated in-place.
|
|
280
|
+
// The new content (UPDATED_xxx) should be visible, old content (MARKER_VALUE) should NOT be visible.
|
|
281
|
+
expect(afterReloadSnapshot).toContain(`UPDATED_${randomNumber}`) // New content IS visible
|
|
282
|
+
expect(afterReloadSnapshot).not.toContain('MARKER_VALUE') // Old content NOT visible
|
|
283
|
+
} finally {
|
|
284
|
+
// Restore original content
|
|
285
|
+
fs.writeFileSync(sourceFilePath, originalContent)
|
|
286
|
+
hotReloadSession?.close()
|
|
287
|
+
}
|
|
288
|
+
}, 60000)
|
|
289
|
+
|
|
290
|
+
test('hot reload with navigation - preserves navigation and updates content', async () => {
|
|
291
|
+
// This test verifies React Refresh works with navigation:
|
|
292
|
+
// 1. Navigation stack is preserved (we stay on detail view)
|
|
293
|
+
// 2. Component content IS updated (new code runs)
|
|
294
|
+
// 3. Component state is preserved if hook signature unchanged
|
|
295
|
+
|
|
296
|
+
const hotReloadFixtureDir = path.resolve(__dirname, '../../fixtures/hot-reload-extension')
|
|
297
|
+
const sourceFilePath = path.join(hotReloadFixtureDir, 'src/list-with-navigation.tsx')
|
|
298
|
+
const fs = await import('node:fs')
|
|
299
|
+
|
|
300
|
+
// Read original content to restore later
|
|
301
|
+
const originalContent = fs.readFileSync(sourceFilePath, 'utf-8')
|
|
302
|
+
|
|
303
|
+
// Start a new session for this test
|
|
304
|
+
const hotReloadSession = await launchTerminal({
|
|
305
|
+
command: 'bun',
|
|
306
|
+
args: ['src/cli.tsx', 'dev', hotReloadFixtureDir],
|
|
307
|
+
cols: 70,
|
|
308
|
+
rows: 20,
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
// Wait for the extension to load
|
|
264
313
|
await hotReloadSession.text({
|
|
265
|
-
waitFor: (text) => /Hot Reload Test/i.test(text) && /
|
|
266
|
-
timeout:
|
|
314
|
+
waitFor: (text) => /Hot Reload Test/i.test(text) && /List With Navigation/i.test(text),
|
|
315
|
+
timeout: 10000,
|
|
267
316
|
})
|
|
317
|
+
|
|
318
|
+
// Navigate to "List With Navigation" command (second item)
|
|
319
|
+
await hotReloadSession.press('down')
|
|
268
320
|
await hotReloadSession.waitIdle()
|
|
269
321
|
|
|
270
|
-
// Run the command
|
|
322
|
+
// Run the command (enter opens action panel, enter again runs)
|
|
271
323
|
await hotReloadSession.press('enter')
|
|
272
324
|
await hotReloadSession.press('enter')
|
|
273
325
|
await hotReloadSession.waitIdle()
|
|
274
326
|
|
|
275
|
-
// Wait for the
|
|
327
|
+
// Wait for the list to show OR the detail view (enter might auto-execute first action)
|
|
276
328
|
await hotReloadSession.text({
|
|
277
|
-
waitFor: (text) =>
|
|
329
|
+
waitFor: (text) => /Item One/i.test(text),
|
|
278
330
|
timeout: 10000,
|
|
279
331
|
})
|
|
280
332
|
|
|
281
|
-
|
|
282
|
-
|
|
333
|
+
// Check if we're on the list or already on detail view
|
|
334
|
+
let currentSnapshot = await hotReloadSession.text()
|
|
335
|
+
|
|
336
|
+
if (currentSnapshot.includes('Click to see details')) {
|
|
337
|
+
// We're on the list - push to detail view
|
|
338
|
+
await hotReloadSession.press('enter')
|
|
339
|
+
await hotReloadSession.waitIdle()
|
|
340
|
+
|
|
341
|
+
// Wait for the detail view with the marker
|
|
342
|
+
await hotReloadSession.text({
|
|
343
|
+
waitFor: (text) => /NAV_MARKER_VALUE/i.test(text) && /Item One Details/i.test(text),
|
|
344
|
+
timeout: 10000,
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
// else: we're already on the detail view (auto-executed)
|
|
348
|
+
|
|
349
|
+
const detailSnapshot = await hotReloadSession.text()
|
|
350
|
+
expect(detailSnapshot).toContain('Item One Details')
|
|
351
|
+
expect(detailSnapshot).toContain('NAV_MARKER_VALUE')
|
|
352
|
+
expect(detailSnapshot).toContain('Counter: 0')
|
|
353
|
+
|
|
354
|
+
// Generate a random number for the update
|
|
355
|
+
const randomNumber = Math.floor(Math.random() * 1000000)
|
|
356
|
+
|
|
357
|
+
// Update the source file with the random number
|
|
358
|
+
const newContent = originalContent.replace('NAV_MARKER_VALUE', `UPDATED_NAV_${randomNumber}`)
|
|
359
|
+
fs.writeFileSync(sourceFilePath, newContent)
|
|
360
|
+
|
|
361
|
+
// Wait for rebuild to complete
|
|
362
|
+
await hotReloadSession.waitIdle()
|
|
363
|
+
await new Promise((resolve) => setTimeout(resolve, 4000)) // Give rebuild time
|
|
364
|
+
|
|
365
|
+
const afterReloadSnapshot = await hotReloadSession.text()
|
|
366
|
+
|
|
367
|
+
// React Refresh is working:
|
|
368
|
+
// - Navigation is preserved (still on detail view)
|
|
369
|
+
// - Content IS updated (new marker value visible)
|
|
370
|
+
// - State should be preserved if hook signature unchanged
|
|
371
|
+
|
|
372
|
+
expect(afterReloadSnapshot).toContain('Item One Details')
|
|
373
|
+
expect(afterReloadSnapshot).toContain(`UPDATED_NAV_${randomNumber}`) // New content IS visible
|
|
374
|
+
expect(afterReloadSnapshot).not.toContain('NAV_MARKER_VALUE') // Old content NOT visible
|
|
375
|
+
expect(afterReloadSnapshot).toContain('Counter: 0') // State preserved (hook signature unchanged)
|
|
283
376
|
} finally {
|
|
284
377
|
// Restore original content
|
|
285
378
|
fs.writeFileSync(sourceFilePath, originalContent)
|
|
286
379
|
hotReloadSession?.close()
|
|
287
380
|
}
|
|
288
|
-
},
|
|
381
|
+
}, 90000)
|
package/src/extensions/home.tsx
CHANGED
|
@@ -18,7 +18,7 @@ interface ExtensionCommand {
|
|
|
18
18
|
extensionDir?: string
|
|
19
19
|
command: any
|
|
20
20
|
bundledPath?: string
|
|
21
|
-
|
|
21
|
+
loadComponent?: () => Promise<(props: any) => any>
|
|
22
22
|
packageJson?: any
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -34,7 +34,7 @@ const builtinExtensions: ExtensionCommand[] = [
|
|
|
34
34
|
mode: 'view',
|
|
35
35
|
icon: 'Store',
|
|
36
36
|
},
|
|
37
|
-
|
|
37
|
+
loadComponent: async () => Store,
|
|
38
38
|
},
|
|
39
39
|
]
|
|
40
40
|
|
|
@@ -45,22 +45,21 @@ function ExtensionsList({
|
|
|
45
45
|
allCommands: ExtensionCommand[]
|
|
46
46
|
initialSearchQuery?: string
|
|
47
47
|
}): any {
|
|
48
|
-
const { push } = useNavigation()
|
|
48
|
+
const { push, replace } = useNavigation()
|
|
49
49
|
const [searchText, setSearchText] = React.useState(initialSearchQuery)
|
|
50
50
|
|
|
51
51
|
const handleCommandSelect = async (item: ExtensionCommand) => {
|
|
52
52
|
clearCommandArguments()
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
54
|
try {
|
|
57
55
|
await runCommand({
|
|
58
56
|
command: item.command,
|
|
59
57
|
extensionName: item.extensionName,
|
|
60
58
|
packageJson: item.packageJson,
|
|
61
59
|
bundledPath: item.bundledPath,
|
|
62
|
-
|
|
60
|
+
loadComponent: item.loadComponent,
|
|
63
61
|
push,
|
|
62
|
+
replace,
|
|
64
63
|
})
|
|
65
64
|
} catch (error: any) {
|
|
66
65
|
await showToast({
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// This module MUST be imported before @opentui/react to ensure the devtools
|
|
2
|
+
// hook exists before the reconciler tries to call injectIntoDevTools()
|
|
3
|
+
|
|
4
|
+
// Store captured renderer internals for manual refresh triggering
|
|
5
|
+
let capturedRendererInternals: any = null
|
|
6
|
+
let RefreshRuntime: typeof import('react-refresh/runtime') | null = null
|
|
7
|
+
|
|
8
|
+
// Initialize React Refresh BEFORE any React rendering
|
|
9
|
+
// This must happen before @opentui/react is loaded
|
|
10
|
+
function initializeReactRefresh() {
|
|
11
|
+
if (!RefreshRuntime) {
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Inject into the global devtools hook
|
|
16
|
+
// This sets up __REACT_DEVTOOLS_GLOBAL_HOOK__ which the reconciler uses
|
|
17
|
+
RefreshRuntime.injectIntoGlobalHook(globalThis)
|
|
18
|
+
|
|
19
|
+
const hook = (globalThis as any).__REACT_DEVTOOLS_GLOBAL_HOOK__
|
|
20
|
+
if (hook) {
|
|
21
|
+
// Intercept the inject call to capture renderer internals
|
|
22
|
+
// This is called by react-reconciler when injectIntoDevTools is called
|
|
23
|
+
const originalInject = hook.inject
|
|
24
|
+
hook.inject = (renderer: any) => {
|
|
25
|
+
// Capture the renderer internals - we need scheduleRefresh
|
|
26
|
+
capturedRendererInternals = renderer
|
|
27
|
+
// Call the original inject
|
|
28
|
+
return originalInject.call(hook, renderer)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Set up the globals that the babel transform expects
|
|
33
|
+
// These are called by the transformed code to register components
|
|
34
|
+
;(globalThis as any).$RefreshReg$ = (type: any, id: string) => {
|
|
35
|
+
RefreshRuntime.register(type, id)
|
|
36
|
+
}
|
|
37
|
+
;(globalThis as any).$RefreshSig$ = () => {
|
|
38
|
+
return RefreshRuntime.createSignatureFunctionForTransform()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Only load react-refresh in non-production
|
|
43
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
44
|
+
// Dynamic import to avoid bundling in production
|
|
45
|
+
RefreshRuntime = require('react-refresh/runtime')
|
|
46
|
+
initializeReactRefresh()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Get captured renderer internals
|
|
50
|
+
export function getRendererInternals() {
|
|
51
|
+
return capturedRendererInternals
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if we have a valid renderer with refresh capabilities
|
|
55
|
+
export function hasRefreshCapability() {
|
|
56
|
+
return !!(capturedRendererInternals?.scheduleRefresh)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { RefreshRuntime }
|