@mtayfur/opencode-prompt-enhancer 0.0.9 → 0.0.11

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.9",
3
+ "version": "0.0.11",
4
4
  "description": "OpenCode plugin that rewrites rough drafts into stronger prompts.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -14,6 +14,17 @@ 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 PROGRESS_TOAST_DURATION_MS = 2_000
20
+ const RESULT_TOAST_DURATION_MS = 2_000
21
+ const ENHANCEMENT_CANCELED_MESSAGE = "Prompt enhancement canceled."
22
+ const ENHANCEMENT_ANIMATION_FRAMES = [
23
+ "Enhancing prompt",
24
+ "Enhancing prompt.",
25
+ "Enhancing prompt..",
26
+ "Enhancing prompt...",
27
+ ] as const
17
28
  const DIALOG_TITLE = "Enhance Prompt"
18
29
  const TOAST_TITLE = "Prompt enhancer"
19
30
 
@@ -23,8 +34,13 @@ type ModelRef = {
23
34
  }
24
35
 
25
36
  type Api = Parameters<TuiPlugin>[0]
37
+ type ActiveEnhancement = {
38
+ controller: AbortController
39
+ }
40
+
26
41
  type PluginState = {
27
42
  enhancing: boolean
43
+ activeEnhancement?: ActiveEnhancement
28
44
  promptRef?: TuiPromptRef
29
45
  promptTarget?: PromptTarget
30
46
  lastOriginal?: TuiPromptInfo
@@ -87,6 +103,39 @@ function nextPromptInfo(prompt: TuiPromptInfo, input: string): TuiPromptInfo {
87
103
  }
88
104
  }
89
105
 
