@mtayfur/opencode-prompt-enhancer 0.0.5 → 0.0.7

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.5",
3
+ "version": "0.0.7",
4
4
  "description": "OpenCode plugin that rewrites rough drafts into stronger prompts.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,30 @@
1
+ export const ENHANCER_SYSTEM_PROMPT = `You are a prompt editor for OpenCode, an AI coding assistant.
2
+
3
+ Goal:
4
+ 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.
5
+
6
+ Capabilities:
7
+ 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.
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 when they matter.
13
+ - Preserve explicit constraints, file names, commands, acceptance criteria, and user wording.
14
+ - Use workspace context only when it directly clarifies or narrows the task.
15
+ - Resolve vague references like "this", "that bug", or "the plugin" only when context makes them clear.
16
+ - Do not invent details when context is ambiguous.
17
+ - 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.
18
+ - Keep the rewrite proportional to the draft. Do not add background, motivation, or steps the draft did not imply.
19
+ - Match the draft's structure and tone. Keep prose as prose and lists as lists.
20
+ - If the draft is already precise, make only minimal cleanup.
21
+
22
+ Hard constraints:
23
+ - Do not invent requirements, files, APIs, dependencies, bugs, or acceptance criteria.
24
+ - Do not broaden scope with extra features, refactors, tests, or docs unless the draft implies them.
25
+ - Do not ask follow-up questions.
26
+ - Do not mention context, tags, instructions, or the rewriting process.
27
+ - Do not output explanations, markdown fences, or commentary.
28
+
29
+ Output:
30
+ Return exactly one enhanced user prompt as plain text.`
@@ -7,54 +7,15 @@ import type {
7
7
  TuiPromptInfo,
8
8
  TuiPromptRef,
9
9
  TuiRouteCurrent,
10
- TuiSidebarFileItem,
11
- TuiSidebarTodoItem,
12
10
  TuiSlotPlugin,
13
11
  } from "@opencode-ai/plugin/tui"
12
+ import { ENHANCER_SYSTEM_PROMPT } from "./enhancer-system-prompt"
14
13
 
15
- const MAX_RECENT_MESSAGES = 6
16
- const MAX_CHANGED_FILES = 30
17
- const MAX_PROMPT_PREVIEW_LENGTH = 300
18
- const MAX_TODOS = 10
14
+ const MAX_RECENT_MESSAGES = 3
15
+ const MAX_CHANGED_FILES = 25
16
+ const MAX_PROMPT_PREVIEW_LENGTH = 250
19
17
  const DIALOG_TITLE = "Enhance Prompt"
20
18
  const TOAST_TITLE = "Prompt enhancer"
21
- const TEMP_SESSION_TITLE = "Prompt Enhancer"
22
- const HOME_PROMPT_PLACEHOLDERS = {
23
- normal: ["Fix a TODO in the codebase", "What is the tech stack of this project?", "Fix broken tests"],
24
- shell: ["ls -la", "git status", "pwd"],
25
- }
26
-
27
- const ENHANCER_SYSTEM_PROMPT = `You rewrite rough user drafts into strong prompts for OpenCode, an AI coding assistant.
28
-
29
- Goal:
30
- Turn the user's draft into the best possible next prompt for OpenCode. Preserve the user's intent, scope, and priorities exactly. Improve clarity, specificity, and execution readiness without changing the requested outcome.
31
-
32
- OpenCode context:
33
- OpenCode can inspect the workspace, read and edit files, run commands, and verify changes. Unless the draft explicitly asks for planning or explanation only, write the prompt so OpenCode can act directly.
34
-
35
- Rules:
36
- - Preserve the original request type.
37
- - If the draft is a question, return a better question.
38
- - If the draft is a bug fix, return a better bug-fix request.
39
- - If the draft is a review, return a better review request.
40
- - Preserve explicit constraints, file names, commands, error messages, acceptance criteria, and user wording when they matter.
41
- - Use workspace context only when it clearly helps disambiguate or narrow the task.
42
- - Reference specific files, paths, branches, recent prompts, changed files, or todos only when they are directly relevant.
43
- - Resolve vague references like "this", "that bug", or "the plugin" from context when clear.
44
- - If context does not clearly resolve a reference, do not invent details.
45
- - Expand vague verbs into concrete actions when helpful, such as: identify root cause, apply the smallest correct fix, remove dead code, keep scope tight, preserve behavior, follow local conventions, and verify the changed path.
46
- - Prefer wording that helps OpenCode start work immediately.
47
- - If the draft is already precise, return it with minimal cleanup.
48
-
49
- Do not:
50
- - Do not invent requirements, files, APIs, dependencies, bugs, or acceptance criteria.
51
- - Do not broaden scope with extra features, refactors, tests, or docs unless the draft implies them.
52
- - Do not ask follow-up questions inside the rewritten prompt.
53
- - Do not mention the existence of context, tags, or these instructions.
54
- - Do not output explanations, markdown fences, or commentary.
55
-
56
- Output:
57
- Return exactly one enhanced user prompt as plain text.`
58
19
 
