@markjaquith/agency 2.32.0 → 2.32.2
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/package.json +1 -1
- package/src/utils/interactive.test.tsx +54 -1
- package/src/utils/interactive.tsx +18 -11
package/package.json
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test"
|
|
2
2
|
import { testRender } from "@opentui/solid"
|
|
3
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
4
|
+
import { tmpdir } from "node:os"
|
|
5
|
+
import { dirname, join } from "node:path"
|
|
3
6
|
import {
|
|
4
7
|
fuzzyChoices,
|
|
5
8
|
InteractiveSelectPrompt,
|
|
@@ -8,10 +11,40 @@ import {
|
|
|
8
11
|
} from "./interactive"
|
|
9
12
|
|
|
10
13
|
describe("OpenTUI interaction", () => {
|
|
14
|
+
test("selects the Solid JSX runtime without the project preload", async () => {
|
|
15
|
+
const source = await Bun.file(
|
|
16
|
+
new URL("./interactive.tsx", import.meta.url),
|
|
17
|
+
).text()
|
|
18
|
+
const root = await mkdtemp(join(tmpdir(), "agency-interactive-jsx-"))
|
|
19
|
+
const entrypoint = join(
|
|
20
|
+
root,
|
|
21
|
+
"node_modules",
|
|
22
|
+
"@markjaquith",
|
|
23
|
+
"agency",
|
|
24
|
+
"interactive.tsx",
|
|
25
|
+
)
|
|
26
|
+
await mkdir(dirname(entrypoint), { recursive: true })
|
|
27
|
+
await writeFile(entrypoint, source)
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const result = await Bun.build({
|
|
31
|
+
entrypoints: [entrypoint],
|
|
32
|
+
packages: "external",
|
|
33
|
+
target: "bun",
|
|
34
|
+
})
|
|
35
|
+
expect(result.success).toBeTrue()
|
|
36
|
+
const output = await result.outputs[0]!.text()
|
|
37
|
+
expect(output).toContain("@opentui/solid/jsx-dev-runtime")
|
|
38
|
+
expect(output).not.toContain("react/jsx-dev-runtime")
|
|
39
|
+
} finally {
|
|
40
|
+
await rm(root, { recursive: true, force: true })
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
11
44
|
test("uses the split-footer renderer contract", () => {
|
|
12
45
|
expect(interactiveRendererConfig).toMatchObject({
|
|
13
46
|
screenMode: "split-footer",
|
|
14
|
-
footerHeight:
|
|
47
|
+
footerHeight: 5,
|
|
15
48
|
externalOutputMode: "capture-stdout",
|
|
16
49
|
clearOnShutdown: false,
|
|
17
50
|
})
|
|
@@ -179,6 +212,26 @@ describe("OpenTUI interaction", () => {
|
|
|
179
212
|
}
|
|
180
213
|
})
|
|
181
214
|
|
|
215
|
+
test("wraps text prompt input onto another line", async () => {
|
|
216
|
+
const setup = await testRender(
|
|
217
|
+
() => <InteractiveTextPrompt prompt="Task ID" onDone={() => {}} />,
|
|
218
|
+
{ width: 16, height: 5 },
|
|
219
|
+
)
|
|
220
|
+
try {
|
|
221
|
+
await setup.renderer.setupTerminal()
|
|
222
|
+
await setup.renderOnce()
|
|
223
|
+
await Bun.sleep(0)
|
|
224
|
+
await setup.mockInput.typeText("alpha beta gamma delta")
|
|
225
|
+
await setup.flush()
|
|
226
|
+
const frame = setup.captureCharFrame()
|
|
227
|
+
expect(frame).toContain("alpha beta")
|
|
228
|
+
expect(frame).toContain("gamma")
|
|
229
|
+
expect(frame).toMatch(/alpha beta\s*\ngamma delta/)
|
|
230
|
+
} finally {
|
|
231
|
+
setup.renderer.destroy()
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
|
|
182
235
|
test("submits text and cancels with ctrl-c or escape", async () => {
|
|
183
236
|
let submitted: string | null | undefined
|
|
184
237
|
const input = await testRender(
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
createCliRenderer,
|
|
3
5
|
type CliRenderer,
|
|
4
6
|
type CliRendererConfig,
|
|
5
|
-
type
|
|
7
|
+
type TextareaRenderable,
|
|
6
8
|
} from "@opentui/core"
|
|
7
9
|
import { render, useKeyboard, type JSX } from "@opentui/solid"
|
|
8
10
|
import { createMemo, createSignal, For } from "solid-js"
|
|
@@ -14,7 +16,7 @@ export interface InteractiveChoice {
|
|
|
14
16
|
|
|
15
17
|
export const interactiveRendererConfig = {
|
|
16
18
|
screenMode: "split-footer",
|
|
17
|
-
footerHeight:
|
|
19
|
+
footerHeight: 5,
|
|
18
20
|
externalOutputMode: "capture-stdout",
|
|
19
21
|
consoleMode: "disabled",
|
|
20
22
|
clearOnShutdown: false,
|
|
@@ -33,7 +35,7 @@ const isCancel = (key: { name: string; ctrl: boolean }) =>
|
|
|
33
35
|
key.name === "escape" || (key.ctrl && key.name === "c")
|
|
34
36
|
|
|
35
37
|
export const InteractiveTextPrompt = (props: PromptProps<string>) => {
|
|
36
|
-
let input:
|
|
38
|
+
let input: TextareaRenderable | undefined
|
|
37
39
|
let value = ""
|
|
38
40
|
useKeyboard((key) => {
|
|
39
41
|
if (isCancel(key)) {
|
|
@@ -51,10 +53,13 @@ export const InteractiveTextPrompt = (props: PromptProps<string>) => {
|
|
|
51
53
|
return (
|
|
52
54
|
<box flexDirection="column" width="100%" height="100%">
|
|
53
55
|
<text fg="#7aa2f7">{props.prompt}</text>
|
|
54
|
-
<
|
|
56
|
+
<textarea
|
|
55
57
|
focused
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
height={2}
|
|
59
|
+
wrapMode="word"
|
|
60
|
+
keyBindings={[{ name: "return", action: "submit" }]}
|
|
61
|
+
onContentChange={() => {
|
|
62
|
+
value = input?.plainText ?? ""
|
|
58
63
|
}}
|
|
59
64
|
ref={(next) => {
|
|
60
65
|
input = next
|
|
@@ -138,7 +143,7 @@ export const fuzzyChoices = (
|
|
|
138
143
|
}
|
|
139
144
|
|
|
140
145
|
export const InteractiveSelectPrompt = (props: SelectPromptProps) => {
|
|
141
|
-
let input:
|
|
146
|
+
let input: TextareaRenderable | undefined
|
|
142
147
|
const [query, setQuery] = createSignal("")
|
|
143
148
|
const [selected, setSelected] = createSignal(0)
|
|
144
149
|
const choices = createMemo(() => fuzzyChoices(props.choices, query()))
|
|
@@ -193,13 +198,16 @@ export const InteractiveSelectPrompt = (props: SelectPromptProps) => {
|
|
|
193
198
|
{props.prompt}
|
|
194
199
|
</text>
|
|
195
200
|
<text fg="#7aa2f7">{" > "}</text>
|
|
196
|
-
<
|
|
201
|
+
<textarea
|
|
197
202
|
focused
|
|
198
203
|
flexGrow={1}
|
|
199
204
|
minWidth={8}
|
|
205
|
+
height={2}
|
|
206
|
+
wrapMode="word"
|
|
200
207
|
placeholder="filter"
|
|
201
|
-
|
|
202
|
-
|
|
208
|
+
keyBindings={[{ name: "return", action: "submit" }]}
|
|
209
|
+
onContentChange={() => {
|
|
210
|
+
setQuery(input?.plainText ?? "")
|
|
203
211
|
setSelected(0)
|
|
204
212
|
}}
|
|
205
213
|
ref={(next) => {
|
|
@@ -264,7 +272,6 @@ async function runInteractive<T>(
|
|
|
264
272
|
} finally {
|
|
265
273
|
if (renderer) {
|
|
266
274
|
await shutdown(renderer)
|
|
267
|
-
process.stdout.write("\n")
|
|
268
275
|
}
|
|
269
276
|
}
|
|
270
277
|
}
|