@markjaquith/agency 3.5.2 → 3.6.1
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/README.md +160 -16
- package/cli-main.ts +13 -2
- package/package.json +11 -5
- package/src/act-schema.ts +62 -0
- package/src/cli-parser.ts +13 -3
- package/src/commands/act-actions.ts +734 -0
- package/src/commands/act-prompts.ts +294 -0
- package/src/commands/act.ts +602 -436
- package/src/utils/chooser.ts +7 -0
- package/src/utils/interactive.js +977 -0
- package/src/workbase/AGENTS.md +49 -0
- package/src/workbase/work-target.ts +6 -4
- package/src/utils/interactive.tsx +0 -496
package/src/workbase/AGENTS.md
CHANGED
|
@@ -4,6 +4,55 @@ This directory is an Agency workbase. Epics, tasks, and phases are durable
|
|
|
4
4
|
Markdown documents; repository aliases and generated Git worktrees provide code
|
|
5
5
|
access.
|
|
6
6
|
|
|
7
|
+
## Discover Actions First
|
|
8
|
+
|
|
9
|
+
After the `agency context . --json` bootstrap, use `agency act --json` as the
|
|
10
|
+
primary way to discover what Agency can do. Prefer its live action catalog over
|
|
11
|
+
searching command help, reading implementation code, or guessing commands from
|
|
12
|
+
memory. It reports available actions, blocked reasons, required inputs, exact
|
|
13
|
+
argv/templates, document revisions, and explicit follow-up choices.
|
|
14
|
+
|
|
15
|
+
Keep discovery focused on the work at hand:
|
|
16
|
+
|
|
17
|
+
- Current task: `agency act --task <context-task-id> --json`.
|
|
18
|
+
- Current phase: `agency act --task <context-task-id> --phase <context-phase-id> --json`.
|
|
19
|
+
- One operation: add `--action <action-id>` to either selector.
|
|
20
|
+
- Workbase-level creation or repository actions: use
|
|
21
|
+
`agency act --action <action-id> --json` without an item selector.
|
|
22
|
+
- Unsure which action or item applies: `agency act --json`.
|
|
23
|
+
|
|
24
|
+
Use selectors returned by context, not IDs inferred from checkout or branch
|
|
25
|
+
names. Execute an available `command` as argv from the returned workbase root.
|
|
26
|
+
For a `commandTemplate`, substitute required input placeholders; append optional
|
|
27
|
+
inputs using their declared `option`. Preserve multiline input as one argv value.
|
|
28
|
+
Re-discover after mutations because availability and revisions may change.
|
|
29
|
+
|
|
30
|
+
Discovery is read-only and does not grant consent. `followUpCommands` describe
|
|
31
|
+
the action's bookkeeping; `nextActions` require a separate explicit choice.
|
|
32
|
+
Creation never implies starting Work. Respect blocked reasons and the consent
|
|
33
|
+
boundaries below. The command fast paths remain useful when the exact operation
|
|
34
|
+
and arguments are already established; do not rediscover between every known
|
|
35
|
+
step or use help unless discovery does not cover the operation.
|
|
36
|
+
|
|
37
|
+
### Current Work and Branch Context
|
|
38
|
+
|
|
39
|
+
When the request concerns this execution unit, target its context task/phase
|
|
40
|
+
rather than browsing the entire workbase. Use `authority.writable.checkoutPath`
|
|
41
|
+
for repository work and the declared delivery branch for publication.
|
|
42
|
+
|
|
43
|
+
For new work that builds on the current implementation (a follow-up, additional
|
|
44
|
+
phase, or explicitly requested continuation), prefer the current execution's
|
|
45
|
+
declared branch as `--base` when that matches the user's intent. Do not blindly
|
|
46
|
+
copy `main` from examples or accept a wizard default. Confirm the branch from
|
|
47
|
+
Agency context; when checking the live Git branch is necessary, do so in the
|
|
48
|
+
writable checkout, not the workbase root. An explicit user base takes precedence.
|
|
49
|
+
Unrelated new work should use its intended integration base instead.
|
|
50
|
+
|
|
51
|
+
Branch ancestry is not a completion dependency. Choosing the current branch as
|
|
52
|
+
the base does not add `--depends-on`, reuse the current item for explicitly new
|
|
53
|
+
work, switch branches, or expand write authority. Pass the chosen base as the
|
|
54
|
+
discovered `base` input to the native creation/handoff command.
|
|
55
|
+
|
|
7
56
|
## Command Fast Paths
|
|
8
57
|
|
|
9
58
|
When a request clearly matches one of these intents, use the exact recipe without
|
|
@@ -68,7 +68,7 @@ const statuses: Record<
|
|
|
68
68
|
> = {
|
|
69
69
|
open: { icon: "", color: macchiato.overlay1 },
|
|
70
70
|
working: { icon: "", color: macchiato.blue },
|
|
71
|
-
delegated: { icon: "", color: macchiato.
|
|
71
|
+
delegated: { icon: "", color: macchiato.blue },
|
|
72
72
|
done: { icon: "", color: macchiato.green },
|
|
73
73
|
dropped: { icon: "", color: macchiato.red },
|
|
74
74
|
}
|
|
@@ -77,11 +77,13 @@ const kinds: Record<
|
|
|
77
77
|
WorkTarget["kind"],
|
|
78
78
|
{ readonly icon: string; readonly color: string }
|
|
79
79
|
> = {
|
|
80
|
-
epic: { icon: "", color: macchiato.
|
|
81
|
-
task: { icon: "", color: macchiato.
|
|
82
|
-
phase: { icon: "
|
|
80
|
+
epic: { icon: "", color: macchiato.overlay1 },
|
|
81
|
+
task: { icon: "", color: macchiato.overlay1 },
|
|
82
|
+
phase: { icon: "", color: macchiato.overlay1 },
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
export { statuses as workStatusStyle, kinds as workKindStyle }
|
|
86
|
+
|
|
85
87
|
const hexToAnsi = (color: string) => {
|
|
86
88
|
const value = color.slice(1)
|
|
87
89
|
return [0, 2, 4]
|
|
@@ -1,496 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
createCliRenderer,
|
|
5
|
-
type CliRenderer,
|
|
6
|
-
type CliRendererConfig,
|
|
7
|
-
type TextareaRenderable,
|
|
8
|
-
type TextNodeOptions,
|
|
9
|
-
} from "@opentui/core"
|
|
10
|
-
import {
|
|
11
|
-
render,
|
|
12
|
-
useKeyboard,
|
|
13
|
-
useTerminalDimensions,
|
|
14
|
-
type JSX,
|
|
15
|
-
} from "@opentui/solid"
|
|
16
|
-
import { createMemo, createSignal, For } from "solid-js"
|
|
17
|
-
import type { ChoiceSegment } from "./chooser"
|
|
18
|
-
import { macchiato } from "./theme"
|
|
19
|
-
|
|
20
|
-
export interface InteractiveChoice {
|
|
21
|
-
readonly key: string
|
|
22
|
-
readonly label: string
|
|
23
|
-
readonly depth?: number
|
|
24
|
-
readonly segments?: readonly ChoiceSegment[]
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const interactiveRendererConfig = {
|
|
28
|
-
screenMode: "split-footer",
|
|
29
|
-
footerHeight: 5,
|
|
30
|
-
externalOutputMode: "capture-stdout",
|
|
31
|
-
consoleMode: "disabled",
|
|
32
|
-
clearOnShutdown: false,
|
|
33
|
-
exitOnCtrlC: false,
|
|
34
|
-
useMouse: false,
|
|
35
|
-
autoFocus: false,
|
|
36
|
-
openConsoleOnError: false,
|
|
37
|
-
} satisfies CliRendererConfig
|
|
38
|
-
|
|
39
|
-
export const interactiveSelectRendererConfig = {
|
|
40
|
-
...interactiveRendererConfig,
|
|
41
|
-
screenMode: "alternate-screen",
|
|
42
|
-
externalOutputMode: "passthrough",
|
|
43
|
-
} satisfies CliRendererConfig
|
|
44
|
-
|
|
45
|
-
interface PromptProps<T> {
|
|
46
|
-
readonly prompt: string
|
|
47
|
-
readonly onDone: (value: T | null) => void
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const isCancel = (key: { name: string; ctrl: boolean }) =>
|
|
51
|
-
key.name === "escape" || (key.ctrl && key.name === "c")
|
|
52
|
-
|
|
53
|
-
interface PromptKey {
|
|
54
|
-
readonly name: string
|
|
55
|
-
readonly ctrl: boolean
|
|
56
|
-
preventDefault(): void
|
|
57
|
-
stopPropagation(): void
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const removedText = (before: string, after: string) => {
|
|
61
|
-
let start = 0
|
|
62
|
-
while (
|
|
63
|
-
start < before.length &&
|
|
64
|
-
start < after.length &&
|
|
65
|
-
before[start] === after[start]
|
|
66
|
-
) {
|
|
67
|
-
start++
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
let end = 0
|
|
71
|
-
while (
|
|
72
|
-
before[before.length - end - 1] === after[after.length - end - 1] &&
|
|
73
|
-
end < before.length - start &&
|
|
74
|
-
end < after.length - start
|
|
75
|
-
) {
|
|
76
|
-
end++
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return before.slice(start, before.length - end)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const createReadlineEditing = (
|
|
83
|
-
getInput: () => TextareaRenderable | undefined,
|
|
84
|
-
onInput?: (value: string) => void,
|
|
85
|
-
) => {
|
|
86
|
-
let value = ""
|
|
87
|
-
let killBuffer = ""
|
|
88
|
-
let beforeKill: string | undefined
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
get value() {
|
|
92
|
-
return value
|
|
93
|
-
},
|
|
94
|
-
handleInput(next: string) {
|
|
95
|
-
if (beforeKill !== undefined) {
|
|
96
|
-
const killed = removedText(beforeKill, next)
|
|
97
|
-
if (killed) killBuffer = killed
|
|
98
|
-
beforeKill = undefined
|
|
99
|
-
}
|
|
100
|
-
value = next
|
|
101
|
-
onInput?.(next)
|
|
102
|
-
},
|
|
103
|
-
handleKey(key: PromptKey) {
|
|
104
|
-
const current = getInput()?.plainText
|
|
105
|
-
if (current !== undefined && current !== value) this.handleInput(current)
|
|
106
|
-
if (!key.ctrl) return false
|
|
107
|
-
if (key.name === "y") {
|
|
108
|
-
key.preventDefault()
|
|
109
|
-
key.stopPropagation()
|
|
110
|
-
if (killBuffer) getInput()?.insertText(killBuffer)
|
|
111
|
-
return true
|
|
112
|
-
}
|
|
113
|
-
if (key.name === "u" || key.name === "k" || key.name === "w") {
|
|
114
|
-
beforeKill = value
|
|
115
|
-
}
|
|
116
|
-
return false
|
|
117
|
-
},
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export const InteractiveTextPrompt = (props: PromptProps<string>) => {
|
|
122
|
-
let input: TextareaRenderable | undefined
|
|
123
|
-
const editing = createReadlineEditing(() => input)
|
|
124
|
-
useKeyboard((key) => {
|
|
125
|
-
if (isCancel(key)) {
|
|
126
|
-
key.preventDefault()
|
|
127
|
-
key.stopPropagation()
|
|
128
|
-
props.onDone(null)
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
if (editing.handleKey(key)) return
|
|
132
|
-
if (key.name !== "return") return
|
|
133
|
-
key.preventDefault()
|
|
134
|
-
key.stopPropagation()
|
|
135
|
-
props.onDone(editing.value)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
return (
|
|
139
|
-
<box
|
|
140
|
-
flexDirection="column"
|
|
141
|
-
width="100%"
|
|
142
|
-
height="100%"
|
|
143
|
-
backgroundColor={macchiato.base}
|
|
144
|
-
>
|
|
145
|
-
<text fg={macchiato.blue}>{props.prompt}</text>
|
|
146
|
-
<textarea
|
|
147
|
-
focused
|
|
148
|
-
height={2}
|
|
149
|
-
wrapMode="word"
|
|
150
|
-
backgroundColor={macchiato.mantle}
|
|
151
|
-
focusedBackgroundColor={macchiato.surface0}
|
|
152
|
-
textColor={macchiato.text}
|
|
153
|
-
focusedTextColor={macchiato.text}
|
|
154
|
-
cursorColor={macchiato.rosewater}
|
|
155
|
-
keyBindings={[{ name: "return", action: "submit" }]}
|
|
156
|
-
onContentChange={() => {
|
|
157
|
-
editing.handleInput(input?.plainText ?? "")
|
|
158
|
-
}}
|
|
159
|
-
ref={(next) => {
|
|
160
|
-
input = next
|
|
161
|
-
queueMicrotask(() => {
|
|
162
|
-
if (input && !input.isDestroyed) input.focus()
|
|
163
|
-
})
|
|
164
|
-
}}
|
|
165
|
-
/>
|
|
166
|
-
<text fg={macchiato.overlay1} wrapMode="none">
|
|
167
|
-
enter submit | esc cancel | ctrl-y yank
|
|
168
|
-
</text>
|
|
169
|
-
</box>
|
|
170
|
-
)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
interface SelectPromptProps extends PromptProps<string> {
|
|
174
|
-
readonly choices: readonly InteractiveChoice[]
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const isWordBoundary = (value: string, index: number) =>
|
|
178
|
-
index === 0 || /[\s/_.:-]/.test(value[index - 1]!)
|
|
179
|
-
|
|
180
|
-
const fuzzyScore = (value: string, query: string) => {
|
|
181
|
-
const candidate = value.toLowerCase()
|
|
182
|
-
const needle = query.toLowerCase()
|
|
183
|
-
let previous = new Float64Array(candidate.length)
|
|
184
|
-
let current = new Float64Array(candidate.length)
|
|
185
|
-
previous.fill(Number.NEGATIVE_INFINITY)
|
|
186
|
-
let bestScore = Number.NEGATIVE_INFINITY
|
|
187
|
-
|
|
188
|
-
for (let queryIndex = 0; queryIndex < needle.length; queryIndex++) {
|
|
189
|
-
current.fill(Number.NEGATIVE_INFINITY)
|
|
190
|
-
let bestEarlier = Number.NEGATIVE_INFINITY
|
|
191
|
-
bestScore = Number.NEGATIVE_INFINITY
|
|
192
|
-
for (let index = 0; index < candidate.length; index++) {
|
|
193
|
-
if (queryIndex > 0 && index > 0) {
|
|
194
|
-
bestEarlier = Math.max(bestEarlier, previous[index - 1]! + index - 1)
|
|
195
|
-
}
|
|
196
|
-
if (candidate[index] !== needle[queryIndex]) continue
|
|
197
|
-
|
|
198
|
-
const boundaryBonus = isWordBoundary(candidate, index) ? 8 : 0
|
|
199
|
-
if (queryIndex === 0) {
|
|
200
|
-
current[index] = 10 + boundaryBonus - index
|
|
201
|
-
bestScore = Math.max(bestScore, current[index]!)
|
|
202
|
-
continue
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const contiguous =
|
|
206
|
-
index > 0 ? previous[index - 1]! + 12 : Number.NEGATIVE_INFINITY
|
|
207
|
-
const gapped = bestEarlier - index + 1
|
|
208
|
-
current[index] = Math.max(contiguous, gapped) + 10 + boundaryBonus
|
|
209
|
-
bestScore = Math.max(bestScore, current[index]!)
|
|
210
|
-
}
|
|
211
|
-
if (!Number.isFinite(bestScore)) return null
|
|
212
|
-
const swap = previous
|
|
213
|
-
previous = current
|
|
214
|
-
current = swap
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return bestScore - candidate.length / 1000
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export const fuzzyChoices = (
|
|
221
|
-
choices: readonly InteractiveChoice[],
|
|
222
|
-
query: string,
|
|
223
|
-
) => {
|
|
224
|
-
if (!query) return choices
|
|
225
|
-
return choices
|
|
226
|
-
.map((choice, index) => ({
|
|
227
|
-
choice,
|
|
228
|
-
index,
|
|
229
|
-
score: fuzzyScore(choice.label, query),
|
|
230
|
-
}))
|
|
231
|
-
.filter(
|
|
232
|
-
(
|
|
233
|
-
match,
|
|
234
|
-
): match is typeof match & {
|
|
235
|
-
score: number
|
|
236
|
-
} => match.score !== null,
|
|
237
|
-
)
|
|
238
|
-
.sort((left, right) => right.score - left.score || left.index - right.index)
|
|
239
|
-
.map((match) => match.choice)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const choiceDepth = (choice: InteractiveChoice) => choice.depth ?? 0
|
|
243
|
-
|
|
244
|
-
const hasSibling = (
|
|
245
|
-
choices: readonly InteractiveChoice[],
|
|
246
|
-
index: number,
|
|
247
|
-
direction: -1 | 1,
|
|
248
|
-
) => {
|
|
249
|
-
const depth = choiceDepth(choices[index]!)
|
|
250
|
-
for (
|
|
251
|
-
let siblingIndex = index + direction;
|
|
252
|
-
siblingIndex >= 0 && siblingIndex < choices.length;
|
|
253
|
-
siblingIndex += direction
|
|
254
|
-
) {
|
|
255
|
-
const siblingDepth = choiceDepth(choices[siblingIndex]!)
|
|
256
|
-
if (siblingDepth < depth) return false
|
|
257
|
-
if (siblingDepth === depth) return true
|
|
258
|
-
}
|
|
259
|
-
return false
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export const hierarchyPrefix = (
|
|
263
|
-
choices: readonly InteractiveChoice[],
|
|
264
|
-
index: number,
|
|
265
|
-
) => {
|
|
266
|
-
const choice = choices[index]
|
|
267
|
-
if (!choice || choice.depth === undefined) return ""
|
|
268
|
-
const depth = choiceDepth(choice)
|
|
269
|
-
let prefix = ""
|
|
270
|
-
let ancestorIndex = index
|
|
271
|
-
|
|
272
|
-
for (let ancestorDepth = depth - 1; ancestorDepth >= 0; ancestorDepth--) {
|
|
273
|
-
for (ancestorIndex--; ancestorIndex >= 0; ancestorIndex--) {
|
|
274
|
-
if (choiceDepth(choices[ancestorIndex]!) === ancestorDepth) break
|
|
275
|
-
}
|
|
276
|
-
prefix = `${ancestorIndex >= 0 && hasSibling(choices, ancestorIndex, 1) ? "│ " : " "}${prefix}`
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const hasPrevious = hasSibling(choices, index, -1)
|
|
280
|
-
const hasNext = hasSibling(choices, index, 1)
|
|
281
|
-
return `${prefix}${!hasPrevious && hasNext ? "╭" : hasNext ? "├" : "╰"}─ `
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export const InteractiveSelectPrompt = (props: SelectPromptProps) => {
|
|
285
|
-
let input: TextareaRenderable | undefined
|
|
286
|
-
const dimensions = useTerminalDimensions()
|
|
287
|
-
const [query, setQuery] = createSignal("")
|
|
288
|
-
const editing = createReadlineEditing(() => input, setQuery)
|
|
289
|
-
const [selected, setSelected] = createSignal(0)
|
|
290
|
-
const choices = createMemo(() => fuzzyChoices(props.choices, query()))
|
|
291
|
-
const displaySegments = (choice: InteractiveChoice) =>
|
|
292
|
-
choice.segments ?? [{ text: choice.label }]
|
|
293
|
-
const move = (offset: -1 | 1) => {
|
|
294
|
-
const count = choices().length
|
|
295
|
-
if (count === 0) return
|
|
296
|
-
setSelected((current) => (current + offset + count) % count)
|
|
297
|
-
}
|
|
298
|
-
useKeyboard((key) => {
|
|
299
|
-
if (key.name === "escape" && query()) {
|
|
300
|
-
key.preventDefault()
|
|
301
|
-
key.stopPropagation()
|
|
302
|
-
input?.clear()
|
|
303
|
-
editing.handleInput("")
|
|
304
|
-
setQuery("")
|
|
305
|
-
setSelected(0)
|
|
306
|
-
return
|
|
307
|
-
}
|
|
308
|
-
if (isCancel(key)) {
|
|
309
|
-
key.preventDefault()
|
|
310
|
-
key.stopPropagation()
|
|
311
|
-
props.onDone(null)
|
|
312
|
-
return
|
|
313
|
-
}
|
|
314
|
-
if (key.name === "up" || (key.ctrl && key.name === "p")) {
|
|
315
|
-
key.preventDefault()
|
|
316
|
-
key.stopPropagation()
|
|
317
|
-
move(-1)
|
|
318
|
-
return
|
|
319
|
-
}
|
|
320
|
-
if (key.name === "down" || (key.ctrl && key.name === "n")) {
|
|
321
|
-
key.preventDefault()
|
|
322
|
-
key.stopPropagation()
|
|
323
|
-
move(1)
|
|
324
|
-
return
|
|
325
|
-
}
|
|
326
|
-
if (editing.handleKey(key)) return
|
|
327
|
-
if (key.name !== "return") return
|
|
328
|
-
key.preventDefault()
|
|
329
|
-
key.stopPropagation()
|
|
330
|
-
const choice = choices()[selected()]
|
|
331
|
-
if (choice) props.onDone(choice.key)
|
|
332
|
-
})
|
|
333
|
-
|
|
334
|
-
const visible = () => {
|
|
335
|
-
const visibleCount = Math.max(dimensions().height - 3, 1)
|
|
336
|
-
const start = Math.min(
|
|
337
|
-
Math.max(selected() - Math.floor(visibleCount / 2), 0),
|
|
338
|
-
Math.max(choices().length - visibleCount, 0),
|
|
339
|
-
)
|
|
340
|
-
return choices()
|
|
341
|
-
.slice(start, start + visibleCount)
|
|
342
|
-
.map((choice, offset) => ({
|
|
343
|
-
choice,
|
|
344
|
-
index: start + offset,
|
|
345
|
-
originalIndex: props.choices.indexOf(choice),
|
|
346
|
-
}))
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return (
|
|
350
|
-
<box
|
|
351
|
-
flexDirection="column"
|
|
352
|
-
width="100%"
|
|
353
|
-
height="100%"
|
|
354
|
-
backgroundColor={macchiato.base}
|
|
355
|
-
>
|
|
356
|
-
<box flexDirection="row" width="100%">
|
|
357
|
-
<text fg={macchiato.blue} flexShrink={1} wrapMode="none">
|
|
358
|
-
{props.prompt}
|
|
359
|
-
</text>
|
|
360
|
-
<text fg={macchiato.blue}>{" > "}</text>
|
|
361
|
-
<textarea
|
|
362
|
-
focused
|
|
363
|
-
flexGrow={1}
|
|
364
|
-
minWidth={8}
|
|
365
|
-
height={2}
|
|
366
|
-
wrapMode="word"
|
|
367
|
-
placeholder="filter"
|
|
368
|
-
placeholderColor={macchiato.overlay0}
|
|
369
|
-
backgroundColor={macchiato.mantle}
|
|
370
|
-
focusedBackgroundColor={macchiato.surface0}
|
|
371
|
-
textColor={macchiato.text}
|
|
372
|
-
focusedTextColor={macchiato.text}
|
|
373
|
-
cursorColor={macchiato.rosewater}
|
|
374
|
-
keyBindings={[{ name: "return", action: "submit" }]}
|
|
375
|
-
onContentChange={() => {
|
|
376
|
-
editing.handleInput(input?.plainText ?? "")
|
|
377
|
-
setSelected(0)
|
|
378
|
-
}}
|
|
379
|
-
ref={(next) => {
|
|
380
|
-
input = next
|
|
381
|
-
queueMicrotask(() => {
|
|
382
|
-
if (input && !input.isDestroyed) input.focus()
|
|
383
|
-
})
|
|
384
|
-
}}
|
|
385
|
-
/>
|
|
386
|
-
</box>
|
|
387
|
-
<box flexDirection="column" flexGrow={1}>
|
|
388
|
-
<For
|
|
389
|
-
each={visible()}
|
|
390
|
-
fallback={<text fg={macchiato.overlay1}>No matches</text>}
|
|
391
|
-
>
|
|
392
|
-
{({ choice, index, originalIndex }) => (
|
|
393
|
-
<box
|
|
394
|
-
width="100%"
|
|
395
|
-
height={1}
|
|
396
|
-
backgroundColor={
|
|
397
|
-
index === selected() ? macchiato.surface1 : macchiato.base
|
|
398
|
-
}
|
|
399
|
-
>
|
|
400
|
-
<text
|
|
401
|
-
fg={index === selected() ? macchiato.text : macchiato.subtext0}
|
|
402
|
-
bg={index === selected() ? macchiato.surface1 : macchiato.base}
|
|
403
|
-
wrapMode="none"
|
|
404
|
-
>
|
|
405
|
-
<span
|
|
406
|
-
style={
|
|
407
|
-
{
|
|
408
|
-
fg: index === selected() ? macchiato.mauve : undefined,
|
|
409
|
-
} as TextNodeOptions
|
|
410
|
-
}
|
|
411
|
-
>
|
|
412
|
-
{index === selected() ? "▌ " : " "}
|
|
413
|
-
</span>
|
|
414
|
-
<span style={{ fg: macchiato.overlay1 } as TextNodeOptions}>
|
|
415
|
-
{query() ? "" : hierarchyPrefix(props.choices, originalIndex)}
|
|
416
|
-
</span>
|
|
417
|
-
<For each={displaySegments(choice)}>
|
|
418
|
-
{(segment) => (
|
|
419
|
-
<span style={{ fg: segment.color } as TextNodeOptions}>
|
|
420
|
-
{segment.text}
|
|
421
|
-
</span>
|
|
422
|
-
)}
|
|
423
|
-
</For>
|
|
424
|
-
</text>
|
|
425
|
-
</box>
|
|
426
|
-
)}
|
|
427
|
-
</For>
|
|
428
|
-
</box>
|
|
429
|
-
<text fg={macchiato.overlay1} wrapMode="none">
|
|
430
|
-
enter select | esc clear/cancel | arrows/ctrl-n/p | ctrl-y yank
|
|
431
|
-
</text>
|
|
432
|
-
</box>
|
|
433
|
-
)
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const shutdown = async (renderer: CliRenderer) => {
|
|
437
|
-
await renderer.idle().catch(() => undefined)
|
|
438
|
-
if (renderer.externalOutputMode === "capture-stdout") {
|
|
439
|
-
renderer.externalOutputMode = "passthrough"
|
|
440
|
-
}
|
|
441
|
-
if (renderer.screenMode === "split-footer")
|
|
442
|
-
renderer.screenMode = "main-screen"
|
|
443
|
-
if (!renderer.isDestroyed) renderer.destroy()
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
async function runInteractive<T>(
|
|
447
|
-
view: (finish: (value: T | null) => void) => JSX.Element,
|
|
448
|
-
config: CliRendererConfig = interactiveRendererConfig,
|
|
449
|
-
) {
|
|
450
|
-
let finish!: (value: T | null) => void
|
|
451
|
-
let settled = false
|
|
452
|
-
const result = new Promise<T | null>((resolve) => {
|
|
453
|
-
finish = (value) => {
|
|
454
|
-
if (settled) return
|
|
455
|
-
settled = true
|
|
456
|
-
resolve(value)
|
|
457
|
-
}
|
|
458
|
-
})
|
|
459
|
-
let renderer: CliRenderer | undefined
|
|
460
|
-
try {
|
|
461
|
-
renderer = await createCliRenderer({
|
|
462
|
-
...config,
|
|
463
|
-
onDestroy: () => finish(null),
|
|
464
|
-
})
|
|
465
|
-
await render(() => view(finish), renderer)
|
|
466
|
-
renderer.requestRender()
|
|
467
|
-
return await result
|
|
468
|
-
} finally {
|
|
469
|
-
if (renderer) {
|
|
470
|
-
await shutdown(renderer)
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
export const promptText = async (prompt: string) => {
|
|
476
|
-
const result = await runInteractive<string>((finish) => (
|
|
477
|
-
<InteractiveTextPrompt prompt={prompt} onDone={finish} />
|
|
478
|
-
))
|
|
479
|
-
if (result === null) throw new Error("Interactive input cancelled")
|
|
480
|
-
return result
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
export const promptSelect = (
|
|
484
|
-
prompt: string,
|
|
485
|
-
choices: readonly InteractiveChoice[],
|
|
486
|
-
) =>
|
|
487
|
-
runInteractive<string>(
|
|
488
|
-
(finish) => (
|
|
489
|
-
<InteractiveSelectPrompt
|
|
490
|
-
prompt={prompt}
|
|
491
|
-
choices={choices}
|
|
492
|
-
onDone={finish}
|
|
493
|
-
/>
|
|
494
|
-
),
|
|
495
|
-
interactiveSelectRendererConfig,
|
|
496
|
-
)
|