@mtayfur/opencode-prompt-enhancer 0.0.10 → 0.0.12

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mtayfur/opencode-prompt-enhancer",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "OpenCode plugin that rewrites rough drafts into stronger prompts.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -14,6 +14,16 @@ import { ENHANCER_SYSTEM_PROMPT } from "./enhancer-system-prompt"
14
14
  const MAX_RECENT_MESSAGES = 3
15
15
  const MAX_CHANGED_FILES = 25
16
16
  const MAX_PROMPT_PREVIEW_LENGTH = 250
17
+ const ENHANCEMENT_TIMEOUT_MS = 60_000
18
+ const ENHANCEMENT_ANIMATION_INTERVAL_MS = 250
19
+ const TOAST_DURATION_MS = 3_000
20
+ const ENHANCEMENT_CANCELED_MESSAGE = "Prompt enhancement canceled."
21
+ const ENHANCEMENT_ANIMATION_FRAMES = [
22
+ "Enhancing prompt",
23
+ "Enhancing prompt.",
24
+ "Enhancing prompt..",
25
+ "Enhancing prompt...",
26
+ ] as const
17
27
  const DIALOG_TITLE = "Enhance Prompt"
18
28
  const TOAST_TITLE = "Prompt enhancer"
19
29
 
@@ -23,8 +33,18 @@ type ModelRef = {
23
33
  }
24
34
 
25
35
  type Api = Parameters<TuiPlugin>[0]
36
+ type ActiveEnhancement = {
37
+ controller: AbortController
38
+ stopAnimation?: () => void
39
+ handle: PromptHandle
40
+ originalPrompt?: TuiPromptInfo
41
+ input: string
42
+ canceled?: boolean
43
+ }
44
+
26
45
  type PluginState = {
27
46
  enhancing: boolean
47
+ activeEnhancement?: ActiveEnhancement
28
48
  promptRef?: TuiPromptRef
29
49
  promptTarget?: PromptTarget
30
50
  lastOriginal?: TuiPromptInfo
@@ -87,6 +107,39 @@ function nextPromptInfo(prompt: TuiPromptInfo, input: string): TuiPromptInfo {
87
107
  }
88
108
  }
89
109
 
