Repository navigation
Expand file tree
/
Copy pathplugin-v2.ts
More file actions
128 lines (119 loc) · 5.65 KB
/
Copy pathplugin-v2.ts
File metadata and controls
128 lines (119 loc) · 5.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// opencode-rtl-fix — OpenCode v2 server plugin.
//
// V2 loads this file directly (`"./v2": "./plugin-v2.ts"`) and resolves
// `@opencode/plugin` at runtime, so this file intentionally lives OUTSIDE
// `src/` (tsc never compiles it) and uses no static type imports.
// Shared bidi logic comes from the built `dist/core.js` (same as v1).
//
// Mapping from v1 hooks (see src/server.ts):
// chat.message -> ctx.session.hook("prompt")
// experimental.chat.system.transform -> ctx.session.hook("context" + "compaction" + "generate" + "title")
// experimental.chat.messages.transform -> ctx.session.hook("context")
// tool.execute.after -> ctx.tool.hook("execute.after")
// shell.env -> ctx.shell.hook("create.before")
// (experimental.text.complete has no v2 equivalent; assistant text is
// covered by sanitizing context messages + system guidance.)
import { Plugin } from "@opencode/plugin"
import { formatBidiText, normalizeOptions, statusText, systemPrompt } from "./dist/core.js"
import { ensureDesktopPatch } from "./dist/desktop.js"
const ID = "opencode-rtl-fix"
function mutateTextFields(value, format) {
if (typeof value !== "object" || value === null || Array.isArray(value)) return
for (const key of ["text", "content", "value", "output", "summary", "title"]) {
const current = value[key]
if (typeof current !== "string" || !current.trim()) continue
value[key] = format(current)
}
}
function mutateMessages(messages, format) {
if (!Array.isArray(messages)) return
for (const message of messages) {
if (!message || typeof message !== "object") continue
mutateTextFields(message, format)
const parts = message.parts
if (Array.isArray(parts)) for (const part of parts) mutateTextFields(part, format)
}
}
export default {
...Plugin.define({
id: ID,
async setup(ctx) {
const settings = normalizeOptions(ctx.options)
const plain = { ...settings, alignRtlParagraphs: false }
const clean = !settings.inlineControls
const userMode = settings.isolateUserMessages
const assistantMode = settings.isolateAssistantText
const fmtUser = (s) => formatBidiText(s, userMode, plain)
const fmtAssistant = (s) => formatBidiText(s, assistantMode, plain)
ensureDesktopPatch({ enabled: settings.desktop, log: (message) => console.log(`[${ID}] ${message}`) })
console.log(`[${ID}] initialized (${statusText(settings)})`)
// 1) Prompt admission — the v2 equivalent of v1 `chat.message`.
await ctx.session.hook("prompt", (event) => {
if (!settings.enabled) return
if (!clean && userMode === "off") return
if (event.prompt) mutateTextFields(event.prompt, fmtUser)
const files = event.prompt?.files
if (Array.isArray(files)) for (const f of files) mutateTextFields(f, fmtUser)
})
// 2) System guidance + history sanitation for every model-request kind.
// v1 only had one system hook; v2 splits requests into context /
// compaction / generate / title, so register on all four.
const guidance = systemPrompt(settings)
const applyContext = (event) => {
if (!settings.enabled) return
if (guidance && Array.isArray(event.system)) {
const exists = event.system.some(
(s) => (typeof s === "string" ? s : s?.text ?? "").includes("RTL language support is active."),
)
if (!exists) event.system.push({ type: "text", text: guidance })
}
if (clean || userMode !== "off") mutateMessages(event.messages, fmtUser)
}
await ctx.session.hook("context", applyContext)
await ctx.session.hook("compaction", (event) => {
if (!settings.enabled) return
if (clean || userMode !== "off") mutateMessages(event.messages, fmtUser)
if (event.result) mutateTextFields(event.result, fmtAssistant)
})
await ctx.session.hook("generate", applyContext)
await ctx.session.hook("title", (event) => {
if (!settings.enabled) return
if (event.result) mutateTextFields(event.result, fmtAssistant)
})
// 3) Tool output — v2 equivalent of v1 `tool.execute.after`.
if (settings.enabled && settings.isolateToolOutput !== "off") {
const toolMode = settings.isolateToolOutput
await ctx.tool.hook("execute.after", (event) => {
const out = event.output ?? event.result ?? event.data
if (typeof out === "string") {
const fixed = formatBidiText(out, toolMode, plain)
if (event.output !== undefined) event.output = fixed
else if (event.result !== undefined) event.result = fixed
else if (event.data !== undefined) event.data = fixed
} else if (out && typeof out === "object") {
mutateTextFields(out, (s) => formatBidiText(s, toolMode, plain))
}
})
}
// 4) Direction env — v2 equivalent of v1 `shell.env`.
if (settings.directionEnv) {
await ctx.shell.hook("create.before", (event) => {
event.env = {
...event.env,
OPENCODE_RTL: settings.enabled ? "1" : "0",
OPENCODE_RTL_LANGUAGE: settings.language,
OPENCODE_RTL_USER_ISOLATION: settings.isolateUserMessages,
OPENCODE_RTL_ASSISTANT_ISOLATION: settings.isolateAssistantText,
}
})
}
},
}),
// V1 object entrypoint (opencode >= 1.18.29 also accepts this shape).
// Kept so the SAME package runs on v1.19 without a separate install.
async server(input, options) {
const { server } = await import("./dist/server.js")
const hooks = await server(input, options)
return hooks
},
}