@xinleibird/bridge-opencode 0.2.8 → 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
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
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/bridge.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
-
import type { FilePart, Part } from "@opencode-ai/sdk";
|
|
3
2
|
import crypto from "node:crypto";
|
|
4
3
|
import { access } from "node:fs/promises";
|
|
5
4
|
import { basename, isAbsolute, join } from "node:path";
|
|
5
|
+
import { pathToFileURL } from "bun";
|
|
6
6
|
|
|
7
7
|
interface BufferStatus {
|
|
8
8
|
isCurrent: boolean;
|
|
@@ -127,72 +127,17 @@ export const BridgePlugin: Plugin = async ({ directory }) => {
|
|
|
127
127
|
}
|
|
128
128
|
},
|
|
129
129
|
|
|
130
|
-
"experimental.chat.messages.transform": async (_, output) => {
|
|
131
|
-
let selections: Awaited<ReturnType<typeof getVisualSelections>>;
|
|
132
|
-
try {
|
|
133
|
-
selections = await getVisualSelections();
|
|
134
|
-
} catch {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
if (!selections || selections.length === 0) return;
|
|
138
|
-
|
|
139
|
-
const filteredSelections = selections.filter((s) => !s.cwd || s.cwd === cwd);
|
|
140
|
-
if (filteredSelections.length === 0) return;
|
|
141
|
-
|
|
142
|
-
const userMessages = output.messages.filter((m) => m.info.role === "user");
|
|
143
|
-
if (userMessages.length === 0) return;
|
|
144
|
-
|
|
145
|
-
const latestUserMessage = userMessages[userMessages.length - 1];
|
|
146
|
-
const msgInfo = latestUserMessage.info;
|
|
147
|
-
if (!msgInfo || msgInfo.role !== "user") return;
|
|
148
|
-
if (!latestUserMessage.parts) {
|
|
149
|
-
latestUserMessage.parts = [];
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const refs: string[] = [];
|
|
153
|
-
for (const s of filteredSelections) {
|
|
154
|
-
try {
|
|
155
|
-
await access(s.filePath);
|
|
156
|
-
} catch {
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
if (!s.startLine) continue;
|
|
160
|
-
|
|
161
|
-
const fileRef = `${s.filePath}:${s.startLine}-${s.endLine}`;
|
|
162
|
-
refs.push(fileRef);
|
|
163
|
-
|
|
164
|
-
const fileName = basename(s.filePath);
|
|
165
|
-
|
|
166
|
-
const filePart: FilePart = {
|
|
167
|
-
id: crypto.randomUUID(),
|
|
168
|
-
sessionID: msgInfo.sessionID,
|
|
169
|
-
messageID: msgInfo.id,
|
|
170
|
-
type: "file",
|
|
171
|
-
mime: "text/plain",
|
|
172
|
-
filename: fileName,
|
|
173
|
-
url: `file://${s.filePath}?start=${s.startLine}&end=${s.endLine}`,
|
|
174
|
-
source: {
|
|
175
|
-
text: {
|
|
176
|
-
start: 0,
|
|
177
|
-
value: fileRef,
|
|
178
|
-
end: fileRef.length,
|
|
179
|
-
},
|
|
180
|
-
type: "file",
|
|
181
|
-
path: s.filePath,
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
latestUserMessage.parts.push(filePart);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (refs.length === 0) return;
|
|
188
|
-
|
|
189
|
-
const textPart = latestUserMessage.parts.find((p) => p.type === "text") as Part | undefined;
|
|
190
|
-
if (textPart && textPart.type === "text" && typeof textPart.text === "string") {
|
|
191
|
-
textPart.text = `${refs.join("\n")}\n\n${textPart.text}`;
|
|
192
|
-
}
|
|
193
|
-
},
|
|
194
|
-
|
|
195
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
|
+
|
|
196
141
|
let selections: Awaited<ReturnType<typeof getVisualSelections>>;
|
|
197
142
|
try {
|
|
198
143
|
selections = await getVisualSelections();
|
|
@@ -221,10 +166,20 @@ export const BridgePlugin: Plugin = async ({ directory }) => {
|
|
|
221
166
|
messageID: input.messageID ?? "",
|
|
222
167
|
mime: "text/plain",
|
|
223
168
|
filename: `${fileName}:${s.startLine}-${s.endLine}`,
|
|
224
|
-
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\`\`\``,
|
|
225
179
|
});
|
|
226
180
|
}
|
|
227
181
|
},
|
|
228
182
|
};
|
|
229
183
|
};
|
|
184
|
+
|
|
230
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
|
}
|