110
+ function errorFromReason(reason: unknown, fallbackMessage: string): Error {
111
+ return reason instanceof Error ? reason : new Error(fallbackMessage)
112
+ }
113
+
114
+ async function withRequestTimeout<T>(
115
+ signal: AbortSignal,
116
+ timeoutMs: number,
117
+ run: (signal: AbortSignal) => Promise<T>,
118
+ ): Promise<T> {
119
+ const controller = new AbortController()
120
+ const timeoutMessage = `Prompt enhancement timed out after ${Math.floor(timeoutMs / 1000)} seconds.`
121
+ const onAbort = () => controller.abort(signal.reason)
122
+ const timeout = setTimeout(() => controller.abort(new Error(timeoutMessage)), timeoutMs)
123
+
124
+ if (signal.aborted) {
125
+ controller.abort(signal.reason)
126
+ } else {
127
+ signal.addEventListener("abort", onAbort, { once: true })
128
+ }
129
+
130
+ try {
131
+ return await run(controller.signal)
132
+ } catch (error) {
133
+ if (controller.signal.aborted && !signal.aborted) {
134
+ throw errorFromReason(controller.signal.reason, timeoutMessage)
135
+ }
136
+ throw error
137
+ } finally {
138
+ clearTimeout(timeout)
139
+ signal.removeEventListener("abort", onAbort)
140
+ }
141
+ }
142
+
90
143
  function samePromptTarget(left: PromptTarget | undefined, right: PromptTarget | undefined): boolean {
91
144
  if (!left || !right || left.name !== right.name) return false
92
145
  if (left.name === "home" && right.name === "home") {
@@ -138,6 +191,7 @@ async function clearPrompt(api: Api, state: PluginState, handle: PromptHandle, s
138
191
  const promptRef = handle.ref
139
192
  if (promptRef) {
140
193
  promptRef.set(nextPromptInfo(template ?? promptRef.current, ""))
194
+ promptRef.blur()
141
195
  return true
142
196
  }
143
197
 
@@ -186,7 +240,62 @@ async function restorePrompt(
186
240
  return true
187
241
  }
188
242
 
189
- return false
243
+ const requestOptions = { signal, throwOnError: true } as const
244
+ await api.client.tui.clearPrompt({ directory: handle.directory }, requestOptions)
245
+ if (prompt.input) {
246
+ await api.client.tui.appendPrompt({ directory: handle.directory, text: prompt.input }, requestOptions)
247
+ }
248
+ return true
249
+ }
250
+
251
+ async function cancelActiveEnhancement(
252
+ api: Api,
253
+ state: PluginState,
254
+ signal: AbortSignal,
255
+ ): Promise<boolean> {
256
+ const active = state.activeEnhancement
257
+ if (!state.enhancing || !active) return false
258
+
259
+ active.canceled = true
260
+ active.stopAnimation?.()
261
+ active.controller.abort(new Error(ENHANCEMENT_CANCELED_MESSAGE))
262
+
263
+ if (active.originalPrompt) {
264
+ return restorePrompt(api, state, active.handle, active.originalPrompt, signal)
265
+ }
266
+
267
+ return writePrompt(api, state, active.handle, active.input, signal)
268
+ }
269
+
270
+ function startEnhancementAnimation(
271
+ api: Api,
272
+ state: PluginState,
273
+ handle: PromptHandle,
274
+ template?: TuiPromptInfo,
275
+ ): () => void {
276
+ const promptRef = handle.ref
277
+ if (!promptRef) return () => {}
278
+
279
+ let frame = 0
280
+ let stopped = false
281
+
282
+ const render = () => {
283
+ if (stopped) return
284
+ if (!isPromptHandleActive(api, state, handle)) {
285
+ stopped = true
286
+ return
287
+ }
288
+
289
+ promptRef.set(nextPromptInfo(template ?? promptRef.current, ENHANCEMENT_ANIMATION_FRAMES[frame]))
290
+ frame = (frame + 1) % ENHANCEMENT_ANIMATION_FRAMES.length
291
+ }
292
+
293
+ render()
294
+ const interval = setInterval(render, ENHANCEMENT_ANIMATION_INTERVAL_MS)
295
+ return () => {
296
+ stopped = true
297
+ clearInterval(interval)
298
+ }
190
299
  }
191
300
 
192
301
  function gatherContext(api: Api): string {
@@ -225,8 +334,6 @@ function gatherContext(api: Api): string {
225
334
  const files = diff.slice(0, MAX_CHANGED_FILES).map((f) => ` @${f.file}`)
226
335
  sections.push(`Files changed in session:\n${files.join("\n")}`)
227
336
  }
228
-
229
-
230
337
  }
231
338
 
232
339
  return sections.join("\n\n")
@@ -259,20 +366,24 @@ async function enhanceWithModel(
259
366
  if (!tempSessionID) throw new Error("Failed to start prompt enhancer.")
260
367
 
261
368
  try {
262
- const response = await api.client.session.prompt(
263
- {
264
- sessionID: tempSessionID,
265
- directory,
266
- model,
267
- system: ENHANCER_SYSTEM_PROMPT,
268
- parts: [
269
- {
270
- type: "text",
271
- text: userMessage,
272
- },
273
- ],
274
- },
275
- { signal, throwOnError: true },
369
+ const response = await withRequestTimeout(
370
+ signal,
371
+ ENHANCEMENT_TIMEOUT_MS,
372
+ (requestSignal) => api.client.session.prompt(
373
+ {
374
+ sessionID: tempSessionID,
375
+ directory,
376
+ model,
377
+ system: ENHANCER_SYSTEM_PROMPT,
378
+ parts: [
379
+ {
380
+ type: "text",
381
+ text: userMessage,
382
+ },
383
+ ],
384
+ },
385
+ { signal: requestSignal, throwOnError: true },
386
+ ),
276
387
  )
277
388
 
278
389
  const parts = response.data?.parts
@@ -282,11 +393,9 @@ async function enhanceWithModel(
282
393
  if (!enhanced) throw new Error("Enhancer returned no text.")
283
394
  return enhanced
284
395
  } finally {
285
- try {
286
- await api.client.session.delete({ sessionID: tempSessionID, directory })
287
- } catch {
396
+ void api.client.session.delete({ sessionID: tempSessionID, directory }).catch(() => {
288
397
  // Best-effort cleanup for the temporary enhancement session.
289
- }
398
+ })
290
399
  }
291
400
  }
292
401
 
@@ -347,10 +456,25 @@ function openEnhanceDialog(
347
456
  variant: "info",
348
457
  title: TOAST_TITLE,
349
458
  message: "Enhancing prompt...",
350
- duration: 8_000,
459
+ duration: TOAST_DURATION_MS,
351
460
  })
352
461
 
462
+ const enhancementController = new AbortController()
463
+ const onLifecycleAbort = () => enhancementController.abort(signal.reason)
464
+ state.activeEnhancement = {
465
+ controller: enhancementController,
466
+ handle,
467
+ originalPrompt,
468
+ input,
469
+ }
470
+ if (signal.aborted) {
471
+ enhancementController.abort(signal.reason)
472
+ } else {
473
+ signal.addEventListener("abort", onLifecycleAbort, { once: true })
474
+ }
475
+
353
476
  void (async () => {
477
+ let stopAnimation = () => {}
354
478
  try {
355
479
  const cleared = await clearPrompt(api, state, handle, signal, originalPrompt)
356
480
  if (!cleared) {
@@ -362,8 +486,18 @@ function openEnhanceDialog(
362
486
  return
363
487
  }
364
488
 
365
- const enhanced = await enhanceWithModel(api, options, input, signal)
489
+ stopAnimation = startEnhancementAnimation(api, state, handle, originalPrompt)
490
+ if (state.activeEnhancement) {
491
+ state.activeEnhancement.stopAnimation = stopAnimation
492
+ }
493
+
494
+ const enhanced = await enhanceWithModel(api, options, input, enhancementController.signal)
366
495
  if (signal.aborted) return
496
+ if (enhancementController.signal.aborted) {
497
+ throw errorFromReason(enhancementController.signal.reason, ENHANCEMENT_CANCELED_MESSAGE)
498
+ }
499
+
500
+ stopAnimation()
367
501
 
368
502
  const wrote = await writePrompt(api, state, handle, enhanced, signal, originalPrompt)
369
503
  if (!wrote) {
@@ -374,6 +508,9 @@ function openEnhanceDialog(
374
508
  })
375
509
  return
376
510
  }
511
+ if (enhancementController.signal.aborted) {
512
+ throw errorFromReason(enhancementController.signal.reason, ENHANCEMENT_CANCELED_MESSAGE)
513
+ }
377
514
 
378
515
  state.lastOriginal = originalPrompt
379
516
  state.lastEnhancedInput = enhanced
@@ -382,11 +519,19 @@ function openEnhanceDialog(
382
519
  variant: "success",
383
520
  title: "Prompt enhanced",
384
521
  message: "Enhanced prompt added to input.",
385
- duration: 3000,
522
+ duration: TOAST_DURATION_MS,
386
523
  })
387
524
  } catch (error) {
388
525
  if (signal.aborted) return
389
526
 
527
+ stopAnimation()
528
+
529
+ const canceled = enhancementController.signal.aborted && !signal.aborted
530
+ if (canceled && state.activeEnhancement?.canceled) {
531
+ // cancelActiveEnhancement already stopped the animation and restored the prompt.
532
+ return
533
+ }
534
+
390
535
  let restored = true
391
536
  if (originalPrompt) {
392
537
  try {
@@ -402,13 +547,23 @@ function openEnhanceDialog(
402
547
  restored = false
403
548
  }
404
549
  }
405
-
406
- const baseMessage = error instanceof Error ? error.message : "Prompt enhancement failed."
550
+ const baseMessage = canceled
551
+ ? ENHANCEMENT_CANCELED_MESSAGE
552
+ : error instanceof Error
553
+ ? error.message
554
+ : "Prompt enhancement failed."
407
555
  const message = restored
408
- ? baseMessage
556
+ ? canceled
557
+ ? "Enhancement canceled. Original prompt restored."
558
+ : baseMessage
409
559
  : `${baseMessage} Original prompt could not be restored because the prompt changed. Please re-enter your prompt manually.`
410
- api.ui.toast({ variant: "error", title: TOAST_TITLE, message })
560
+ api.ui.toast({ variant: canceled && restored ? "info" : "error", title: TOAST_TITLE, message })
411
561
  } finally {
562
+ stopAnimation()
563
+ signal.removeEventListener("abort", onLifecycleAbort)
564
+ if (state.activeEnhancement?.controller === enhancementController) {
565
+ state.activeEnhancement = undefined
566
+ }
412
567
  state.enhancing = false
413
568
  }
414
569
  })()
@@ -422,6 +577,30 @@ function revertEnhancement(
422
577
  state: PluginState,
423
578
  signal: AbortSignal,
424
579
  ): void {
580
+ if (state.enhancing && state.activeEnhancement) {
581
+ void (async () => {
582
+ try {
583
+ const restored = await cancelActiveEnhancement(api, state, signal)
584
+ if (!restored) {
585
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Prompt changed while canceling." })
586
+ return
587
+ }
588
+
589
+ api.ui.toast({
590
+ variant: "info",
591
+ title: TOAST_TITLE,
592
+ message: "Enhancement canceled. Original prompt restored.",
593
+ duration: TOAST_DURATION_MS,
594
+ })
595
+ } catch (error) {
596
+ if (signal.aborted) return
597
+ const message = error instanceof Error ? error.message : "Cancel failed."
598
+ api.ui.toast({ variant: "error", title: TOAST_TITLE, message })
599
+ }
600
+ })()
601
+ return
602
+ }
603
+
425
604
  if (!state.lastOriginal) {
426
605
  api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "No enhancement to revert." })
427
606
  return
@@ -473,7 +652,7 @@ function revertEnhancement(
473
652
  variant: "success",
474
653
  title: TOAST_TITLE,
475
654
  message: "Reverted to original prompt.",
476
- duration: 3000,
655
+ duration: TOAST_DURATION_MS,
477
656
  })
478
657
  } catch (error) {
479
658
  if (signal.aborted) return
@@ -541,6 +720,8 @@ const tui: TuiPlugin = async (api, options) => {
541
720
  api.lifecycle.onDispose(() => {
542
721
  unregister()
543
722
  api.ui.dialog.clear()
723
+ state.activeEnhancement?.controller.abort(api.lifecycle.signal.reason)
724
+ state.activeEnhancement = undefined
544
725
  state.enhancing = false
545
726
  state.promptRef = undefined
546
727
  state.promptTarget = undefined