106
+ function errorFromReason(reason: unknown, fallbackMessage: string): Error {
107
+ return reason instanceof Error ? reason : new Error(fallbackMessage)
108
+ }
109
+
110
+ async function withRequestTimeout<T>(
111
+ signal: AbortSignal,
112
+ timeoutMs: number,
113
+ run: (signal: AbortSignal) => Promise<T>,
114
+ ): Promise<T> {
115
+ const controller = new AbortController()
116
+ const timeoutMessage = `Prompt enhancement timed out after ${Math.floor(timeoutMs / 1000)} seconds.`
117
+ const onAbort = () => controller.abort(signal.reason)
118
+ const timeout = setTimeout(() => controller.abort(new Error(timeoutMessage)), timeoutMs)
119
+
120
+ if (signal.aborted) {
121
+ controller.abort(signal.reason)
122
+ } else {
123
+ signal.addEventListener("abort", onAbort, { once: true })
124
+ }
125
+
126
+ try {
127
+ return await run(controller.signal)
128
+ } catch (error) {
129
+ if (controller.signal.aborted && !signal.aborted) {
130
+ throw errorFromReason(controller.signal.reason, timeoutMessage)
131
+ }
132
+ throw error
133
+ } finally {
134
+ clearTimeout(timeout)
135
+ signal.removeEventListener("abort", onAbort)
136
+ }
137
+ }
138
+
90
139
  function samePromptTarget(left: PromptTarget | undefined, right: PromptTarget | undefined): boolean {
91
140
  if (!left || !right || left.name !== right.name) return false
92
141
  if (left.name === "home" && right.name === "home") {
@@ -138,6 +187,7 @@ async function clearPrompt(api: Api, state: PluginState, handle: PromptHandle, s
138
187
  const promptRef = handle.ref
139
188
  if (promptRef) {
140
189
  promptRef.set(nextPromptInfo(template ?? promptRef.current, ""))
190
+ promptRef.blur()
141
191
  return true
142
192
  }
143
193
 
@@ -186,7 +236,43 @@ async function restorePrompt(
186
236
  return true
187
237
  }
188
238
 
189
- return false
239
+ const requestOptions = { signal, throwOnError: true } as const
240
+ await api.client.tui.clearPrompt({ directory: handle.directory }, requestOptions)
241
+ if (prompt.input) {
242
+ await api.client.tui.appendPrompt({ directory: handle.directory, text: prompt.input }, requestOptions)
243
+ }
244
+ return true
245
+ }
246
+
247
+ function startEnhancementAnimation(
248
+ api: Api,
249
+ state: PluginState,
250
+ handle: PromptHandle,
251
+ template?: TuiPromptInfo,
252
+ ): () => void {
253
+ const promptRef = handle.ref
254
+ if (!promptRef) return () => {}
255
+
256
+ let frame = 0
257
+ let stopped = false
258
+
259
+ const render = () => {
260
+ if (stopped) return
261
+ if (!isPromptHandleActive(api, state, handle)) {
262
+ stopped = true
263
+ return
264
+ }
265
+
266
+ promptRef.set(nextPromptInfo(template ?? promptRef.current, ENHANCEMENT_ANIMATION_FRAMES[frame]))
267
+ frame = (frame + 1) % ENHANCEMENT_ANIMATION_FRAMES.length
268
+ }
269
+
270
+ render()
271
+ const interval = setInterval(render, ENHANCEMENT_ANIMATION_INTERVAL_MS)
272
+ return () => {
273
+ stopped = true
274
+ clearInterval(interval)
275
+ }
190
276
  }
191
277
 
192
278
  function gatherContext(api: Api): string {
@@ -259,20 +345,24 @@ async function enhanceWithModel(
259
345
  if (!tempSessionID) throw new Error("Failed to start prompt enhancer.")
260
346
 
261
347
  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 },
348
+ const response = await withRequestTimeout(
349
+ signal,
350
+ ENHANCEMENT_TIMEOUT_MS,
351
+ (requestSignal) => api.client.session.prompt(
352
+ {
353
+ sessionID: tempSessionID,
354
+ directory,
355
+ model,
356
+ system: ENHANCER_SYSTEM_PROMPT,
357
+ parts: [
358
+ {
359
+ type: "text",
360
+ text: userMessage,
361
+ },
362
+ ],
363
+ },
364
+ { signal: requestSignal, throwOnError: true },
365
+ ),
276
366
  )
277
367
 
278
368
  const parts = response.data?.parts
@@ -282,11 +372,9 @@ async function enhanceWithModel(
282
372
  if (!enhanced) throw new Error("Enhancer returned no text.")
283
373
  return enhanced
284
374
  } finally {
285
- try {
286
- await api.client.session.delete({ sessionID: tempSessionID, directory })
287
- } catch {
375
+ void api.client.session.delete({ sessionID: tempSessionID, directory }).catch(() => {
288
376
  // Best-effort cleanup for the temporary enhancement session.
289
- }
377
+ })
290
378
  }
291
379
  }
292
380
 
@@ -347,10 +435,20 @@ function openEnhanceDialog(
347
435
  variant: "info",
348
436
  title: TOAST_TITLE,
349
437
  message: "Enhancing prompt...",
350
- duration: 8_000,
438
+ duration: PROGRESS_TOAST_DURATION_MS,
351
439
  })
352
440
 
441
+ const enhancementController = new AbortController()
442
+ const onLifecycleAbort = () => enhancementController.abort(signal.reason)
443
+ state.activeEnhancement = { controller: enhancementController }
444
+ if (signal.aborted) {
445
+ enhancementController.abort(signal.reason)
446
+ } else {
447
+ signal.addEventListener("abort", onLifecycleAbort, { once: true })
448
+ }
449
+
353
450
  void (async () => {
451
+ let stopAnimation = () => {}
354
452
  try {
355
453
  const cleared = await clearPrompt(api, state, handle, signal, originalPrompt)
356
454
  if (!cleared) {
@@ -362,8 +460,15 @@ function openEnhanceDialog(
362
460
  return
363
461
  }
364
462
 
365
- const enhanced = await enhanceWithModel(api, options, input, signal)
463
+ stopAnimation = startEnhancementAnimation(api, state, handle, originalPrompt)
464
+
465
+ const enhanced = await enhanceWithModel(api, options, input, enhancementController.signal)
366
466
  if (signal.aborted) return
467
+ if (enhancementController.signal.aborted) {
468
+ throw errorFromReason(enhancementController.signal.reason, ENHANCEMENT_CANCELED_MESSAGE)
469
+ }
470
+
471
+ stopAnimation()
367
472
 
368
473
  const wrote = await writePrompt(api, state, handle, enhanced, signal, originalPrompt)
369
474
  if (!wrote) {
@@ -374,6 +479,9 @@ function openEnhanceDialog(
374
479
  })
375
480
  return
376
481
  }
482
+ if (enhancementController.signal.aborted) {
483
+ throw errorFromReason(enhancementController.signal.reason, ENHANCEMENT_CANCELED_MESSAGE)
484
+ }
377
485
 
378
486
  state.lastOriginal = originalPrompt
379
487
  state.lastEnhancedInput = enhanced
@@ -382,11 +490,13 @@ function openEnhanceDialog(
382
490
  variant: "success",
383
491
  title: "Prompt enhanced",
384
492
  message: "Enhanced prompt added to input.",
385
- duration: 3000,
493
+ duration: RESULT_TOAST_DURATION_MS,
386
494
  })
387
495
  } catch (error) {
388
496
  if (signal.aborted) return
389
497
 
498
+ stopAnimation()
499
+
390
500
  let restored = true
391
501
  if (originalPrompt) {
392
502
  try {
@@ -403,12 +513,24 @@ function openEnhanceDialog(
403
513
  }
404
514
  }
405
515
 
406
- const baseMessage = error instanceof Error ? error.message : "Prompt enhancement failed."
516
+ const canceled = enhancementController.signal.aborted && !signal.aborted
517
+ const baseMessage = canceled
518
+ ? ENHANCEMENT_CANCELED_MESSAGE
519
+ : error instanceof Error
520
+ ? error.message
521
+ : "Prompt enhancement failed."
407
522
  const message = restored
408
- ? baseMessage
523
+ ? canceled
524
+ ? "Enhancement canceled. Original prompt restored."
525
+ : baseMessage
409
526
  : `${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 })
527
+ api.ui.toast({ variant: canceled && restored ? "info" : "error", title: TOAST_TITLE, message })
411
528
  } finally {
529
+ stopAnimation()
530
+ signal.removeEventListener("abort", onLifecycleAbort)
531
+ if (state.activeEnhancement?.controller === enhancementController) {
532
+ state.activeEnhancement = undefined
533
+ }
412
534
  state.enhancing = false
413
535
  }
414
536
  })()
@@ -422,6 +544,11 @@ function revertEnhancement(
422
544
  state: PluginState,
423
545
  signal: AbortSignal,
424
546
  ): void {
547
+ if (state.enhancing && state.activeEnhancement) {
548
+ state.activeEnhancement.controller.abort(new Error(ENHANCEMENT_CANCELED_MESSAGE))
549
+ return
550
+ }
551
+
425
552
  if (!state.lastOriginal) {
426
553
  api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "No enhancement to revert." })
427
554
  return
@@ -473,7 +600,7 @@ function revertEnhancement(
473
600
  variant: "success",
474
601
  title: TOAST_TITLE,
475
602
  message: "Reverted to original prompt.",
476
- duration: 3000,
603
+ duration: RESULT_TOAST_DURATION_MS,
477
604
  })
478
605
  } catch (error) {
479
606
  if (signal.aborted) return
@@ -522,10 +649,6 @@ const tui: TuiPlugin = async (api, options) => {
522
649
  category: "Prompt",
523
650
  keybind: "ctrl+e",
524
651
  suggested: true,
525
- slash: {
526
- name: "enhance",
527
- aliases: ["enhance-prompt"],
528
- },
529
652
  onSelect: () => {
530
653
  openEnhanceDialog(api, options, state, api.lifecycle.signal)
531
654
  },
@@ -536,10 +659,6 @@ const tui: TuiPlugin = async (api, options) => {
536
659
  description: "Revert last prompt enhancement (Ctrl+Shift+E)",
537
660
  category: "Prompt",
538
661
  keybind: "ctrl+shift+e",
539
- slash: {
540
- name: "revert-enhance",
541
- aliases: ["revert-enhancement"],
542
- },
543
662
  onSelect: () => {
544
663
  revertEnhancement(api, state, api.lifecycle.signal)
545
664
  },
@@ -549,6 +668,8 @@ const tui: TuiPlugin = async (api, options) => {
549
668
  api.lifecycle.onDispose(() => {
550
669
  unregister()
551
670
  api.ui.dialog.clear()
671
+ state.activeEnhancement?.controller.abort(api.lifecycle.signal.reason)
672
+ state.activeEnhancement = undefined
552
673
  state.enhancing = false
553
674
  state.promptRef = undefined
554
675
  state.promptTarget = undefined