opencode-translate 0.0.12 → 0.0.13

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/activation.ts +45 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-translate",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
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
@@ -137,26 +137,14 @@ function mergeTranslatedMetadata(state: TranslateState, part: TextPartLike, engl
137
137
  // OpenCode's flag semantics, observed from packages/opencode and packages/ui:
138
138
  // synthetic: true -> hidden from the user UI, still sent to the LLM
139
139
  // ignored: true -> hidden from the LLM, still shown in the user UI
140
- // The translation preview, activation banner, and failure notices are
141
- // user-facing status/diagnostic parts that must not leak into the LLM
142
- // prompt, so they use synthetic:false + ignored:true.
143
- function createSyntheticTextPart(
144
- sessionID: string,
145
- messageID: string,
146
- text: string,
147
- metadata: Record<string, unknown>,
148
- ): TextPartLike {
149
- return {
150
- id: createSyntheticPartID(),
151
- sessionID,
152
- messageID,
153
- type: "text",
154
- text,
155
- synthetic: false,
156
- ignored: true,
157
- metadata,
158
- }
159
- }
140
+ // both true -> hidden from both (used for metadata-only marker
141
+ // parts like the activation banner that exist purely
142
+ // to carry state across reloads)
143
+ // The plugin's user-facing status text (translation preview, activation
144
+ // banner, failure notice) lives inline on the source-language user part
145
+ // itself rather than as sibling parts, because OpenCode's
146
+ // `UserMessageDisplay` renders only the first non-synthetic text part
147
+ // per user message.
160
148
 
161
149
  // LLM-only text part: hidden from the TUI but the only LLM-visible
162
150
  // representation of the user's source-language text. The original
@@ -337,11 +325,21 @@ export function createHooks(ctx: PluginInput, rawOptions: PluginOptions = {}, de
337
325
  const nextParts: TextPartLike[] = []
338
326
  let eligibleIndex = 0
339
327
  const translationErrors: { part: TextPartLike; error: unknown }[] = []
328
+ // Track the first user-authored text part so the activation
329
+ // banner can be inlined onto it. OpenCode's `UserMessageDisplay`
330
+ // renders only the first non-synthetic text part per user
331
+ // message, so any standalone banner part is never visible in
332
+ // the TUI.
333
+ let firstUserTextPart: (TextPartLike & { text: string }) | undefined
340
334
 
341
335
  for (const part of output.parts as TextPartLike[]) {
342
336
  nextParts.push(part)
343
337
  if (!isUserAuthoredTextPart(part)) continue
344
338
 
339
+ if (firstUserTextPart === undefined) {
340
+ firstUserTextPart = part
341
+ }
342
+
345
343
  const currentEligibleIndex = eligibleIndex
346
344
  eligibleIndex += 1
347
345
  if (part.text.trim().length === 0) continue
@@ -421,13 +419,36 @@ export function createHooks(ctx: PluginInput, rawOptions: PluginOptions = {}, de
421
419
  }
422
420
 
423
421
  if (activatedThisTurn) {
424
- nextParts.push(
425
- createSyntheticTextPart(input.sessionID, output.message.id, createActivationBannerText(options), {
422
+ const bannerText = createActivationBannerText(options)
423
+ // Inline the activation banner into the first user-authored
424
+ // text part so it actually reaches the TUI (see comment above
425
+ // for the single-text-part rendering constraint). The banner
426
+ // sits underneath the inline `→ EN: ...` preview that was
427
+ // appended during translation, giving the user a one-time
428
+ // confirmation that translation mode just turned on.
429
+ if (firstUserTextPart !== undefined) {
430
+ firstUserTextPart.text = `${firstUserTextPart.text}\n\n_${bannerText}_`
431
+ }
432
+ // Also emit the banner as a metadata-only synthetic part so
433
+ // `extractStoredState`'s canonical `translate_role ===
434
+ // "activation_banner"` marker is preserved across reloads.
435
+ // `synthetic:true + ignored:true` keeps it hidden from both
436
+ // the TUI and the LLM serializer; it exists purely as a
437
+ // database row carrying state metadata.
438
+ nextParts.push({
439
+ id: createSyntheticPartID(),
440
+ sessionID: input.sessionID,
441
+ messageID: output.message.id,
442
+ type: "text",
443
+ text: bannerText,
444
+ synthetic: true,
445
+ ignored: true,
446
+ metadata: {
426
447
  ...activeState,
427
448
  translate_role: "activation_banner",
428
449
  translate_spec_version: SPEC_VERSION,
429
- }),
430
- )
450
+ },
451
+ })
431
452
  }
432
453
 
433
454
  output.parts.splice(0, output.parts.length, ...(nextParts as typeof output.parts))