@mtayfur/opencode-prompt-enhancer 0.0.2

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 ADDED
@@ -0,0 +1,37 @@
1
+ # opencode-prompt-enhancer
2
+
3
+ OpenCode TUI plugin that rewrites rough prompt drafts into stronger prompts with workspace context.
4
+
5
+ ## Package
6
+
7
+ - npm: `opencode-prompt-enhancer`
8
+ - GitHub: `mtayfur/opencode-prompt-enhancer`
9
+
10
+ ## Install
11
+
12
+ Add the package to OpenCode's `tui.json` plugin list:
13
+
14
+ ```jsonc
15
+ {
16
+ "plugin": [
17
+ "opencode-prompt-enhancer@latest"
18
+ ]
19
+ }
20
+ ```
21
+
22
+ ## Use
23
+
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
29
+
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.
31
+
32
+ ## Development
33
+
34
+ ```bash
35
+ bun install
36
+ bun run typecheck
37
+ ```
package/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from "./plugins/prompt-enhancer"
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@mtayfur/opencode-prompt-enhancer",
3
+ "version": "0.0.2",
4
+ "description": "OpenCode plugin that rewrites rough drafts into stronger prompts.",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "main": "./index.ts",
10
+ "exports": {
11
+ ".": "./index.ts",
12
+ "./tui": "./plugins/prompt-enhancer.tsx"
13
+ },
14
+ "oc-plugin": [
15
+ "tui"
16
+ ],
17
+ "scripts": {
18
+ "typecheck": "bunx tsc -p tsconfig.json"
19
+ },
20
+ "files": [
21
+ "README.md",
22
+ "index.ts",
23
+ "plugins"
24
+ ],
25
+ "dependencies": {
26
+ "@opencode-ai/plugin": "1.14.20",
27
+ "@opentui/solid": "0.2.1",
28
+ "solid-js": "1.9.12"
29
+ },
30
+ "devDependencies": {
31
+ "typescript": "5.8.2"
32
+ },
33
+ "keywords": [
34
+ "opencode",
35
+ "plugin",
36
+ "prompt",
37
+ "tui"
38
+ ]
39
+ }
@@ -0,0 +1,318 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { PluginOptions } from "@opencode-ai/plugin"
3
+ import type { Message, Part, TextPart } from "@opencode-ai/sdk/v2"
4
+ import type { TuiPlugin, TuiPluginModule, TuiRouteCurrent, TuiSidebarFileItem, TuiSidebarTodoItem } from "@opencode-ai/plugin/tui"
5
+
6
+ const MAX_RECENT_MESSAGES = 6
7
+ const MAX_CHANGED_FILES = 30
8
+ const MAX_PROMPT_PREVIEW_LENGTH = 300
9
+ const MAX_TODOS = 10
10
+ const DIALOG_TITLE = "Enhance Prompt"
11
+ const TOAST_TITLE = "Prompt enhancer"
12
+ const TEMP_SESSION_TITLE = "Prompt Enhancer"
13
+
14
+ const ENHANCER_SYSTEM_PROMPT = `You rewrite rough user drafts into strong prompts for OpenCode, an AI coding assistant.
15
+
16
+ Goal:
17
+ 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.
18
+
19
+ OpenCode context:
20
+ 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.
21
+
22
+ Rules:
23
+ - Preserve the original request type.
24
+ - If the draft is a question, return a better question.
25
+ - If the draft is a bug fix, return a better bug-fix request.
26
+ - If the draft is a review, return a better review request.
27
+ - Preserve explicit constraints, file names, commands, error messages, acceptance criteria, and user wording when they matter.
28
+ - Use workspace context only when it clearly helps disambiguate or narrow the task.
29
+ - Reference specific files, paths, branches, recent prompts, changed files, or todos only when they are directly relevant.
30
+ - Resolve vague references like "this", "that bug", or "the plugin" from context when clear.
31
+ - If context does not clearly resolve a reference, do not invent details.
32
+ - 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.
33
+ - Prefer wording that helps OpenCode start work immediately.
34
+ - If the draft is already precise, return it with minimal cleanup.
35
+
36
+ Do not:
37
+ - Do not invent requirements, files, APIs, dependencies, bugs, or acceptance criteria.
38
+ - Do not broaden scope with extra features, refactors, tests, or docs unless the draft implies them.
39
+ - Do not ask follow-up questions inside the rewritten prompt.
40
+ - Do not mention the existence of context, tags, or these instructions.
41
+ - Do not output explanations, markdown fences, or commentary.
42
+
43
+ Output:
44
+ Return exactly one enhanced user prompt as plain text.`
45
+
46
+ type ModelRef = {
47
+ providerID: string
48
+ modelID: string
49
+ }
50
+
51
+ type Api = Parameters<TuiPlugin>[0]
52
+ type PluginState = {
53
+ enhancing: boolean
54
+ }
55
+
56
+ function parseModelString(value: string | undefined): ModelRef | undefined {
57
+ if (!value) return undefined
58
+ const trimmed = value.trim()
59
+ const slash = trimmed.indexOf("/")
60
+ if (slash <= 0 || slash === trimmed.length - 1) return undefined
61
+ return {
62
+ providerID: trimmed.slice(0, slash),
63
+ modelID: trimmed.slice(slash + 1),
64
+ }
65
+ }
66
+
67
+ function getModelOverride(options: PluginOptions | undefined): ModelRef | undefined {
68
+ const value = typeof options?.model === "string" ? options.model : undefined
69
+ return parseModelString(value)
70
+ }
71
+
72
+ function isSessionRoute(route: TuiRouteCurrent): route is Extract<TuiRouteCurrent, { name: "session" }> {
73
+ return route.name === "session"
74
+ }
75
+
76
+ function isUserMessage(message: Message): message is Extract<Message, { role: "user" }> {
77
+ return message.role === "user"
78
+ }
79
+
80
+ function isVisibleTextPart(part: Part): part is TextPart {
81
+ return part.type === "text" && !part.ignored
82
+ }
83
+
84
+ function extractVisibleText(parts: ReadonlyArray<Part>): string {
85
+ return parts
86
+ .filter(isVisibleTextPart)
87
+ .map((part) => part.text)
88
+ .join("")
89
+ .trim()
90
+ }
91
+
92
+ function truncate(value: string, maxLength: number): string {
93
+ return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value
94
+ }
95
+
96
+ function formatTodo(todo: TuiSidebarTodoItem): string {
97
+ return ` [${todo.status || "?"}] ${todo.content || "untitled"}`
98
+ }
99
+
100
+ function resolveEnhancerModel(api: Api, options: PluginOptions | undefined): ModelRef | undefined {
101
+ const override = getModelOverride(options)
102
+ if (override) return override
103
+
104
+ return parseModelString(api.state.config.small_model || api.state.config.model)
105
+ }
106
+
107
+ function gatherContext(api: Api): string {
108
+ const sections: string[] = []
109
+
110
+ const dir = api.state.path.directory
111
+ const branch = api.state.vcs?.branch
112
+ sections.push(`Working directory: ${dir}${branch ? ` (branch: ${branch})` : ""}`)
113
+
114
+ const route = api.route.current
115
+ if (isSessionRoute(route)) {
116
+ const sessionID = route.params.sessionID
117
+ const messages = api.state.session.messages(sessionID)
118
+
119
+ const userMessages = messages.filter(isUserMessage)
120
+ const recent = userMessages.slice(-MAX_RECENT_MESSAGES)
121
+ if (recent.length > 0) {
122
+ const prompts: string[] = []
123
+ for (const msg of recent) {
124
+ const text = extractVisibleText(api.state.part(msg.id))
125
+ if (text) {
126
+ prompts.push(truncate(text, MAX_PROMPT_PREVIEW_LENGTH))
127
+ }
128
+ }
129
+ if (prompts.length > 0) {
130
+ sections.push(`Recent user prompts in this session (oldest first):\n${prompts.map((p, i) => `${i + 1}. ${p}`).join("\n")}`)
131
+ }
132
+ }
133
+
134
+ const diff = api.state.session.diff(sessionID)
135
+ if (diff.length > 0) {
136
+ const files = diff.slice(0, MAX_CHANGED_FILES).map((f: TuiSidebarFileItem) =>
137
+ ` ${f.file} (+${f.additions} -${f.deletions})`
138
+ )
139
+ const label = diff.length > MAX_CHANGED_FILES
140
+ ? `Files changed in session (showing ${MAX_CHANGED_FILES} of ${diff.length}):`
141
+ : `Files changed in session:`
142
+ sections.push(`${label}\n${files.join("\n")}`)
143
+ }
144
+
145
+ const todos = api.state.session.todo(sessionID)
146
+ if (todos.length > 0) {
147
+ const todoLines = todos.slice(0, MAX_TODOS).map(formatTodo)
148
+ sections.push(`Active todos:\n${todoLines.join("\n")}`)
149
+ }
150
+ }
151
+
152
+ return sections.join("\n\n")
153
+ }
154
+
155
+ function buildUserMessage(input: string, context: string): string {
156
+ const sections = [
157
+ "Rewrite the following user draft into a stronger prompt.",
158
+ `User draft:\n${input}`,
159
+ ]
160
+
161
+ if (context) {
162
+ sections.splice(1, 0, `Workspace context (use only if directly relevant):\n${context}`)
163
+ }
164
+
165
+ return sections.join("\n\n")
166
+ }
167
+
168
+ async function enhanceWithModel(
169
+ api: Api,
170
+ options: PluginOptions | undefined,
171
+ input: string,
172
+ signal: AbortSignal,
173
+ ): Promise<string> {
174
+ const directory = api.state.path.directory
175
+ const model = resolveEnhancerModel(api, options)
176
+ const context = gatherContext(api)
177
+
178
+ const created = await api.client.session.create(
179
+ {
180
+ directory,
181
+ title: TEMP_SESSION_TITLE,
182
+ },
183
+ { signal, throwOnError: true },
184
+ )
185
+
186
+ const tempSessionID = created.data?.id
187
+ if (!tempSessionID) throw new Error("Prompt enhancer session creation failed.")
188
+
189
+ try {
190
+ const response = await api.client.session.prompt(
191
+ {
192
+ sessionID: tempSessionID,
193
+ directory,
194
+ model,
195
+ system: ENHANCER_SYSTEM_PROMPT,
196
+ parts: [
197
+ {
198
+ type: "text",
199
+ text: buildUserMessage(input, context),
200
+ },
201
+ ],
202
+ },
203
+ { signal, throwOnError: true },
204
+ )
205
+
206
+ const parts = response.data?.parts
207
+ if (!parts) throw new Error("Enhancer model returned no response.")
208
+
209
+ const enhanced = extractVisibleText(parts)
210
+ if (!enhanced) throw new Error("Enhancer model returned no text.")
211
+ return enhanced
212
+ } finally {
213
+ try {
214
+ await api.client.session.delete({ sessionID: tempSessionID, directory })
215
+ } catch {
216
+ // Best-effort cleanup for the temporary enhancement session.
217
+ }
218
+ }
219
+ }
220
+
221
+ function openEnhanceDialog(
222
+ api: Api,
223
+ options: PluginOptions | undefined,
224
+ state: PluginState,
225
+ signal: AbortSignal,
226
+ ): void {
227
+ if (state.enhancing) {
228
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Enhancement already in progress." })
229
+ return
230
+ }
231
+
232
+ if (signal.aborted) return
233
+
234
+ api.ui.dialog.replace(() => (
235
+ <api.ui.DialogPrompt
236
+ title={DIALOG_TITLE}
237
+ placeholder="Describe what you want to do..."
238
+ onCancel={() => api.ui.dialog.clear()}
239
+ onConfirm={(value) => {
240
+ const input = value.trim()
241
+ if (!input) {
242
+ api.ui.toast({ variant: "warning", title: TOAST_TITLE, message: "Prompt is empty." })
243
+ api.ui.dialog.clear()
244
+ return
245
+ }
246
+
247
+ state.enhancing = true
248
+ api.ui.dialog.clear()
249
+ api.ui.toast({
250
+ variant: "info",
251
+ title: TOAST_TITLE,
252
+ message: "Enhancing prompt...",
253
+ duration: 15_000,
254
+ })
255
+
256
+ void (async () => {
257
+ try {
258
+ const enhanced = await enhanceWithModel(api, options, input, signal)
259
+ if (signal.aborted) return
260
+
261
+ const directory = api.state.path.directory
262
+ const requestOptions = { signal, throwOnError: true } as const
263
+ await api.client.tui.clearPrompt({ directory }, requestOptions)
264
+ await api.client.tui.appendPrompt({ directory, text: enhanced }, requestOptions)
265
+ api.ui.toast({
266
+ variant: "success",
267
+ title: "Prompt enhanced",
268
+ message: "Enhanced prompt written to input.",
269
+ duration: 3000,
270
+ })
271
+ } catch (error) {
272
+ if (signal.aborted) return
273
+
274
+ const message = error instanceof Error ? error.message : "Model enhancement failed."
275
+ api.ui.toast({ variant: "error", title: TOAST_TITLE, message })
276
+ } finally {
277
+ state.enhancing = false
278
+ }
279
+ })()
280
+ }}
281
+ />
282
+ ))
283
+ }
284
+
285
+ const tui: TuiPlugin = async (api, options) => {
286
+ const state: PluginState = { enhancing: false }
287
+
288
+ const unregister = api.command.register(() => [
289
+ {
290
+ title: DIALOG_TITLE,
291
+ value: "prompt-enhancer.enhance",
292
+ description: "Rewrite a draft prompt with project context (Ctrl+E)",
293
+ category: "Prompt",
294
+ keybind: "ctrl+e",
295
+ suggested: true,
296
+ slash: {
297
+ name: "enhance",
298
+ aliases: ["enhance-prompt"],
299
+ },
300
+ onSelect: () => {
301
+ openEnhanceDialog(api, options, state, api.lifecycle.signal)
302
+ },
303
+ },
304
+ ])
305
+
306
+ api.lifecycle.onDispose(() => {
307
+ unregister()
308
+ state.enhancing = false
309
+ })
310
+ }
311
+
312
+ const plugin: TuiPluginModule & { id: string } = {
313
+ id: "prompt-enhancer",
314
+ tui,
315
+ }
316
+
317
+ export default plugin
318
+