opencode-translate 0.0.7 → 0.0.9
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/package.json +1 -1
- package/src/activation.ts +55 -34
- package/src/constants.ts +0 -6
package/package.json
CHANGED
package/src/activation.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { randomBytes } from "node:crypto"
|
|
|
2
2
|
import type { Hooks, PluginInput, PluginOptions } from "@opencode-ai/plugin"
|
|
3
3
|
import {
|
|
4
4
|
buildInboundTranslationError,
|
|
5
|
-
buildStaleCacheError,
|
|
6
5
|
isTextPart,
|
|
7
6
|
isTranslateStateRecord,
|
|
8
7
|
isUserAuthoredTextPart,
|
|
@@ -159,6 +158,28 @@ function createSyntheticTextPart(
|
|
|
159
158
|
}
|
|
160
159
|
}
|
|
161
160
|
|
|
161
|
+
// LLM-only text part: hidden from the TUI but the only LLM-visible
|
|
162
|
+
// representation of the user's source-language text. The original
|
|
163
|
+
// user-authored part is marked `ignored:true` so the LLM never sees it,
|
|
164
|
+
// and this synthetic English twin carries the actual prompt content.
|
|
165
|
+
function createLlmOnlyTextPart(
|
|
166
|
+
sessionID: string,
|
|
167
|
+
messageID: string,
|
|
168
|
+
text: string,
|
|
169
|
+
metadata: Record<string, unknown>,
|
|
170
|
+
): TextPartLike {
|
|
171
|
+
return {
|
|
172
|
+
id: createSyntheticPartID(),
|
|
173
|
+
sessionID,
|
|
174
|
+
messageID,
|
|
175
|
+
type: "text",
|
|
176
|
+
text,
|
|
177
|
+
synthetic: true,
|
|
178
|
+
ignored: false,
|
|
179
|
+
metadata,
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
162
183
|
function escapeRegex(value: string): string {
|
|
163
184
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
164
185
|
}
|
|
@@ -257,7 +278,11 @@ async function resolveSessionState(
|
|
|
257
278
|
}),
|
|
258
279
|
)
|
|
259
280
|
const state = extractStoredState(storedMessages)
|
|
260
|
-
|
|
281
|
+
if (state) {
|
|
282
|
+
sessionStateCache.set(sessionID, state)
|
|
283
|
+
} else if (storedMessages.length > 0) {
|
|
284
|
+
sessionStateCache.set(sessionID, null)
|
|
285
|
+
}
|
|
261
286
|
|
|
262
287
|
return {
|
|
263
288
|
sessionActive: Boolean(state),
|
|
@@ -267,10 +292,6 @@ async function resolveSessionState(
|
|
|
267
292
|
}
|
|
268
293
|
}
|
|
269
294
|
|
|
270
|
-
function shouldRequireCache(part: TextPartLike): boolean {
|
|
271
|
-
return isUserAuthoredTextPart(part) && part.text.trim().length > 0
|
|
272
|
-
}
|
|
273
|
-
|
|
274
295
|
export function createHooks(ctx: PluginInput, rawOptions: PluginOptions = {}, deps: HookDependencies = {}): Hooks {
|
|
275
296
|
if (process.env.OPENCODE_TRANSLATE_DISABLE === "1") {
|
|
276
297
|
return {}
|
|
@@ -306,6 +327,8 @@ export function createHooks(ctx: PluginInput, rawOptions: PluginOptions = {}, de
|
|
|
306
327
|
}
|
|
307
328
|
activatedThisTurn = true
|
|
308
329
|
sessionStateCache.set(input.sessionID, activeState)
|
|
330
|
+
} else {
|
|
331
|
+
sessionStateCache.set(input.sessionID, null)
|
|
309
332
|
}
|
|
310
333
|
}
|
|
311
334
|
|
|
@@ -336,7 +359,13 @@ export function createHooks(ctx: PluginInput, rawOptions: PluginOptions = {}, de
|
|
|
336
359
|
...(part.metadata ?? {}),
|
|
337
360
|
...mergeTranslatedMetadata(activeState, part, english),
|
|
338
361
|
}
|
|
362
|
+
// Hide the user's source-language text from the LLM. The TUI
|
|
363
|
+
// still renders it because `ignored:true` only affects the
|
|
364
|
+
// user-side LLM serializer (`message-v2.ts:773`).
|
|
365
|
+
part.ignored = true
|
|
339
366
|
|
|
367
|
+
// UI-only preview so the user can verify the translation that
|
|
368
|
+
// was sent to the LLM.
|
|
340
369
|
nextParts.push(
|
|
341
370
|
createSyntheticTextPart(part.sessionID, part.messageID, `→ EN: ${english}`, {
|
|
342
371
|
translate_role: "translation_preview",
|
|
@@ -345,6 +374,17 @@ export function createHooks(ctx: PluginInput, rawOptions: PluginOptions = {}, de
|
|
|
345
374
|
translate_part_index: currentEligibleIndex,
|
|
346
375
|
}),
|
|
347
376
|
)
|
|
377
|
+
|
|
378
|
+
// LLM-only English twin. This is the actual prompt the model
|
|
379
|
+
// sees in place of the now-`ignored` source-language part.
|
|
380
|
+
nextParts.push(
|
|
381
|
+
createLlmOnlyTextPart(part.sessionID, part.messageID, english, {
|
|
382
|
+
translate_role: "llm_only_translation",
|
|
383
|
+
translate_nonce: activeState.translate_nonce,
|
|
384
|
+
translate_source_hash: sourceHash,
|
|
385
|
+
translate_part_index: currentEligibleIndex,
|
|
386
|
+
}),
|
|
387
|
+
)
|
|
348
388
|
} catch (error) {
|
|
349
389
|
// Fall back to sending the original text to the LLM so the user
|
|
350
390
|
// still gets a response. Surface the error as a synthetic part.
|
|
@@ -398,35 +438,16 @@ export function createHooks(ctx: PluginInput, rawOptions: PluginOptions = {}, de
|
|
|
398
438
|
const activeState = resolved.state
|
|
399
439
|
if (!activeState) return
|
|
400
440
|
|
|
441
|
+
// User parts need no in-place rewriting: the source-language text
|
|
442
|
+
// part is `ignored:true` so the LLM serializer skips it, and a
|
|
443
|
+
// synthetic English twin (created in `chat.message`) carries the
|
|
444
|
+
// actual prompt content. Only assistant parts still need their
|
|
445
|
+
// localized trailer stripped before re-entering the model.
|
|
401
446
|
for (const message of output.messages as MessageWithPartsLike[]) {
|
|
402
|
-
if (message.info.role
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const metadata = asMetadata(part)
|
|
407
|
-
const sourceHash = hashText(part.text)
|
|
408
|
-
if (
|
|
409
|
-
metadata.translate_enabled === true &&
|
|
410
|
-
metadata.translate_nonce === activeState.translate_nonce &&
|
|
411
|
-
metadata.translate_source_hash === sourceHash &&
|
|
412
|
-
typeof metadata.translate_en === "string"
|
|
413
|
-
) {
|
|
414
|
-
part.text = metadata.translate_en
|
|
415
|
-
continue
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// Stale cache or untranslated text. Send it through as-is
|
|
419
|
-
// (English history would be ideal, but we shouldn't block the
|
|
420
|
-
// session). Also log so the user can diagnose if needed.
|
|
421
|
-
await logError(client, buildStaleCacheError())
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
if (message.info.role === "assistant") {
|
|
426
|
-
for (const part of message.parts) {
|
|
427
|
-
if (!isTextPart(part)) continue
|
|
428
|
-
part.text = extractEnglishHistoryText(part.text, activeState.translate_nonce)
|
|
429
|
-
}
|
|
447
|
+
if (message.info.role !== "assistant") continue
|
|
448
|
+
for (const part of message.parts) {
|
|
449
|
+
if (!isTextPart(part)) continue
|
|
450
|
+
part.text = extractEnglishHistoryText(part.text, activeState.translate_nonce)
|
|
430
451
|
}
|
|
431
452
|
}
|
|
432
453
|
} catch (error) {
|
package/src/constants.ts
CHANGED
|
@@ -216,12 +216,6 @@ export function buildInboundTranslationError(sourceLanguage: string, reason: str
|
|
|
216
216
|
)
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
export function buildStaleCacheError(): Error {
|
|
220
|
-
return new Error(
|
|
221
|
-
`[${PLUGIN_NAME}:STALE_CACHE] A previously translated user message was edited. Resend the message or start a new session.`,
|
|
222
|
-
)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
219
|
export function buildAuthUnavailableError(providerID: string, envVar: string): Error {
|
|
226
220
|
return new Error(
|
|
227
221
|
`[${PLUGIN_NAME}:AUTH_UNAVAILABLE] No credential found for provider "${providerID}". Set ${envVar} in the environment, run "opencode auth login ${providerID}", or set options.apiKey in opencode.json.`,
|