@xinleibird/bridge-opencode 0.2.9 → 0.3.0
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 +1 -0
- package/bridge.ts +22 -52
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Bridge between opencode and Neovim, inspired by [sidekick](https://github.com/Ni
|
|
|
7
7
|
- **Buffer protection**: When you have unsaved changes in a buffer, opencode waits — edits are denied and your work is preserved
|
|
8
8
|
- **Auto-reload**: When opencode modifies a file you have open, the buffer is auto-reloaded with cursor position preserved
|
|
9
9
|
- **Visual selection context**: Visual selections in Neovim are sent to opencode as chat context
|
|
10
|
+
- `#this`: Type `#this` in chat to attach the current visual selection from Neovim
|
|
10
11
|
|
|
11
12
|
## Structure
|
|
12
13
|
|
package/bridge.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Plugin } from "@opencode-ai/plugin";
|
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import { access } from "node:fs/promises";
|
|
4
4
|
import { basename, isAbsolute, join } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "bun";
|
|
5
6
|
|
|
6
7
|
interface BufferStatus {
|
|
7
8
|
isCurrent: boolean;
|
|
@@ -126,58 +127,17 @@ export const BridgePlugin: Plugin = async ({ directory }) => {
|
|
|
126
127
|
}
|
|
127
128
|
},
|
|
128
129
|
|
|
129
|
-
"experimental.chat.messages.transform": async (_, output) => {
|
|
130
|
-
let selections: Awaited<ReturnType<typeof getVisualSelections>>;
|
|
131
|
-
try {
|
|
132
|
-
selections = await getVisualSelections();
|
|
133
|
-
} catch {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (!selections || selections.length === 0) return;
|
|
137
|
-
|
|
138
|
-
const filteredSelections = selections.filter((s) => !s.cwd || s.cwd === cwd);
|
|
139
|
-
if (filteredSelections.length === 0) return;
|
|
140
|
-
|
|
141
|
-
const userMessages = output.messages.filter((m) => m.info.role === "user");
|
|
142
|
-
if (userMessages.length === 0) return;
|
|
143
|
-
|
|
144
|
-
const latestUserMessage = userMessages[userMessages.length - 1];
|
|
145
|
-
if (!latestUserMessage.parts) {
|
|
146
|
-
latestUserMessage.parts = [];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const blocks: string[] = [];
|
|
150
|
-
for (const s of filteredSelections) {
|
|
151
|
-
try {
|
|
152
|
-
await access(s.filePath);
|
|
153
|
-
} catch {
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (!s.startLine || !s.content) continue;
|
|
157
|
-
|
|
158
|
-
const fileName = basename(s.filePath);
|
|
159
|
-
blocks.push(
|
|
160
|
-
`Visual selection from nvim:\n## ${fileName}(${s.filePath}:${s.startLine}-${s.endLine})\n\`\`\`\n${s.content}\n\`\`\``,
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (blocks.length === 0) return;
|
|
165
|
-
|
|
166
|
-
const textPart = latestUserMessage.parts.findLast((p) => p.type === "text");
|
|
167
|
-
if (textPart && typeof textPart.text === "string") {
|
|
168
|
-
textPart.text += `\n\n${blocks.join("\n\n")}`;
|
|
169
|
-
} else {
|
|
170
|
-
latestUserMessage.parts.push({
|
|
171
|
-
id: crypto.randomUUID(),
|
|
172
|
-
sessionID: latestUserMessage.info.sessionID,
|
|
173
|
-
messageID: latestUserMessage.info.id,
|
|
174
|
-
type: "text",
|
|
175
|
-
text: blocks.join("\n\n"),
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
},
|
|
179
|
-
|
|
180
130
|
"chat.message": async (input, output) => {
|
|
131
|
+
const isTriggered = output.parts.some(
|
|
132
|
+
(p) =>
|
|
133
|
+
p.type === "text" &&
|
|
134
|
+
"text" in p &&
|
|
135
|
+
!p.synthetic &&
|
|
136
|
+
typeof p.text === "string" &&
|
|
137
|
+
p.text.includes("#this"),
|
|
138
|
+
);
|
|
139
|
+
if (!isTriggered) return;
|
|
140
|
+
|
|
181
141
|
let selections: Awaited<ReturnType<typeof getVisualSelections>>;
|
|
182
142
|
try {
|
|
183
143
|
selections = await getVisualSelections();
|
|
@@ -206,10 +166,20 @@ export const BridgePlugin: Plugin = async ({ directory }) => {
|
|
|
206
166
|
messageID: input.messageID ?? "",
|
|
207
167
|
mime: "text/plain",
|
|
208
168
|
filename: `${fileName}:${s.startLine}-${s.endLine}`,
|
|
209
|
-
url:
|
|
169
|
+
url: pathToFileURL(s.filePath).href,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
output.parts.push({
|
|
173
|
+
type: "text",
|
|
174
|
+
id: crypto.randomUUID(),
|
|
175
|
+
sessionID: input.sessionID,
|
|
176
|
+
messageID: input.messageID ?? "",
|
|
177
|
+
synthetic: true,
|
|
178
|
+
text: `## current selection — ${fileName}(${s.filePath}:${s.startLine}-${s.endLine}):\n\`\`\`\n${s.content}\n\`\`\``,
|
|
210
179
|
});
|
|
211
180
|
}
|
|
212
181
|
},
|
|
213
182
|
};
|
|
214
183
|
};
|
|
184
|
+
|
|
215
185
|
export default BridgePlugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xinleibird/bridge-opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@napi-rs/cli": "^2.18.0",
|
|
25
|
+
"@types/bun": "^1.3.14",
|
|
25
26
|
"@types/node": "^25.9.1"
|
|
26
27
|
},
|
|
27
28
|
"peerDependencies": {
|
|
@@ -38,8 +39,5 @@
|
|
|
38
39
|
"aarch64-unknown-linux-gnu"
|
|
39
40
|
]
|
|
40
41
|
}
|
|
41
|
-
},
|
|
42
|
-
"dependencies": {
|
|
43
|
-
"@opencode-ai/sdk": "^1.15.12"
|
|
44
42
|
}
|
|
45
43
|
}
|