@mtayfur/opencode-prompt-enhancer 0.0.6 → 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 +21 -12
- package/package.json +1 -1
- package/plugins/enhancer-system-prompt.ts +30 -0
- package/plugins/prompt-enhancer.tsx +27 -92
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
|
|
3
|
+
OpenCode TUI plugin that rewrites rough prompt drafts into clearer, stronger prompts.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What it does
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
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
|
-
|
|
31
|
+
OpenCode `>=1.3.14` is required.
|
|
23
32
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
@@ -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.`
|
|
@@ -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
|
|
@@ -85,37 +50,20 @@ function parseModelString(value: string | undefined): ModelRef | undefined {
|
|
|
85
50
|
}
|
|
86
51
|
}
|
|
87
52
|
|
|
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
53
|
function isSessionRoute(route: TuiRouteCurrent): route is Extract<TuiRouteCurrent, { name: "session" }> {
|
|
94
54
|
return route.name === "session"
|
|
95
55
|
}
|
|
96
56
|
|
|
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
57
|
function extractVisibleText(parts: ReadonlyArray<Part>): string {
|
|
106
58
|
return parts
|
|
107
|
-
.filter(
|
|
59
|
+
.filter((part): part is TextPart => part.type === "text" && !part.ignored)
|
|
108
60
|
.map((part) => part.text)
|
|
109
61
|
.join("")
|
|
110
62
|
.trim()
|
|
111
63
|
}
|
|
112
64
|
|
|
113
|
-
function truncate(value: string, maxLength: number): string {
|
|
114
|
-
return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value
|
|
115
|
-
}
|
|
116
|
-
|
|
117
65
|
function resolveEnhancerModel(api: Api, options: PluginOptions | undefined): ModelRef | undefined {
|
|
118
|
-
const override =
|
|
66
|
+
const override = typeof options?.model === "string" ? parseModelString(options.model) : undefined
|
|
119
67
|
if (override) return override
|
|
120
68
|
|
|
121
69
|
return parseModelString(api.state.config.small_model || api.state.config.model)
|
|
@@ -148,25 +96,14 @@ function samePromptTarget(left: PromptTarget | undefined, right: PromptTarget |
|
|
|
148
96
|
return false
|
|
149
97
|
}
|
|
150
98
|
|
|
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
99
|
function isPromptHandleActive(api: Api, state: PluginState, handle: PromptHandle): boolean {
|
|
169
|
-
|
|
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
|
+
}
|
|
170
107
|
if (api.state.path.directory !== handle.directory) return false
|
|
171
108
|
|
|
172
109
|
if (handle.ref) {
|
|
@@ -247,7 +184,7 @@ async function restorePrompt(
|
|
|
247
184
|
return true
|
|
248
185
|
}
|
|
249
186
|
|
|
250
|
-
return
|
|
187
|
+
return false
|
|
251
188
|
}
|
|
252
189
|
|
|
253
190
|
function gatherContext(api: Api): string {
|
|
@@ -261,14 +198,14 @@ function gatherContext(api: Api): string {
|
|
|
261
198
|
const sessionID = route.params.sessionID
|
|
262
199
|
const messages = api.state.session.messages(sessionID)
|
|
263
200
|
|
|
264
|
-
const userMessages = messages.filter(
|
|
201
|
+
const userMessages = messages.filter((message): message is Extract<Message, { role: "user" }> => message.role === "user")
|
|
265
202
|
const recent = userMessages.slice(-MAX_RECENT_MESSAGES).reverse()
|
|
266
203
|
if (recent.length > 0) {
|
|
267
204
|
const prompts: string[] = []
|
|
268
205
|
for (const msg of recent) {
|
|
269
206
|
const text = extractVisibleText(api.state.part(msg.id))
|
|
270
207
|
if (text) {
|
|
271
|
-
prompts.push(
|
|
208
|
+
prompts.push(text.length > MAX_PROMPT_PREVIEW_LENGTH ? `${text.slice(0, MAX_PROMPT_PREVIEW_LENGTH)}...` : text)
|
|
272
209
|
}
|
|
273
210
|
}
|
|
274
211
|
if (prompts.length > 0) {
|
|
@@ -287,19 +224,6 @@ function gatherContext(api: Api): string {
|
|
|
287
224
|
return sections.join("\n\n")
|
|
288
225
|
}
|
|
289
226
|
|
|
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
227
|
async function enhanceWithModel(
|
|
304
228
|
api: Api,
|
|
305
229
|
options: PluginOptions | undefined,
|
|
@@ -309,11 +233,18 @@ async function enhanceWithModel(
|
|
|
309
233
|
const directory = api.state.path.directory
|
|
310
234
|
const model = resolveEnhancerModel(api, options)
|
|
311
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")
|
|
312
243
|
|
|
313
244
|
const created = await api.client.session.create(
|
|
314
245
|
{
|
|
315
246
|
directory,
|
|
316
|
-
title:
|
|
247
|
+
title: `Prompt Enhancer ${Math.random().toString(36).slice(2, 8)}`,
|
|
317
248
|
},
|
|
318
249
|
{ signal, throwOnError: true },
|
|
319
250
|
)
|
|
@@ -331,7 +262,7 @@ async function enhanceWithModel(
|
|
|
331
262
|
parts: [
|
|
332
263
|
{
|
|
333
264
|
type: "text",
|
|
334
|
-
text:
|
|
265
|
+
text: userMessage,
|
|
335
266
|
},
|
|
336
267
|
],
|
|
337
268
|
},
|
|
@@ -366,7 +297,11 @@ function openEnhanceDialog(
|
|
|
366
297
|
|
|
367
298
|
if (signal.aborted) return
|
|
368
299
|
|
|
369
|
-
const target =
|
|
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)
|
|
370
305
|
if (!target) {
|
|
371
306
|
api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Enhancement only works from a prompt." })
|
|
372
307
|
return
|