@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
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: `file://${s.filePath}?start=${s.startLine}&end=${s.endLine}`,
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.2.8",
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
  }