@mtayfur/opencode-prompt-enhancer 0.0.6 → 0.0.8

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 CHANGED
@@ -1,11 +1,20 @@
1
1
  # opencode-prompt-enhancer
2
2
 
3
- OpenCode TUI plugin that rewrites rough prompt drafts into stronger prompts with workspace context.
3
+ OpenCode TUI plugin that rewrites rough prompt drafts into clearer, stronger prompts.
4
4
 
5
- ## Package
5
+ ## What it does
6
6
 
7
- - npm: `opencode-prompt-enhancer`
8
- - GitHub: `mtayfur/opencode-prompt-enhancer`
7
+ - Rewrites rough prompt drafts into clearer, stronger prompts.
8
+ - Uses lightweight workspace context.
9
+ - Keeps the original intent and language, and does not read file contents.
10
+
11
+ ## Context used
12
+
13
+ The enhancer uses:
14
+
15
+ - the current working directory
16
+ - recent user prompts in the current session
17
+ - files changed in the current session
9
18
 
10
19
  ## Install
11
20
 
@@ -14,20 +23,20 @@ Add the package to OpenCode's `tui.json` plugin list:
14
23
  ```jsonc
15
24
  {
16
25
  "plugin": [
17
- "opencode-prompt-enhancer@latest"
26
+ "@mtayfur/opencode-prompt-enhancer@latest"
18
27
  ]
19
28
  }
20
29
  ```
21
30
 
22
- ## Use
31
+ OpenCode `>=1.3.14` is required.
23
32
 
24
- After restarting OpenCode, press `Ctrl+E` or run `/enhance`.
25
-
26
- The plugin opens a dialog, rewrites your draft with workspace context, and writes the enhanced prompt back into the current input.
27
-
28
- ## Context
33
+ ## Use
29
34
 
30
- The enhancer uses the current working directory, branch, recent user prompts, changed files, and active todos. It passes lightweight context only and does not read file contents.
35
+ 1. Open OpenCode in a workspace.
36
+ 2. Enter a rough prompt in the TUI prompt.
37
+ 3. Press `Ctrl+E` or run `/enhance`.
38
+ 4. Review the prefilled dialog, edit it if needed, and confirm.
39
+ 5. The enhanced prompt replaces the current input.
31
40
 
32
41
  ## Development
33
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mtayfur/opencode-prompt-enhancer",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "OpenCode plugin that rewrites rough drafts into stronger prompts.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -27,6 +27,7 @@
27
27
  "peerDependencies": {
28
28
  "@opencode-ai/plugin": "*",
29
29
  "@opencode-ai/sdk": "*",
30
+ "@opentui/core": "*",
30
31
  "@opentui/solid": "*",
31
32
  "solid-js": "*"
32
33
  },
@@ -0,0 +1,29 @@
1
+ export const ENHANCER_SYSTEM_PROMPT = `You are a prompt editor for OpenCode, an AI coding assistant.
2
+
3
+ Goal:
4
+ Clean up the user's draft only when it improves clarity. Preserve intent, scope, priorities, language, and tone. Always fix typos and awkward phrasing; otherwise return the draft unchanged when it is already clear.
5
+
6
+ Capabilities:
7
+ OpenCode can inspect the workspace, edit files, run commands, and verify changes. Respect the user's requested mode: if the draft asks for discussion, planning, or review, keep it that way.
8
+
9
+ Rewrite rules:
10
+ - Preserve the request form. Questions stay questions; bug-fix requests stay bug-fix requests; review requests stay review requests.
11
+ - Preserve the language of the draft. Do not translate.
12
+ - Preserve inline code, file paths, command strings, error messages, identifiers, and quoted phrases verbatim.
13
+ - Preserve explicit constraints, file names, commands, acceptance criteria, and user wording.
14
+ - Use workspace context only when it directly clarifies an ambiguous reference.
15
+ - Resolve vague references like "this" or "that bug" only when context makes them unambiguous.
16
+ - Do not invent details when context is ambiguous.
17
+ - Do not add background, motivation, implementation steps, acceptance criteria, or structure the draft did not imply.
18
+ - Match the draft's structure and tone. Keep prose as prose and lists as lists.
19
+
20
+ Hard constraints:
21
+ - Do not invent requirements, files, APIs, dependencies, bugs, or acceptance criteria.
22
+ - Do not broaden scope with extra features, refactors, tests, or docs.
23
+ - Do not inject assistant-style directives (e.g., "identify root cause", "verify the changed path", "follow local conventions").
24
+ - Do not ask follow-up questions.
25
+ - Do not mention context, tags, instructions, or the rewriting process.
26
+ - Do not output explanations, markdown fences, or commentary.
27
+
28
+ Output:
29
+ Return exactly one enhanced user prompt as plain text.`
@@ -9,48 +9,13 @@ import type {
9
9
  TuiRouteCurrent,
10
10
  TuiSlotPlugin,
11
11
  } from "@opencode-ai/plugin/tui"