59
20
  type ModelRef = {
60
21
  providerID: string
@@ -89,41 +50,20 @@ function parseModelString(value: string | undefined): ModelRef | undefined {
89
50
  }
90
51
  }
91
52
 
92
- function getModelOverride(options: PluginOptions | undefined): ModelRef | undefined {
93
- const value = typeof options?.model === "string" ? options.model : undefined
94
- return parseModelString(value)
95
- }
96
-
97
53
  function isSessionRoute(route: TuiRouteCurrent): route is Extract<TuiRouteCurrent, { name: "session" }> {
98
54
  return route.name === "session"
99
55
  }
100
56
 
101
- function isUserMessage(message: Message): message is Extract<Message, { role: "user" }> {
102
- return message.role === "user"
103
- }
104
-
105
- function isVisibleTextPart(part: Part): part is TextPart {
106
- return part.type === "text" && !part.ignored
107
- }
108
-
109
57
  function extractVisibleText(parts: ReadonlyArray<Part>): string {
110
58
  return parts
111
- .filter(isVisibleTextPart)
59
+ .filter((part): part is TextPart => part.type === "text" && !part.ignored)
112
60
  .map((part) => part.text)
113
61
  .join("")
114
62
  .trim()
115
63
  }
116
64
 
117
- function truncate(value: string, maxLength: number): string {
118
- return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value
119
- }
120
-
121
- function formatTodo(todo: TuiSidebarTodoItem): string {
122
- return ` [${todo.status || "?"}] ${todo.content || "untitled"}`
123
- }
124
-
125
65
  function resolveEnhancerModel(api: Api, options: PluginOptions | undefined): ModelRef | undefined {
126
- const override = getModelOverride(options)
66
+ const override = typeof options?.model === "string" ? parseModelString(options.model) : undefined
127
67
  if (override) return override
128
68
 
129
69
  return parseModelString(api.state.config.small_model || api.state.config.model)
@@ -156,25 +96,14 @@ function samePromptTarget(left: PromptTarget | undefined, right: PromptTarget |
156
96
  return false
157
97
  }
158
98
 
159
- function resolvePromptTarget(route: TuiRouteCurrent, workspaceID?: string): PromptTarget | undefined {
160
- if (route.name === "home") return { name: "home", workspaceID }
161
- if (isSessionRoute(route)) {
162
- return { name: "session", sessionID: route.params.sessionID }
163
- }
164
- return undefined
165
- }
166
-
167
- function isPromptTargetActive(route: TuiRouteCurrent, target: PromptTarget): boolean {
168
- if (target.name === "home") return route.name === "home"
169
- return isSessionRoute(route) && route.params.sessionID === target.sessionID
170
- }
171
-
172
- function currentPromptTarget(api: Api, state: PluginState): PromptTarget | undefined {
173
- return state.promptTarget ?? resolvePromptTarget(api.route.current)
174
- }
175
-
176
99
  function isPromptHandleActive(api: Api, state: PluginState, handle: PromptHandle): boolean {
177
- if (!isPromptTargetActive(api.route.current, handle.target)) return false
100
+ const route = api.route.current
101
+ const target = handle.target
102
+ if (target.name === "home") {
103
+ if (route.name !== "home") return false
104
+ } else if (!isSessionRoute(route) || route.params.sessionID !== target.sessionID) {
105
+ return false
106
+ }
178
107
  if (api.state.path.directory !== handle.directory) return false
179
108
 
180
109
  if (handle.ref) {
@@ -255,65 +184,41 @@ async function restorePrompt(
255
184
  return true
256
185
  }
257
186
 
258
- return writePrompt(api, state, handle, prompt.input, signal, prompt)
187
+ return false
259
188
  }
260
189
 
261
190
  function gatherContext(api: Api): string {
262
191
  const sections: string[] = []
263
192
 
264
193
  const dir = api.state.path.directory
265
- const branch = api.state.vcs?.branch
266
- sections.push(`Working directory: ${dir}${branch ? ` (branch: ${branch})` : ""}`)
194
+ sections.push(`Working directory: ${dir}`)
267
195
 
268
196
  const route = api.route.current
269
197
  if (isSessionRoute(route)) {
270
198
  const sessionID = route.params.sessionID
271
199
  const messages = api.state.session.messages(sessionID)
272
200
 
273
- const userMessages = messages.filter(isUserMessage)
274
- const recent = userMessages.slice(-MAX_RECENT_MESSAGES)
201
+ const userMessages = messages.filter((message): message is Extract<Message, { role: "user" }> => message.role === "user")
202
+ const recent = userMessages.slice(-MAX_RECENT_MESSAGES).reverse()
275
203
  if (recent.length > 0) {
276
204
  const prompts: string[] = []
277
205
  for (const msg of recent) {
278
206
  const text = extractVisibleText(api.state.part(msg.id))
279
207
  if (text) {
280
- prompts.push(truncate(text, MAX_PROMPT_PREVIEW_LENGTH))
208
+ prompts.push(text.length > MAX_PROMPT_PREVIEW_LENGTH ? `${text.slice(0, MAX_PROMPT_PREVIEW_LENGTH)}...` : text)
281
209
  }
282
210
  }
283
211
  if (prompts.length > 0) {
284
- sections.push(`Recent user prompts in this session (oldest first):\n${prompts.map((p, i) => `${i + 1}. ${p}`).join("\n")}`)
212
+ sections.push(`Recent user prompts in this session (newest first):\n${prompts.map((p, i) => `${i + 1}. ${p}`).join("\n")}`)
285
213
  }
286
214
  }
287
215
 
288
216
  const diff = api.state.session.diff(sessionID)
289
217
  if (diff.length > 0) {
290
- const files = diff.slice(0, MAX_CHANGED_FILES).map((f: TuiSidebarFileItem) =>
291
- ` ${f.file} (+${f.additions} -${f.deletions})`
292
- )
293
- const label = diff.length > MAX_CHANGED_FILES
294
- ? `Files changed in session (showing ${MAX_CHANGED_FILES} of ${diff.length}):`
295
- : `Files changed in session:`
296
- sections.push(`${label}\n${files.join("\n")}`)
218
+ const files = diff.slice(0, MAX_CHANGED_FILES).map((f) => ` ${f.file}`)
219
+ sections.push(`Files changed in session:\n${files.join("\n")}`)
297
220
  }
298
221
 
299
- const todos = api.state.session.todo(sessionID)
300
- if (todos.length > 0) {
301
- const todoLines = todos.slice(0, MAX_TODOS).map(formatTodo)
302
- sections.push(`Active todos:\n${todoLines.join("\n")}`)
303
- }
304
- }
305
-
306
- return sections.join("\n\n")
307
- }
308
-
309
- function buildUserMessage(input: string, context: string): string {
310
- const sections = [
311
- "Rewrite the following user draft into a stronger prompt.",
312
- `User draft:\n${input}`,
313
- ]
314
-
315
- if (context) {
316
- sections.splice(1, 0, `Workspace context (use only if directly relevant):\n${context}`)
317
222
  }
318
223
 
319
224
  return sections.join("\n\n")
@@ -328,17 +233,24 @@ async function enhanceWithModel(
328
233
  const directory = api.state.path.directory
329
234
  const model = resolveEnhancerModel(api, options)
330
235
  const context = gatherContext(api)
236
+ const userMessage = context
237
+ ? [
238
+ "Rewrite the following user draft into a stronger prompt.",
239
+ `Workspace context (use only if directly relevant):\n${context}`,
240
+ `User draft:\n${input}`,
241
+ ].join("\n\n")
242
+ : ["Rewrite the following user draft into a stronger prompt.", `User draft:\n${input}`].join("\n\n")
331
243
 
332
244
  const created = await api.client.session.create(
333
245
  {
334
246
  directory,
335
- title: TEMP_SESSION_TITLE,
247
+ title: `Prompt Enhancer ${Math.random().toString(36).slice(2, 8)}`,
336
248
  },
337
249
  { signal, throwOnError: true },
338
250
  )
339
251
 
340
252
  const tempSessionID = created.data?.id
341
- if (!tempSessionID) throw new Error("Prompt enhancer session creation failed.")
253
+ if (!tempSessionID) throw new Error("Failed to start prompt enhancer.")
342
254
 
343
255
  try {
344
256
  const response = await api.client.session.prompt(
@@ -350,7 +262,7 @@ async function enhanceWithModel(
350
262
  parts: [
351
263
  {
352
264
  type: "text",
353
- text: buildUserMessage(input, context),
265
+ text: userMessage,
354
266
  },
355
267
  ],
356
268
  },
@@ -358,10 +270,10 @@ async function enhanceWithModel(
358
270
  )
359
271
 
360
272
  const parts = response.data?.parts
361
- if (!parts) throw new Error("Enhancer model returned no response.")
273
+ if (!parts) throw new Error("Enhancer returned no response.")
362
274
 
363
275
  const enhanced = extractVisibleText(parts)
364
- if (!enhanced) throw new Error("Enhancer model returned no text.")
276
+ if (!enhanced) throw new Error("Enhancer returned no text.")
365
277
  return enhanced
366
278
  } finally {
367
279
  try {
@@ -379,15 +291,19 @@ function openEnhanceDialog(
379
291
  signal: AbortSignal,
380
292
  ): void {
381
293
  if (state.enhancing) {
382
- api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Enhancement already in progress." })
294
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Enhancement in progress." })
383
295
  return
384
296
  }
385
297
 
386
298
  if (signal.aborted) return
387
299
 
388
- const target = currentPromptTarget(api, state)
300
+ const target = state.promptTarget ?? (api.route.current.name === "home"
301
+ ? { name: "home" as const }
302
+ : isSessionRoute(api.route.current)
303
+ ? { name: "session" as const, sessionID: api.route.current.params.sessionID }
304
+ : undefined)
389
305
  if (!target) {
390
- api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Prompt enhancement is only available from a prompt view." })
306
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Enhancement only works from a prompt." })
391
307
  return
392
308
  }
393
309
 
@@ -402,19 +318,19 @@ function openEnhanceDialog(
402
318
  api.ui.dialog.replace(() => (
403
319
  <api.ui.DialogPrompt
404
320
  title={DIALOG_TITLE}
405
- placeholder="Describe what you want to do..."
321
+ placeholder="Describe the task..."
406
322
  value={initialValue}
407
323
  onCancel={() => api.ui.dialog.clear()}
408
324
  onConfirm={(value) => {
409
325
  const input = value.trim()
410
326
  if (!input) {
411
- api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Prompt is empty." })
327
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Enter a prompt first." })
412
328
  api.ui.dialog.clear()
413
329
  return
414
330
  }
415
331
 
416
332
  if (!isPromptHandleActive(api, state, handle)) {
417
- api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Prompt changed while the dialog was open." })
333
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Prompt changed while dialog was open." })
418
334
  api.ui.dialog.clear()
419
335
  return
420
336
  }
@@ -448,7 +364,7 @@ function openEnhanceDialog(
448
364
  api.ui.toast({
449
365
  variant: "warning",
450
366
  title: TOAST_TITLE,
451
- message: "Enhanced prompt is ready, but the original prompt is no longer active.",
367
+ message: "Enhanced prompt is ready, but that prompt is no longer active.",
452
368
  })
453
369
  return
454
370
  }
@@ -456,7 +372,7 @@ function openEnhanceDialog(
456
372
  api.ui.toast({
457
373
  variant: "success",
458
374
  title: "Prompt enhanced",
459
- message: "Enhanced prompt written to input.",
375
+ message: "Enhanced prompt added to input.",
460
376
  duration: 3000,
461
377
  })
462
378
  } catch (error) {
@@ -478,10 +394,10 @@ function openEnhanceDialog(
478
394
  }
479
395
  }
480
396
 
481
- const baseMessage = error instanceof Error ? error.message : "Model enhancement failed."
397
+ const baseMessage = error instanceof Error ? error.message : "Prompt enhancement failed."
482
398
  const message = restored
483
399
  ? baseMessage
484
- : `${baseMessage} Original prompt could not be restored because the active prompt changed.`
400
+ : `${baseMessage} Original prompt could not be restored because the prompt changed.`
485
401
  api.ui.toast({ variant: "error", title: TOAST_TITLE, message })
486
402
  } finally {
487
403
  state.enhancing = false
@@ -503,7 +419,6 @@ const tui: TuiPlugin = async (api, options) => {
503
419
  ref={(ref) => bindPromptRef(state, { name: "home", workspaceID: props.workspace_id }, props.ref, ref)}
504
420
  workspaceID={props.workspace_id}
505
421
  right={<api.ui.Slot name="home_prompt_right" workspace_id={props.workspace_id} />}
506
- placeholders={HOME_PROMPT_PLACEHOLDERS}
507
422
  />
508
423
  )
509
424
  },
@@ -528,7 +443,7 @@ const tui: TuiPlugin = async (api, options) => {
528
443
  {
529
444
  title: DIALOG_TITLE,
530
445
  value: "prompt-enhancer.enhance",
531
- description: "Enhance prompt with project context (Ctrl+E)",
446
+ description: "Enhance current prompt (Ctrl+E)",
532
447
  category: "Prompt",
533
448
  keybind: "ctrl+e",
534
449
  suggested: true,