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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-translate",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "OpenCode plugin that lets the user chat in a configured source language while the main chat loop only sees English.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
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
- sessionStateCache.set(sessionID, state ?? null)
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 === "user") {
403
- for (const part of message.parts) {
404
- if (!isTextPart(part)) continue
405
- if (!shouldRequireCache(part)) continue
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.`,