12
+ import { ENHANCER_SYSTEM_PROMPT } from "./enhancer-system-prompt"
12
13
 
13
14
  const MAX_RECENT_MESSAGES = 3
14
15
  const MAX_CHANGED_FILES = 25
15
16
  const MAX_PROMPT_PREVIEW_LENGTH = 250
16
17
  const DIALOG_TITLE = "Enhance Prompt"
17
18
  const TOAST_TITLE = "Prompt enhancer"
18
- const TEMP_SESSION_TITLE = "Prompt Enhancer"
19
-
20
- function makeTempSessionTitle(): string {
21
- return `${TEMP_SESSION_TITLE} ${Math.random().toString(36).slice(2, 8)}`
22
- }
23
-
24
- const ENHANCER_SYSTEM_PROMPT = `You are a prompt editor for OpenCode, an AI coding assistant.
25
-
26
- Goal:
27
- Rewrite the user's draft into the strongest possible next prompt for OpenCode. Preserve the user's intent, scope, priorities, and language. Improve clarity, specificity, and execution readiness without changing the requested outcome.
28
-
29
- Capabilities:
30
- OpenCode can inspect the workspace, edit files, run commands, and verify changes. When the draft does not explicitly ask for discussion or planning only, rewrite it so OpenCode can act directly.
31
-
32
- Rewrite rules:
33
- - Preserve the request form. Questions stay questions; bug-fix requests stay bug-fix requests; review requests stay review requests.
34
- - Preserve the language of the draft. Do not translate.
35
- - Preserve inline code, file paths, command strings, error messages, identifiers, and quoted phrases verbatim when they matter.
36
- - Preserve explicit constraints, file names, commands, acceptance criteria, and user wording.
37
- - Use workspace context only when it directly clarifies or narrows the task.
38
- - Resolve vague references like "this", "that bug", or "the plugin" only when context makes them clear.
39
- - Do not invent details when context is ambiguous.
40
- - Expand vague verbs into concrete actions when it helps OpenCode act immediately, such as identify the root cause, apply the smallest correct fix, remove dead code, keep scope tight, preserve behavior, follow local conventions, and verify the changed path.
41
- - Keep the rewrite proportional to the draft. Do not add background, motivation, or steps the draft did not imply.
42
- - Match the draft's structure and tone. Keep prose as prose and lists as lists.
43
- - If the draft is already precise, make only minimal cleanup.
44
-
45
- Hard constraints:
46
- - Do not invent requirements, files, APIs, dependencies, bugs, or acceptance criteria.
47
- - Do not broaden scope with extra features, refactors, tests, or docs unless the draft implies them.
48
- - Do not ask follow-up questions.
49
- - Do not mention context, tags, instructions, or the rewriting process.
50
- - Do not output explanations, markdown fences, or commentary.
51
-
52
- Output:
53
- Return exactly one enhanced user prompt as plain text.`
54
19
 
55
20
  type ModelRef = {
56
21
  providerID: string
@@ -62,6 +27,8 @@ type PluginState = {
62
27
  enhancing: boolean
63
28
  promptRef?: TuiPromptRef
64
29
  promptTarget?: PromptTarget
30
+ lastOriginal?: TuiPromptInfo
31
+ lastEnhancedInput?: string
65
32
  }
66
33
 
67
34
  type PromptTarget =
@@ -85,37 +52,20 @@ function parseModelString(value: string | undefined): ModelRef | undefined {
85
52
  }
86
53
  }
