@mtayfur/opencode-prompt-enhancer 1.0.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.
@@ -0,0 +1,990 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { PluginOptions } from "@opencode-ai/plugin"
3
+ import type { Message, Part, TextPart } from "@opencode-ai/sdk/v2"
4
+ import type {
5
+ TuiPlugin,
6
+ TuiPluginModule,
7
+ TuiPromptInfo,
8
+ TuiPromptRef,
9
+ TuiRouteCurrent,
10
+ TuiSlotPlugin,
11
+ } from "@opencode-ai/plugin/tui"
12
+ import { useTerminalDimensions } from "@opentui/solid"
13
+ import { Show, createMemo, createSignal } from "solid-js"
14
+ import { ENHANCER_SYSTEM_PROMPT } from "./enhancer-system-prompt"
15
+
16
+ const MAX_RECENT_MESSAGES = 3
17
+ const MAX_CHANGED_FILES = 25
18
+ const MAX_CONTEXT_ITEM_PREVIEW_LENGTH = 250
19
+ const CONTEXT_TRUNCATION_MARKER = "\n[... truncated ...]\n"
20
+ const ENHANCEMENT_TIMEOUT_MS = 60_000
21
+ const ENHANCEMENT_ANIMATION_INTERVAL_MS = 250
22
+ const TOAST_DURATION_MS = 3_000
23
+ const ENHANCEMENT_CANCELED_MESSAGE = "Prompt enhancement canceled."
24
+ const ENHANCEMENT_ANIMATION_FRAMES = [
25
+ "Enhancing prompt",
26
+ "Enhancing prompt.",
27
+ "Enhancing prompt..",
28
+ "Enhancing prompt...",
29
+ ] as const
30
+ const DIALOG_TITLE = "Enhance Prompt"
31
+ const DIALOG_WIDTH_RATIO = 0.60
32
+ const DIALOG_MAX_WIDTH = 120
33
+ const DIALOG_MIN_WIDTH = 40
34
+ const DIALOG_SCREEN_MARGIN_X = 4
35
+ const DIALOG_SCREEN_MARGIN_Y = 4
36
+ const DIALOG_HEIGHT = 24
37
+ const DIALOG_PADDING_X = 2
38
+ const DIALOG_PADDING_Y = 1
39
+ const DIALOG_TEXTAREA_RESERVED_HEIGHT = 7
40
+ const DIALOG_TEXTAREA_VERTICAL_PADDING = 2
41
+ const DIALOG_BACKDROP_COLOR = "#000000"
42
+ const DIALOG_BACKDROP_OPACITY = 0.65
43
+ const DIALOG_PLACEHOLDER = "Describe the task..."
44
+ const DIALOG_HINT = "Enter to enhance • Shift+Enter for newline • Esc to cancel"
45
+ const TOAST_TITLE = "Prompt enhancer"
46
+
47
+ type ModelRef = {
48
+ providerID: string
49
+ modelID: string
50
+ }
51
+
52
+ type Api = Parameters<TuiPlugin>[0]
53
+ type ActiveEnhancement = {
54
+ controller: AbortController
55
+ stopAnimation: () => void
56
+ clearPromise?: Promise<boolean>
57
+ handle: PromptHandle
58
+ originalPrompt?: TuiPromptInfo
59
+ input: string
60
+ canceled?: boolean
61
+ }
62
+
63
+ type PluginState = {
64
+ activeEnhancement?: ActiveEnhancement
65
+ promptRef?: TuiPromptRef
66
+ promptTarget?: PromptTarget
67
+ lastEnhancement?: {
68
+ original: TuiPromptInfo
69
+ enhancedInput: string
70
+ target: PromptTarget
71
+ directory: string
72
+ }
73
+ }
74
+
75
+ type PromptTarget =
76
+ | { name: "home" }
77
+ | { name: "session", sessionID: string }
78
+
79
+ type PromptHandle = {
80
+ target: PromptTarget
81
+ directory: string
82
+ ref?: TuiPromptRef
83
+ }
84
+
85
+ type PromptUpdate = {
86
+ fallbackInput: string
87
+ createPromptInfo: (current: TuiPromptInfo) => TuiPromptInfo
88
+ refAction: "focus" | "blur"
89
+ }
90
+
91
+ type DialogTextareaRef = {
92
+ plainText: string
93
+ cursorOffset: number
94
+ focus(): void
95
+ }
96
+
97
+ type EnhanceDialogState = {
98
+ initialValue: string
99
+ onCancel: () => void
100
+ onConfirm: (value: string) => void
101
+ }
102
+
103
+ type SetEnhanceDialog = (dialog: EnhanceDialogState | undefined) => void
104
+
105
+ type EnhancementInput = {
106
+ command?: string
107
+ draft: string
108
+ }
109
+
110
+ function parseEnhancementInput(input: string): EnhancementInput {
111
+ const match = input.match(/^(\/[A-Za-z0-9][A-Za-z0-9._:-]*(?:\/[A-Za-z0-9][A-Za-z0-9._:-]*)*)(?:(?: +|\n)([\s\S]*))?$/)
112
+ if (!match) return { draft: input }
113
+
114
+ return {
115
+ command: match[1],
116
+ draft: match[2] ?? "",
117
+ }
118
+ }
119
+
120
+ function formatEnhancedInput(input: EnhancementInput, enhancedDraft: string): string {
121
+ if (!input.command) return enhancedDraft
122
+
123
+ const nested = parseEnhancementInput(enhancedDraft)
124
+ const draft = nested.command === input.command ? nested.draft : enhancedDraft
125
+ return draft ? `${input.command} ${draft}` : input.command
126
+ }
127
+
128
+ function parseModelString(value: string | undefined): ModelRef | undefined {
129
+ if (!value) return undefined
130
+ const trimmed = value.trim()
131
+ const slash = trimmed.indexOf("/")
132
+ if (slash <= 0 || slash === trimmed.length - 1) return undefined
133
+ return {
134
+ providerID: trimmed.slice(0, slash),
135
+ modelID: trimmed.slice(slash + 1),
136
+ }
137
+ }
138
+
139
+ function isSessionRoute(route: TuiRouteCurrent): route is Extract<TuiRouteCurrent, { name: "session" }> {
140
+ return route.name === "session"
141
+ }
142
+
143
+ function extractVisibleText(parts: ReadonlyArray<Part>): string {
144
+ return parts
145
+ .filter((part): part is TextPart => part.type === "text" && !part.ignored)
146
+ .map((part) => part.text)
147
+ .join("")
148
+ }
149
+
150
+ function formatContextPreview(text: string): string {
151
+ if (text.length <= MAX_CONTEXT_ITEM_PREVIEW_LENGTH) return text
152
+
153
+ const available = MAX_CONTEXT_ITEM_PREVIEW_LENGTH - CONTEXT_TRUNCATION_MARKER.length
154
+ const headLength = Math.ceil(available / 2)
155
+ const tailLength = available - headLength
156
+ return `${text.slice(0, headLength)}${CONTEXT_TRUNCATION_MARKER}${text.slice(-tailLength)}`
157
+ }
158
+
159
+ function indentContextContinuation(text: string, indentation: string): string {
160
+ return text.replaceAll("\n", `\n${indentation}`)
161
+ }
162
+
163
+ function resolveEnhancerModel(api: Api, options: PluginOptions | undefined): ModelRef | undefined {
164
+ const modelOverride = typeof options?.model === "string" ? options.model : undefined
165
+ if (modelOverride?.trim()) {
166
+ const override = parseModelString(modelOverride)
167
+ if (override) return override
168
+
169
+ api.ui.toast({
170
+ variant: "warning",
171
+ title: TOAST_TITLE,
172
+ message: `Invalid model override ${JSON.stringify(modelOverride)}; expected "provider/model". Using default model.`,
173
+ })
174
+ }
175
+
176
+ return parseModelString(api.state.config.small_model || api.state.config.model)
177
+ }
178
+
179
+ function clonePromptInfo(prompt: TuiPromptInfo): TuiPromptInfo {
180
+ return {
181
+ input: prompt.input,
182
+ mode: prompt.mode,
183
+ parts: prompt.parts.map((part) => ({ ...part })),
184
+ }
185
+ }
186
+
187
+ function nextPromptInfo(prompt: TuiPromptInfo, input: string): TuiPromptInfo {
188
+ return {
189
+ input,
190
+ mode: prompt.mode,
191
+ parts: prompt.parts.filter((part) => part.type !== "text").map((part) => ({ ...part })),
192
+ }
193
+ }
194
+
195
+ function samePromptInfo(left: TuiPromptInfo, right: TuiPromptInfo): boolean {
196
+ return left.input === right.input && left.mode === right.mode && JSON.stringify(left.parts) === JSON.stringify(right.parts)
197
+ }
198
+
199
+ function errorFromReason(reason: unknown, fallbackMessage: string): Error {
200
+ return reason instanceof Error ? reason : new Error(fallbackMessage)
201
+ }
202
+
203
+ function clamp(value: number, min: number, max: number): number {
204
+ return Math.min(Math.max(value, min), max)
205
+ }
206
+
207
+ async function withRequestTimeout<T>(
208
+ signal: AbortSignal,
209
+ timeoutMs: number,
210
+ run: (signal: AbortSignal) => Promise<T>,
211
+ ): Promise<T> {
212
+ const controller = new AbortController()
213
+ const timeoutMessage = `Prompt enhancement timed out after ${Math.floor(timeoutMs / 1000)} seconds.`
214
+ const onAbort = () => controller.abort(signal.reason)
215
+ const timeout = setTimeout(() => controller.abort(new Error(timeoutMessage)), timeoutMs)
216
+
217
+ if (signal.aborted) {
218
+ controller.abort(signal.reason)
219
+ } else {
220
+ signal.addEventListener("abort", onAbort, { once: true })
221
+ }
222
+
223
+ try {
224
+ return await run(controller.signal)
225
+ } catch (error) {
226
+ if (controller.signal.aborted && !signal.aborted) {
227
+ throw errorFromReason(controller.signal.reason, timeoutMessage)
228
+ }
229
+ throw error
230
+ } finally {
231
+ clearTimeout(timeout)
232
+ signal.removeEventListener("abort", onAbort)
233
+ }
234
+ }
235
+
236
+ function samePromptTarget(left: PromptTarget | undefined, right: PromptTarget | undefined): boolean {
237
+ if (!left || !right || left.name !== right.name) return false
238
+ if (left.name === "session" && right.name === "session") {
239
+ return left.sessionID === right.sessionID
240
+ }
241
+ return true
242
+ }
243
+
244
+ function isPromptHandleActive(api: Api, state: PluginState, handle: PromptHandle): boolean {
245
+ const route = api.route.current
246
+ const target = handle.target
247
+ if (target.name === "home") {
248
+ if (route.name !== "home") return false
249
+ } else if (!isSessionRoute(route) || route.params.sessionID !== target.sessionID) {
250
+ return false
251
+ }
252
+ if (api.state.path.directory !== handle.directory) return false
253
+
254
+ if (handle.ref) {
255
+ return state.promptRef === handle.ref && samePromptTarget(state.promptTarget, handle.target)
256
+ }
257
+
258
+ return true
259
+ }
260
+
261
+ function bindPromptRef(
262
+ state: PluginState,
263
+ target: PromptTarget,
264
+ forwarded: ((ref: TuiPromptRef | undefined) => void) | undefined,
265
+ ref: TuiPromptRef | undefined,
266
+ ): void {
267
+ if (ref) {
268
+ state.promptRef = ref
269
+ state.promptTarget = target
270
+ } else if (samePromptTarget(state.promptTarget, target)) {
271
+ state.promptRef = undefined
272
+ state.promptTarget = undefined
273
+ }
274
+
275
+ forwarded?.(ref)
276
+ }
277
+
278
+ async function applyPromptUpdate(
279
+ api: Api,
280
+ state: PluginState,
281
+ handle: PromptHandle,
282
+ update: PromptUpdate,
283
+ signal: AbortSignal,
284
+ ): Promise<boolean> {
285
+ if (!isPromptHandleActive(api, state, handle)) return false
286
+
287
+ const promptRef = handle.ref
288
+ if (promptRef) {
289
+ promptRef.set(update.createPromptInfo(promptRef.current))
290
+ if (update.refAction === "focus") promptRef.focus()
291
+ else promptRef.blur()
292
+ return true
293
+ }
294
+
295
+ const requestOptions = { signal, throwOnError: true } as const
296
+ await api.client.tui.clearPrompt({ directory: handle.directory }, requestOptions)
297
+ if (update.fallbackInput) {
298
+ await api.client.tui.appendPrompt({ directory: handle.directory, text: update.fallbackInput }, requestOptions)
299
+ }
300
+ return true
301
+ }
302
+
303
+ function clearPrompt(api: Api, state: PluginState, handle: PromptHandle, signal: AbortSignal, template?: TuiPromptInfo): Promise<boolean> {
304
+ return applyPromptUpdate(api, state, handle, {
305
+ fallbackInput: "",
306
+ createPromptInfo: (current) => nextPromptInfo(template ?? current, ""),
307
+ refAction: "blur",
308
+ }, signal)
309
+ }
310
+
311
+ function writePrompt(
312
+ api: Api,
313
+ state: PluginState,
314
+ handle: PromptHandle,
315
+ input: string,
316
+ signal: AbortSignal,
317
+ template?: TuiPromptInfo,
318
+ ): Promise<boolean> {
319
+ return applyPromptUpdate(api, state, handle, {
320
+ fallbackInput: input,
321
+ createPromptInfo: (current) => nextPromptInfo(template ?? current, input),
322
+ refAction: "focus",
323
+ }, signal)
324
+ }
325
+
326
+ function restorePrompt(
327
+ api: Api,
328
+ state: PluginState,
329
+ handle: PromptHandle,
330
+ prompt: TuiPromptInfo,
331
+ signal: AbortSignal,
332
+ ): Promise<boolean> {
333
+ return applyPromptUpdate(api, state, handle, {
334
+ fallbackInput: prompt.input,
335
+ createPromptInfo: () => clonePromptInfo(prompt),
336
+ refAction: "focus",
337
+ }, signal)
338
+ }
339
+
340
+ function restoreEnhancementPrompt(
341
+ api: Api,
342
+ state: PluginState,
343
+ enhancement: ActiveEnhancement,
344
+ signal: AbortSignal,
345
+ ): Promise<boolean> {
346
+ return enhancement.originalPrompt
347
+ ? restorePrompt(api, state, enhancement.handle, enhancement.originalPrompt, signal)
348
+ : writePrompt(api, state, enhancement.handle, enhancement.input, signal)
349
+ }
350
+
351
+ async function cancelActiveEnhancement(
352
+ api: Api,
353
+ state: PluginState,
354
+ signal: AbortSignal,
355
+ ): Promise<boolean> {
356
+ const active = state.activeEnhancement
357
+ if (!active) return false
358
+
359
+ active.canceled = true
360
+ active.stopAnimation()
361
+ active.controller.abort(new Error(ENHANCEMENT_CANCELED_MESSAGE))
362
+
363
+ if (active.clearPromise) {
364
+ await active.clearPromise.catch(() => false)
365
+ }
366
+
367
+ return restoreEnhancementPrompt(api, state, active, signal)
368
+ }
369
+
370
+ function startEnhancementAnimation(
371
+ api: Api,
372
+ state: PluginState,
373
+ handle: PromptHandle,
374
+ template?: TuiPromptInfo,
375
+ ): () => void {
376
+ const promptRef = handle.ref
377
+ if (!promptRef) return () => {}
378
+
379
+ let frame = 0
380
+ let stopped = false
381
+
382
+ const render = () => {
383
+ if (stopped) return
384
+ if (!isPromptHandleActive(api, state, handle)) {
385
+ stopped = true
386
+ return
387
+ }
388
+
389
+ promptRef.set(nextPromptInfo(template ?? promptRef.current, ENHANCEMENT_ANIMATION_FRAMES[frame]))
390
+ frame = (frame + 1) % ENHANCEMENT_ANIMATION_FRAMES.length
391
+ }
392
+
393
+ render()
394
+ const interval = setInterval(render, ENHANCEMENT_ANIMATION_INTERVAL_MS)
395
+ return () => {
396
+ stopped = true
397
+ clearInterval(interval)
398
+ }
399
+ }
400
+
401
+ function gatherContext(api: Api): string {
402
+ const sections: string[] = []
403
+
404
+ const route = api.route.current
405
+ if (isSessionRoute(route)) {
406
+ const sessionID = route.params.sessionID
407
+ const messages = api.state.session.messages(sessionID)
408
+
409
+ const userMessages = messages.filter((message): message is Extract<Message, { role: "user" }> => message.role === "user")
410
+ const recent = userMessages.slice(-MAX_RECENT_MESSAGES).reverse()
411
+ if (recent.length > 0) {
412
+ const prompts: string[] = []
413
+ for (const msg of recent) {
414
+ const text = extractVisibleText(api.state.part(msg.id)).trim()
415
+ if (text) {
416
+ prompts.push(formatContextPreview(text))
417
+ }
418
+ }
419
+ if (prompts.length > 0) {
420
+ const formatted = prompts.map((prompt, index) => `${index + 1}. ${indentContextContinuation(prompt, " ")}`).join("\n")
421
+ sections.push(`Recent user prompts in this session (newest first; use only same-task items):\n${formatted}`)
422
+ }
423
+ }
424
+
425
+ const diff = api.state.session.diff(sessionID)
426
+ if (diff.length > 0) {
427
+ const visibleFiles = diff.slice(0, MAX_CHANGED_FILES)
428
+ const count = diff.length > visibleFiles.length ? `; showing ${visibleFiles.length} of ${diff.length}` : ""
429
+ const files = visibleFiles.map((file) => ` @${file.file}`)
430
+ sections.push(`Files changed in session (candidates only; not proof of task intent${count}):\n${files.join("\n")}`)
431
+ }
432
+ }
433
+
434
+ const metadata = [`Working directory: ${api.state.path.directory}`]
435
+ const branch = api.state.vcs?.branch
436
+ if (branch) {
437
+ metadata.push(`Current branch: ${branch}`)
438
+ }
439
+ sections.push(`Workspace metadata (weak signal only):\n${metadata.join("\n")}`)
440
+
441
+ return sections.join("\n\n")
442
+ }
443
+
444
+ async function enhanceWithModel(
445
+ api: Api,
446
+ options: PluginOptions | undefined,
447
+ input: string,
448
+ signal: AbortSignal,
449
+ ): Promise<string> {
450
+ const directory = api.state.path.directory
451
+ const model = resolveEnhancerModel(api, options)
452
+ const context = gatherContext(api)
453
+ const userMessage = [
454
+ `<CONTEXT>\n${context}\n</CONTEXT>`,
455
+ `<DRAFT>\n${input}\n</DRAFT>`,
456
+ ].join("\n\n")
457
+
458
+ const created = await api.client.session.create(
459
+ {
460
+ directory,
461
+ title: `Prompt Enhancer ${Math.random().toString(36).slice(2, 8)}`,
462
+ permission: [{ permission: "*", action: "deny", pattern: "*" }],
463
+ },
464
+ { signal, throwOnError: true },
465
+ )
466
+
467
+ const tempSessionID = created.data?.id
468
+ if (!tempSessionID) throw new Error("Failed to start prompt enhancer.")
469
+
470
+ try {
471
+ const response = await withRequestTimeout(
472
+ signal,
473
+ ENHANCEMENT_TIMEOUT_MS,
474
+ (requestSignal) => api.client.session.prompt(
475
+ {
476
+ sessionID: tempSessionID,
477
+ directory,
478
+ model,
479
+ system: ENHANCER_SYSTEM_PROMPT,
480
+ parts: [
481
+ {
482
+ type: "text",
483
+ text: userMessage,
484
+ },
485
+ ],
486
+ },
487
+ { signal: requestSignal, throwOnError: true },
488
+ ),
489
+ )
490
+
491
+ const parts = response.data?.parts
492
+ if (!parts) throw new Error("Enhancer returned no response.")
493
+
494
+ const enhanced = extractVisibleText(parts)
495
+ if (!enhanced.trim()) throw new Error("Enhancer returned no text.")
496
+ return enhanced
497
+ } finally {
498
+ try {
499
+ await api.client.session.abort({ sessionID: tempSessionID, directory })
500
+ } catch {
501
+ // WHY: Cleanup continues with deletion when the helper has already stopped.
502
+ }
503
+ try {
504
+ await api.client.session.delete({ sessionID: tempSessionID, directory })
505
+ } catch {
506
+ // WHY: Helper cleanup must not replace a successful enhancement result.
507
+ }
508
+ }
509
+ }
510
+
511
+ function renderEnhanceDialog(api: Api, dialog: EnhanceDialogState) {
512
+ const theme = api.theme.current
513
+ const terminal = useTerminalDimensions()
514
+ const screenWidth = createMemo(() => Math.max(1, terminal().width))
515
+ const screenHeight = createMemo(() => Math.max(1, terminal().height))
516
+ const panelWidth = createMemo(() => {
517
+ const width = screenWidth()
518
+ const availableWidth = Math.max(1, width - DIALOG_SCREEN_MARGIN_X)
519
+ const minWidth = Math.min(DIALOG_MIN_WIDTH, availableWidth)
520
+ const maxWidth = Math.min(DIALOG_MAX_WIDTH, availableWidth)
521
+ return clamp(Math.floor(width * DIALOG_WIDTH_RATIO), minWidth, maxWidth)
522
+ })
523
+ const panelHeight = createMemo(() => {
524
+ const availableHeight = Math.max(1, screenHeight() - DIALOG_SCREEN_MARGIN_Y)
525
+ return Math.min(DIALOG_HEIGHT, availableHeight)
526
+ })
527
+ const textareaBoxHeight = createMemo(() => Math.max(1, panelHeight() - DIALOG_TEXTAREA_RESERVED_HEIGHT))
528
+ const textareaHeight = createMemo(() => Math.max(1, textareaBoxHeight() - DIALOG_TEXTAREA_VERTICAL_PADDING))
529
+ let dialogInput: DialogTextareaRef | undefined
530
+
531
+ return (
532
+ <box
533
+ position="absolute"
534
+ top={0}
535
+ left={0}
536
+ width={screenWidth()}
537
+ height={screenHeight()}
538
+ justifyContent="center"
539
+ alignItems="center"
540
+ >
541
+ <box
542
+ position="absolute"
543
+ top={0}
544
+ left={0}
545
+ width={screenWidth()}
546
+ height={screenHeight()}
547
+ backgroundColor={DIALOG_BACKDROP_COLOR}
548
+ opacity={DIALOG_BACKDROP_OPACITY}
549
+ />
550
+ <box
551
+ flexDirection="column"
552
+ width={panelWidth()}
553
+ height={panelHeight()}
554
+ gap={1}
555
+ paddingX={DIALOG_PADDING_X}
556
+ paddingY={DIALOG_PADDING_Y}
557
+ backgroundColor={theme.backgroundPanel}
558
+ border
559
+ borderColor={theme.border}
560
+ >
561
+ <text>{DIALOG_TITLE}</text>
562
+ <box
563
+ border
564
+ borderColor={theme.borderActive}
565
+ flexDirection="column"
566
+ height={textareaBoxHeight()}
567
+ paddingX={1}
568
+ paddingY={1}
569
+ >
570
+ <textarea
571
+ ref={(node: DialogTextareaRef | undefined) => {
572
+ dialogInput = node
573
+ if (!node) return
574
+ node.cursorOffset = node.plainText.length
575
+ queueMicrotask(() => {
576
+ if (dialogInput === node) node.focus()
577
+ })
578
+ }}
579
+ width="100%"
580
+ height={textareaHeight()}
581
+ initialValue={dialog.initialValue}
582
+ placeholder={DIALOG_PLACEHOLDER}
583
+ wrapMode="word"
584
+ textColor={theme.text}
585
+ placeholderColor={theme.textMuted}
586
+ backgroundColor={theme.backgroundPanel}
587
+ focusedBackgroundColor={theme.backgroundPanel}
588
+ focusedTextColor={theme.text}
589
+ cursorColor={theme.primary}
590
+ keyBindings={[
591
+ { name: "return", action: "submit" },
592
+ { name: "linefeed", action: "submit" },
593
+ { name: "return", shift: true, action: "newline" },
594
+ { name: "linefeed", shift: true, action: "newline" },
595
+ ]}
596
+ onKeyDown={(key) => {
597
+ if (key.name === "escape" || (key.ctrl && key.name === "c")) dialog.onCancel()
598
+ }}
599
+ onSubmit={() => dialog.onConfirm(dialogInput?.plainText ?? "")}
600
+ />
601
+ </box>
602
+ <text fg={theme.textMuted}>{DIALOG_HINT}</text>
603
+ </box>
604
+ </box>
605
+ )
606
+ }
607
+
608
+ function openEnhanceDialog(
609
+ api: Api,
610
+ options: PluginOptions | undefined,
611
+ state: PluginState,
612
+ setEnhanceDialog: SetEnhanceDialog,
613
+ signal: AbortSignal,
614
+ ): void {
615
+ if (state.activeEnhancement) {
616
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Enhancement in progress." })
617
+ return
618
+ }
619
+
620
+ if (signal.aborted) return
621
+
622
+ const target = state.promptTarget ?? (api.route.current.name === "home"
623
+ ? { name: "home" as const }
624
+ : isSessionRoute(api.route.current)
625
+ ? { name: "session" as const, sessionID: api.route.current.params.sessionID }
626
+ : undefined)
627
+ if (!target) {
628
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Enhancement only works from a prompt." })
629
+ return
630
+ }
631
+
632
+ const handle: PromptHandle = {
633
+ target,
634
+ directory: api.state.path.directory,
635
+ ref: state.promptRef,
636
+ }
637
+ const originalPrompt = handle.ref ? clonePromptInfo(handle.ref.current) : undefined
638
+ const initialValue = originalPrompt?.input ?? ""
639
+
640
+ const closeDialog = () => setEnhanceDialog(undefined)
641
+
642
+ const confirmInput = (value: string) => {
643
+ if (state.activeEnhancement) return
644
+
645
+ if (!value.trim()) {
646
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Enter a prompt first." })
647
+ closeDialog()
648
+ return
649
+ }
650
+
651
+ const enhancementInput = parseEnhancementInput(value)
652
+ if (!enhancementInput.draft.trim()) {
653
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Enter instructions after the slash command." })
654
+ closeDialog()
655
+ return
656
+ }
657
+
658
+ const promptChanged = originalPrompt && handle.ref
659
+ ? !samePromptInfo(handle.ref.current, originalPrompt)
660
+ : false
661
+ if (!isPromptHandleActive(api, state, handle) || promptChanged) {
662
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Prompt changed while dialog was open." })
663
+ closeDialog()
664
+ return
665
+ }
666
+
667
+ closeDialog()
668
+ api.ui.toast({
669
+ variant: "info",
670
+ title: TOAST_TITLE,
671
+ message: "Enhancing prompt...",
672
+ duration: TOAST_DURATION_MS,
673
+ })
674
+
675
+ const enhancementController = new AbortController()
676
+ const onLifecycleAbort = () => enhancementController.abort(signal.reason)
677
+ const activeEnhancement: ActiveEnhancement = {
678
+ controller: enhancementController,
679
+ stopAnimation: () => {},
680
+ handle,
681
+ originalPrompt,
682
+ input: value,
683
+ }
684
+ state.activeEnhancement = activeEnhancement
685
+ if (signal.aborted) {
686
+ enhancementController.abort(signal.reason)
687
+ } else {
688
+ signal.addEventListener("abort", onLifecycleAbort, { once: true })
689
+ }
690
+
691
+ void (async () => {
692
+ try {
693
+ const clearPromise = clearPrompt(api, state, handle, signal, originalPrompt)
694
+ activeEnhancement.clearPromise = clearPromise
695
+ const cleared = await clearPromise
696
+ if (!cleared) {
697
+ api.ui.toast({
698
+ variant: "warning",
699
+ title: TOAST_TITLE,
700
+ message: "Prompt changed before enhancement started.",
701
+ })
702
+ return
703
+ }
704
+
705
+ activeEnhancement.stopAnimation = startEnhancementAnimation(api, state, handle, originalPrompt)
706
+
707
+ const enhancedDraft = await enhanceWithModel(api, options, enhancementInput.draft, enhancementController.signal)
708
+ const enhanced = formatEnhancedInput(enhancementInput, enhancedDraft)
709
+ if (signal.aborted) return
710
+ if (enhancementController.signal.aborted) {
711
+ throw errorFromReason(enhancementController.signal.reason, ENHANCEMENT_CANCELED_MESSAGE)
712
+ }
713
+
714
+ activeEnhancement.stopAnimation()
715
+
716
+ const wrote = await writePrompt(api, state, handle, enhanced, signal, originalPrompt)
717
+ if (!wrote) {
718
+ api.ui.toast({
719
+ variant: "warning",
720
+ title: TOAST_TITLE,
721
+ message: "Enhanced prompt is ready, but that prompt is no longer active.",
722
+ })
723
+ return
724
+ }
725
+ if (enhancementController.signal.aborted) {
726
+ throw errorFromReason(enhancementController.signal.reason, ENHANCEMENT_CANCELED_MESSAGE)
727
+ }
728
+
729
+ state.lastEnhancement = originalPrompt
730
+ ? {
731
+ original: clonePromptInfo(originalPrompt),
732
+ enhancedInput: enhanced,
733
+ target: { ...handle.target },
734
+ directory: handle.directory,
735
+ }
736
+ : undefined
737
+
738
+ api.ui.toast({
739
+ variant: "success",
740
+ title: "Prompt enhanced",
741
+ message: "Enhanced prompt added to input.",
742
+ duration: TOAST_DURATION_MS,
743
+ })
744
+ } catch (error) {
745
+ if (signal.aborted) return
746
+
747
+ activeEnhancement.stopAnimation()
748
+
749
+ const canceled = enhancementController.signal.aborted && !signal.aborted
750
+ if (canceled && state.activeEnhancement?.canceled) {
751
+ // cancelActiveEnhancement already stopped the animation and restored the prompt.
752
+ return
753
+ }
754
+
755
+ let restored: boolean
756
+ try {
757
+ restored = await restoreEnhancementPrompt(api, state, activeEnhancement, signal)
758
+ } catch {
759
+ // Best-effort restore; do not suppress the error toast.
760
+ restored = false
761
+ }
762
+ let baseMessage: string
763
+ if (canceled) {
764
+ baseMessage = ENHANCEMENT_CANCELED_MESSAGE
765
+ } else if (error instanceof Error) {
766
+ baseMessage = error.message
767
+ } else {
768
+ baseMessage = "Prompt enhancement failed."
769
+ }
770
+ let message: string
771
+ if (canceled && restored) {
772
+ message = "Enhancement canceled. Original prompt restored."
773
+ } else if (restored) {
774
+ message = baseMessage
775
+ } else {
776
+ message = `${baseMessage} Original prompt could not be restored because the prompt changed. Please re-enter your prompt manually.`
777
+ }
778
+ api.ui.toast({ variant: canceled && restored ? "info" : "error", title: TOAST_TITLE, message })
779
+ } finally {
780
+ activeEnhancement.stopAnimation()
781
+ signal.removeEventListener("abort", onLifecycleAbort)
782
+ if (state.activeEnhancement?.controller === enhancementController) {
783
+ state.activeEnhancement = undefined
784
+ }
785
+ }
786
+ })()
787
+ }
788
+
789
+ setEnhanceDialog({ initialValue, onCancel: closeDialog, onConfirm: confirmInput })
790
+ }
791
+
792
+ function revertEnhancement(
793
+ api: Api,
794
+ state: PluginState,
795
+ signal: AbortSignal,
796
+ ): void {
797
+ if (state.activeEnhancement) {
798
+ void (async () => {
799
+ try {
800
+ const restored = await cancelActiveEnhancement(api, state, signal)
801
+ if (!restored) {
802
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Prompt changed while canceling." })
803
+ return
804
+ }
805
+
806
+ api.ui.toast({
807
+ variant: "info",
808
+ title: TOAST_TITLE,
809
+ message: "Enhancement canceled. Original prompt restored.",
810
+ duration: TOAST_DURATION_MS,
811
+ })
812
+ } catch (error) {
813
+ if (signal.aborted) return
814
+ const message = error instanceof Error ? error.message : "Cancel failed."
815
+ api.ui.toast({ variant: "error", title: TOAST_TITLE, message })
816
+ }
817
+ })()
818
+ return
819
+ }
820
+
821
+ const lastEnhancement = state.lastEnhancement
822
+ if (!lastEnhancement) {
823
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "No enhancement to revert." })
824
+ return
825
+ }
826
+
827
+ const target = state.promptTarget
828
+ if (!target) {
829
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Revert only works from a prompt." })
830
+ return
831
+ }
832
+
833
+ if (api.state.path.directory !== lastEnhancement.directory || !samePromptTarget(target, lastEnhancement.target)) {
834
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "The enhanced prompt is no longer active." })
835
+ return
836
+ }
837
+
838
+ const handle: PromptHandle = {
839
+ target,
840
+ directory: api.state.path.directory,
841
+ ref: state.promptRef,
842
+ }
843
+
844
+ if (!isPromptHandleActive(api, state, handle)) {
845
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Prompt changed since enhancement." })
846
+ return
847
+ }
848
+
849
+ const currentPrompt = handle.ref?.current
850
+ const expectedEnhancedPrompt = nextPromptInfo(lastEnhancement.original, lastEnhancement.enhancedInput)
851
+ if (!currentPrompt || !samePromptInfo(currentPrompt, expectedEnhancedPrompt)) {
852
+ api.ui.toast({
853
+ variant: "warning",
854
+ title: TOAST_TITLE,
855
+ message: "Prompt was manually changed after enhancement. Revert skipped.",
856
+ })
857
+ return
858
+ }
859
+
860
+ void (async () => {
861
+ try {
862
+ const wrote = await restorePrompt(api, state, handle, lastEnhancement.original, signal)
863
+ if (!wrote) {
864
+ api.ui.toast({
865
+ variant: "warning",
866
+ title: TOAST_TITLE,
867
+ message: "Prompt changed while reverting.",
868
+ })
869
+ return
870
+ }
871
+
872
+ state.lastEnhancement = undefined
873
+
874
+ api.ui.toast({
875
+ variant: "success",
876
+ title: TOAST_TITLE,
877
+ message: "Reverted to original prompt.",
878
+ duration: TOAST_DURATION_MS,
879
+ })
880
+ } catch (error) {
881
+ if (signal.aborted) return
882
+ const message = error instanceof Error ? error.message : "Revert failed."
883
+ api.ui.toast({ variant: "error", title: TOAST_TITLE, message })
884
+ }
885
+ })()
886
+ }
887
+
888
+ const tui: TuiPlugin = async (api, options) => {
889
+ const state: PluginState = {}
890
+ const [enhanceDialog, setEnhanceDialog] = createSignal<EnhanceDialogState | undefined>()
891
+
892
+ const promptSlots: TuiSlotPlugin = {
893
+ slots: {
894
+ app() {
895
+ return (
896
+ <Show when={enhanceDialog()}>
897
+ {(dialog) => renderEnhanceDialog(api, dialog())}
898
+ </Show>
899
+ )
900
+ },
901
+ home_prompt(_ctx, props) {
902
+ return (
903
+ <api.ui.Prompt
904
+ ref={(ref) => bindPromptRef(state, { name: "home" }, props.ref, ref)}
905
+ right={<api.ui.Slot name="home_prompt_right" />}
906
+ />
907
+ )
908
+ },
909
+ session_prompt(_ctx, props) {
910
+ return (
911
+ <api.ui.Prompt
912
+ ref={(ref) => bindPromptRef(state, { name: "session", sessionID: props.session_id }, props.ref, ref)}
913
+ sessionID={props.session_id}
914
+ visible={props.visible}
915
+ disabled={props.disabled}
916
+ onSubmit={props.on_submit}
917
+ right={<api.ui.Slot name="session_prompt_right" session_id={props.session_id} />}
918
+ />
919
+ )
920
+ },
921
+ },
922
+ }
923
+
924
+ api.slots.register(promptSlots)
925
+
926
+ const unregister = api.keymap.registerLayer({
927
+ commands: [
928
+ {
929
+ name: "prompt-enhancer.enhance",
930
+ title: DIALOG_TITLE,
931
+ desc: "Enhance current prompt",
932
+ category: "Prompt",
933
+ suggested: true,
934
+ run: () => {
935
+ openEnhanceDialog(api, options, state, setEnhanceDialog, api.lifecycle.signal)
936
+ },
937
+ },
938
+ {
939
+ name: "prompt-enhancer.revert",
940
+ title: "Revert Enhanced Prompt",
941
+ desc: "Revert last prompt enhancement",
942
+ category: "Prompt",
943
+ run: () => {
944
+ revertEnhancement(api, state, api.lifecycle.signal)
945
+ },
946
+ },
947
+ ],
948
+ bindings: [
949
+ {
950
+ key: "ctrl+e",
951
+ cmd: "prompt-enhancer.enhance",
952
+ },
953
+ {
954
+ key: "ctrl+shift+e",
955
+ cmd: "prompt-enhancer.revert",
956
+ },
957
+ ],
958
+ })
959
+
960
+ api.lifecycle.onDispose(() => {
961
+ unregister()
962
+ setEnhanceDialog(undefined)
963
+
964
+ const active = state.activeEnhancement
965
+ if (active) {
966
+ active.stopAnimation()
967
+ if (active.originalPrompt && active.handle.ref) {
968
+ try {
969
+ active.handle.ref.set(clonePromptInfo(active.originalPrompt))
970
+ active.handle.ref.focus()
971
+ } catch {
972
+ // WHY: Teardown restoration is best-effort because the captured prompt ref may already be detached.
973
+ }
974
+ }
975
+ active.controller.abort(api.lifecycle.signal.reason)
976
+ }
977
+
978
+ state.activeEnhancement = undefined
979
+ state.promptRef = undefined
980
+ state.promptTarget = undefined
981
+ state.lastEnhancement = undefined
982
+ })
983
+ }
984
+
985
+ const plugin = {
986
+ id: "prompt-enhancer",
987
+ tui,
988
+ } satisfies TuiPluginModule & { id: string }
989
+
990
+ export default plugin