{"repo":"NousResearch/hermes-agent","public_id":"35107","display_label":"TUI clipboard CJK/UTF-8 encoding fixes for PowerShell/WSL2","summary":"A bug report and multiple fix attempts addressing garbled CJK and emoji characters when copying text to the clipboard via PowerShell in TUI, particularly on WSL2. The fixes explore approaches such as base64 encoding to preserve UTF-8 integrity.","subsystem":null,"counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":35222,"merged_at":"2026-05-30T08:40:44+00:00"},"resolution":{"state":"resolved","by":"pr-35222"},"freshness":{"state":"verified","last_verified_at":"2026-05-30T08:40:44+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":35107,"pr_number":35222,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-30T08:40:44+00:00"}],"members":[{"kind":"issue","number":35107,"title":"/copy in TUI produces garbled CJK text on WSL2","url":null,"state":"closed"},{"kind":"pr","number":35122,"title":"fix(tui): preserve utf8 when copying via powershell","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#35124","number":35124}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":35124,"title":"fix(tui): use base64 encoding for PowerShell clipboard writes to preserve UTF-8","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#35222","number":35222}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":35222,"title":"fix(tui): preserve UTF-8 in PowerShell clipboard writes (CJK/emoji)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["ui-tui/src/__tests__/clipboard.test.ts","ui-tui/src/lib/clipboard.ts"],"fix_hunks":[{"file":"ui-tui/src/__tests__/clipboard.test.ts","hunks":["@@ -269,7 +269,14 @@ describe('writeClipboardText', () => {\n       expect.arrayContaining(['-NoProfile', '-NonInteractive']),\n       expect.anything()\n     )\n-    expect(stdin.end).toHaveBeenCalledWith('wsl text')\n+    // PowerShell uses base64-encoded UTF-8 via command argument, not stdin\n+    expect(stdin.end).not.toHaveBeenCalled()\n+    const calledArgs = start.mock.calls[0][1] as string[]\n+    const commandIdx = calledArgs.indexOf('-Command')\n+    expect(commandIdx).toBeGreaterThan(-1)\n+    const script = calledArgs[commandIdx + 1]\n+    expect(script).toContain('FromBase64String')\n+    expect(script).toContain(Buffer.from('wsl text', 'utf8').toString('base64'))\n   })\n \n   it('prefers the Windows clipboard path over wl-copy inside WSLg', async () => {","@@ -300,7 +307,13 @@ describe('writeClipboardText', () => {\n       expect.arrayContaining(['-NoProfile', '-NonInteractive']),\n       expect.anything()\n     )\n-    expect(stdin.end).toHaveBeenCalledWith('wslg text')\n+    // PowerShell uses base64-encoded UTF-8 via command argument, not stdin\n+    expect(stdin.end).not.toHaveBeenCalled()\n+    const calledArgs = start.mock.calls[0][1] as string[]\n+    const commandIdx = calledArgs.indexOf('-Command')\n+    const script = calledArgs[commandIdx + 1]\n+    expect(script).toContain('FromBase64String')\n+    expect(script).toContain(Buffer.from('wslg text', 'utf8').toString('base64'))\n   })\n \n   it('uses PowerShell on Windows', async () => {","@@ -325,5 +338,32 @@ describe('writeClipboardText', () => {\n       expect.arrayContaining(['-NoProfile', '-NonInteractive']),\n       expect.anything()\n     )\n+    // PowerShell uses base64-encoded UTF-8 via command argument, not stdin\n+    expect(stdin.end).not.toHaveBeenCalled()\n+  })\n+\n+  it('preserves CJK text via base64 encoding in PowerShell on WSL', async () => {\n+    const stdin = { end: vi.fn() }\n+\n+    const child = {\n+      once: vi.fn((event: string, cb: (code?: number) => void) => {\n+        if (event === 'close') {\n+          cb(0)\n+        }\n+\n+        return child\n+      }),\n+      stdin\n+    }\n+\n+    const start = vi.fn().mockReturnValue(child)\n+    const cjkText = '你好世界，测试中文 🎉'\n+\n+    await expect(writeClipboardText(cjkText, 'linux', start as any, { WSL_INTEROP: '/tmp/socket' })).resolves.toBe(true)\n+    const calledArgs = start.mock.calls[0][1] as string[]\n+    const commandIdx = calledArgs.indexOf('-Command')\n+    const script = calledArgs[commandIdx + 1]\n+    expect(script).toContain(Buffer.from(cjkText, 'utf8').toString('base64'))\n+    expect(script).toContain('UTF8.GetString')\n   })\n })"]},{"file":"ui-tui/src/lib/clipboard.ts","hunks":["@@ -91,33 +91,42 @@ export async function readClipboardText(\n   return null\n }\n \n+// PowerShell on Windows/WSL decodes piped stdin with the system ANSI code\n+// page (e.g. CP936), not UTF-8, so $input-based writes mangle CJK/emoji. We\n+// instead base64-encode the UTF-8 bytes and pass them as a -Command argument,\n+// decoding with UTF8.GetString — this removes the stdin-encoding variable\n+// entirely (also immune to BOM injection on redirect). PowerShell entries set\n+// stdin=false; every other backend reads UTF-8 stdin natively.\n+type WriteCmd = { args: readonly string[]; cmd: string; stdin: boolean }\n+\n+function _powershellWriteScript(b64: string): string {\n+  return `Set-Clipboard -Value ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${b64}')))`\n+}\n+\n function writeClipboardCommands(\n   platform: NodeJS.Platform,\n   env: NodeJS.ProcessEnv\n-): Array<{ args: readonly string[]; cmd: string }> {\n+): WriteCmd[] {\n   if (platform === 'darwin') {\n-    return [{ cmd: 'pbcopy', args: [] }]\n+    return [{ cmd: 'pbcopy', args: [], stdin: true }]\n   }\n \n   if (platform === 'win32') {\n-    return [{ cmd: 'powershell', args: ['-NoProfile', '-NonInteractive', '-Command', 'Set-Clipboard -Value $input'] }]\n+    return [{ cmd: 'powershell', args: ['-NoProfile', '-NonInteractive'], stdin: false }]\n   }\n \n-  const attempts: Array<{ args: readonly string[]; cmd: string }> = []\n+  const attempts: WriteCmd[] = []\n \n   if (env.WSL_INTEROP || env.WSL_DISTRO_NAME) {\n-    attempts.push({\n-      cmd: 'powershell.exe',\n-      args: ['-NoProfile', '-NonInteractive', '-Command', 'Set-Clipboard -Value $input']\n-    })\n+    attempts.push({ cmd: 'powershell.exe', args: ['-NoProfile', '-NonInteractive'], stdin: false })\n   }\n \n   if (env.WAYLAND_DISPLAY) {\n-    attempts.push({ cmd: 'wl-copy', args: ['--type', 'text/plain'] })\n+    attempts.push({ cmd: 'wl-copy', args: ['--type', 'text/plain'], stdin: true })\n   }\n \n-  attempts.push({ cmd: 'xclip', args: ['-selection', 'clipboard', '-in'] })\n-  attempts.push({ cmd: 'xsel', args: ['--clipboard', '--input'] })\n+  attempts.push({ cmd: 'xclip', args: ['-selection', 'clipboard', '-in'], stdin: true })\n+  attempts.push({ cmd: 'xsel', args: ['--clipboard', '--input'], stdin: true })\n \n   return attempts\n }","@@ -144,14 +153,21 @@ export async function writeClipboardText(\n ): Promise<boolean> {\n   const candidates = writeClipboardCommands(platform, env)\n \n-  for (const { cmd, args } of candidates) {\n+  for (const cmdEntry of candidates) {\n     try {\n       const ok = await new Promise<boolean>(resolve => {\n-        const child = start(cmd, [...args], { stdio: ['pipe', 'ignore', 'ignore'], windowsHide: true })\n-\n-        child.once('error', () => resolve(false))\n-        child.once('close', code => resolve(code === 0))\n-        child.stdin?.end(text)\n+        if (cmdEntry.stdin) {\n+          const child = start(cmdEntry.cmd, [...cmdEntry.args], { stdio: ['pipe', 'ignore', 'ignore'], windowsHide: true })\n+          child.once('error', () => resolve(false))\n+          child.once('close', (code: number | null) => resolve(code === 0))\n+          child.stdin?.end(text)\n+        } else {\n+          const b64 = Buffer.from(text, 'utf8').toString('base64')\n+          const script = _powershellWriteScript(b64)\n+          const child = start(cmdEntry.cmd, [...cmdEntry.args, '-Command', script], { stdio: ['ignore', 'ignore', 'ignore'], windowsHide: true })\n+          child.once('error', () => resolve(false))\n+          child.once('close', (code: number | null) => resolve(code === 0))\n+        }\n       })\n \n       if (ok) {\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}