87
54
 
88
- function getModelOverride(options: PluginOptions | undefined): ModelRef | undefined {
89
- const value = typeof options?.model === "string" ? options.model : undefined
90
- return parseModelString(value)
91
- }
92
-
93
55
  function isSessionRoute(route: TuiRouteCurrent): route is Extract<TuiRouteCurrent, { name: "session" }> {
94
56
  return route.name === "session"
95
57
  }
96
58
 
97
- function isUserMessage(message: Message): message is Extract<Message, { role: "user" }> {
98
- return message.role === "user"
99
- }
100
-
101
- function isVisibleTextPart(part: Part): part is TextPart {
102
- return part.type === "text" && !part.ignored
103
- }
104
-
105
59
  function extractVisibleText(parts: ReadonlyArray<Part>): string {
106
60
  return parts
107
- .filter(isVisibleTextPart)
61
+ .filter((part): part is TextPart => part.type === "text" && !part.ignored)
108
62
  .map((part) => part.text)
109
63
  .join("")
110
64
  .trim()
111
65
  }
112
66
 
113
- function truncate(value: string, maxLength: number): string {
114
- return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value
115
- }
116
-
117
67
  function resolveEnhancerModel(api: Api, options: PluginOptions | undefined): ModelRef | undefined {
118
- const override = getModelOverride(options)
68
+ const override = typeof options?.model === "string" ? parseModelString(options.model) : undefined
119
69
  if (override) return override
120
70
 
121
71
  return parseModelString(api.state.config.small_model || api.state.config.model)
@@ -148,25 +98,14 @@ function samePromptTarget(left: PromptTarget | undefined, right: PromptTarget |
148
98
  return false
149
99
  }
150
100
 
151
- function resolvePromptTarget(route: TuiRouteCurrent, workspaceID?: string): PromptTarget | undefined {
152
- if (route.name === "home") return { name: "home", workspaceID }
153
- if (isSessionRoute(route)) {
154
- return { name: "session", sessionID: route.params.sessionID }
155
- }
156
- return undefined
157
- }
158
-
159
- function isPromptTargetActive(route: TuiRouteCurrent, target: PromptTarget): boolean {
160
- if (target.name === "home") return route.name === "home"
161
- return isSessionRoute(route) && route.params.sessionID === target.sessionID
162
- }
163
-
164
- function currentPromptTarget(api: Api, state: PluginState): PromptTarget | undefined {
165
- return state.promptTarget ?? resolvePromptTarget(api.route.current)
166
- }
167
-
168
101
  function isPromptHandleActive(api: Api, state: PluginState, handle: PromptHandle): boolean {
169
- if (!isPromptTargetActive(api.route.current, handle.target)) return false
102
+ const route = api.route.current
103
+ const target = handle.target
104
+ if (target.name === "home") {
105
+ if (route.name !== "home") return false
106
+ } else if (!isSessionRoute(route) || route.params.sessionID !== target.sessionID) {
107
+ return false
108
+ }
170
109
  if (api.state.path.directory !== handle.directory) return false
171
110
 
172
111
  if (handle.ref) {
@@ -247,7 +186,7 @@ async function restorePrompt(
247
186
  return true
248
187
  }
249
188
 
250
- return writePrompt(api, state, handle, prompt.input, signal, prompt)
189
+ return false
251
190
  }
252
191
 
253
192
  function gatherContext(api: Api): string {
@@ -256,19 +195,24 @@ function gatherContext(api: Api): string {
256
195
  const dir = api.state.path.directory
257
196
  sections.push(`Working directory: ${dir}`)
258
197
 
198
+ const branch = api.state.vcs?.branch
199
+ if (branch) {
200
+ sections.push(`Current branch: ${branch}`)
201
+ }
202
+
259
203
  const route = api.route.current
260
204
  if (isSessionRoute(route)) {
261
205
  const sessionID = route.params.sessionID
262
206
  const messages = api.state.session.messages(sessionID)
263
207
 
264
- const userMessages = messages.filter(isUserMessage)
208
+ const userMessages = messages.filter((message): message is Extract<Message, { role: "user" }> => message.role === "user")
265
209
  const recent = userMessages.slice(-MAX_RECENT_MESSAGES).reverse()
266
210
  if (recent.length > 0) {
267
211
  const prompts: string[] = []
268
212
  for (const msg of recent) {
269
213
  const text = extractVisibleText(api.state.part(msg.id))
270
214
  if (text) {
271
- prompts.push(truncate(text, MAX_PROMPT_PREVIEW_LENGTH))
215
+ prompts.push(text.length > MAX_PROMPT_PREVIEW_LENGTH ? `${text.slice(0, MAX_PROMPT_PREVIEW_LENGTH)}...` : text)
272
216
  }
273
217
  }
274
218
  if (prompts.length > 0) {
@@ -287,19 +231,6 @@ function gatherContext(api: Api): string {
287
231
  return sections.join("\n\n")
288
232
  }
289
233
 
290
- function buildUserMessage(input: string, context: string): string {
291
- const sections = [
292
- "Rewrite the following user draft into a stronger prompt.",
293
- `User draft:\n${input}`,
294
- ]
295
-
296
- if (context) {
297
- sections.splice(1, 0, `Workspace context (use only if directly relevant):\n${context}`)
298
- }
299
-
300
- return sections.join("\n\n")
301
- }
302
-
303
234
  async function enhanceWithModel(
304
235
  api: Api,
305
236
  options: PluginOptions | undefined,
@@ -309,11 +240,18 @@ async function enhanceWithModel(
309
240
  const directory = api.state.path.directory
310
241
  const model = resolveEnhancerModel(api, options)
311
242
  const context = gatherContext(api)
243
+ const userMessage = context
244
+ ? [
245
+ "Rewrite the following user draft into a stronger prompt.",
246
+ `Workspace context (use only if directly relevant):\n${context}`,
247
+ `User draft:\n${input}`,
248
+ ].join("\n\n")
249
+ : ["Rewrite the following user draft into a stronger prompt.", `User draft:\n${input}`].join("\n\n")
312
250
 
313
251
  const created = await api.client.session.create(
314
252
  {
315
253
  directory,
316
- title: makeTempSessionTitle(),
254
+ title: `Prompt Enhancer ${Math.random().toString(36).slice(2, 8)}`,
317
255
  },
318
256
  { signal, throwOnError: true },
319
257
  )
@@ -331,7 +269,7 @@ async function enhanceWithModel(
331
269
  parts: [
332
270
  {
333
271
  type: "text",
334
- text: buildUserMessage(input, context),
272
+ text: userMessage,
335
273
  },
336
274
  ],
337
275
  },
@@ -366,7 +304,11 @@ function openEnhanceDialog(
366
304
 
367
305
  if (signal.aborted) return
368
306
 
369
- const target = currentPromptTarget(api, state)
307
+ const target = state.promptTarget ?? (api.route.current.name === "home"
308
+ ? { name: "home" as const }
309
+ : isSessionRoute(api.route.current)
310
+ ? { name: "session" as const, sessionID: api.route.current.params.sessionID }
311
+ : undefined)
370
312
  if (!target) {
371
313
  api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Enhancement only works from a prompt." })
372
314
  return
@@ -406,7 +348,7 @@ function openEnhanceDialog(
406
348
  variant: "info",
407
349
  title: TOAST_TITLE,
408
350
  message: "Enhancing prompt...",
409
- duration: 15_000,
351
+ duration: 8_000,
410
352
  })
411
353
 
412
354
  void (async () => {
@@ -434,6 +376,9 @@ function openEnhanceDialog(
434
376
  return
435
377
  }
436
378
 
379
+ state.lastOriginal = originalPrompt
380
+ state.lastEnhancedInput = enhanced
381
+
437
382
  api.ui.toast({
438
383
  variant: "success",
439
384
  title: "Prompt enhanced",
@@ -462,7 +407,7 @@ function openEnhanceDialog(
462
407
  const baseMessage = error instanceof Error ? error.message : "Prompt enhancement failed."
463
408
  const message = restored
464
409
  ? baseMessage
465
- : `${baseMessage} Original prompt could not be restored because the prompt changed.`
410
+ : `${baseMessage} Original prompt could not be restored because the prompt changed. Please re-enter your prompt manually.`
466
411
  api.ui.toast({ variant: "error", title: TOAST_TITLE, message })
467
412
  } finally {
468
413
  state.enhancing = false
@@ -473,6 +418,72 @@ function openEnhanceDialog(
473
418
  ))
474
419
  }
475
420
 
421
+ function revertEnhancement(
422
+ api: Api,
423
+ state: PluginState,
424
+ signal: AbortSignal,
425
+ ): void {
426
+ if (!state.lastOriginal) {
427
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "No enhancement to revert." })
428
+ return
429
+ }
430
+
431
+ const target = state.promptTarget
432
+ if (!target) {
433
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Revert only works from a prompt." })
434
+ return
435
+ }
436
+
437
+ const handle: PromptHandle = {
438
+ target,
439
+ directory: api.state.path.directory,
440
+ ref: state.promptRef,
441
+ }
442
+
443
+ if (!isPromptHandleActive(api, state, handle)) {
444
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Prompt changed since enhancement." })
445
+ return
446
+ }
447
+
448
+ const currentInput = handle.ref?.current.input ?? ""
449
+ if (currentInput !== state.lastEnhancedInput) {
450
+ api.ui.toast({
451
+ variant: "warning",
452
+ title: TOAST_TITLE,
453
+ message: "Prompt was manually changed after enhancement. Revert skipped.",
454
+ })
455
+ return
456
+ }
457
+
458
+ void (async () => {
459
+ try {
460
+ const wrote = await restorePrompt(api, state, handle, state.lastOriginal!, signal)
461
+ if (!wrote) {
462
+ api.ui.toast({
463
+ variant: "warning",
464
+ title: TOAST_TITLE,
465
+ message: "Prompt changed while reverting.",
466
+ })
467
+ return
468
+ }
469
+
470
+ state.lastOriginal = undefined
471
+ state.lastEnhancedInput = undefined
472
+
473
+ api.ui.toast({
474
+ variant: "success",
475
+ title: TOAST_TITLE,
476
+ message: "Reverted to original prompt.",
477
+ duration: 3000,
478
+ })
479
+ } catch (error) {
480
+ if (signal.aborted) return
481
+ const message = error instanceof Error ? error.message : "Revert failed."
482
+ api.ui.toast({ variant: "error", title: TOAST_TITLE, message })
483
+ }
484
+ })()
485
+ }
486
+
476
487
  const tui: TuiPlugin = async (api, options) => {
477
488
  const state: PluginState = { enhancing: false }
478
489
 
@@ -520,19 +531,36 @@ const tui: TuiPlugin = async (api, options) => {
520
531
  openEnhanceDialog(api, options, state, api.lifecycle.signal)
521
532
  },
522
533
  },
534
+ {
535
+ title: "Revert Enhanced Prompt",
536
+ value: "prompt-enhancer.revert",
537
+ description: "Revert last prompt enhancement (Ctrl+Shift+E)",
538
+ category: "Prompt",
539
+ keybind: "ctrl+shift+e",
540
+ slash: {
541
+ name: "revert-enhance",
542
+ aliases: ["revert-enhancement"],
543
+ },
544
+ onSelect: () => {
545
+ revertEnhancement(api, state, api.lifecycle.signal)
546
+ },
547
+ },
523
548
  ])
524
549
 
525
550
  api.lifecycle.onDispose(() => {
526
551
  unregister()
552
+ api.ui.dialog.clear()
527
553
  state.enhancing = false
528
554
  state.promptRef = undefined
529
555
  state.promptTarget = undefined
556
+ state.lastOriginal = undefined
557
+ state.lastEnhancedInput = undefined
530
558
  })
531
559
  }
532
560
 
533
- const plugin: TuiPluginModule & { id: string } = {
561
+ const plugin = {
534
562
  id: "prompt-enhancer",
535
563
  tui,
536
- }
564
+ } satisfies TuiPluginModule & { id: string }
537
565
 
538
566
  export default plugin