cc-zh-watcher 0.1.0 → 0.2.1

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 (3) hide show
  1. package/README.md +237 -121
  2. package/dist/cli.js +2185 -389
  3. package/package.json +6 -5
package/dist/cli.js CHANGED
@@ -39237,8 +39237,7 @@ var {
39237
39237
  } = import__.default;
39238
39238
 
39239
39239
  // src/cli.ts
39240
- import { existsSync as existsSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4 } from "node:fs";
39241
- import { dirname as dirname4 } from "node:path";
39240
+ import { existsSync as existsSync8, readFileSync as readFileSync8 } from "node:fs";
39242
39241
  import { createInterface } from "node:readline/promises";
39243
39242
  import { stdin as input, stdout as output } from "node:process";
39244
39243
 
@@ -44790,6 +44789,12 @@ var PROMPT_KEYS = [
44790
44789
  var DEFAULT_BASE_URL = "https://api.openai.com/v1";
44791
44790
  var DEFAULT_MODEL = "gpt-4o-mini";
44792
44791
  var DEFAULT_TIMEOUT = 30;
44792
+ var DEFAULT_NOTIFY_ON_COMPLETE = true;
44793
+ var DEFAULT_TRANSLATE_THINKING = false;
44794
+ var DEFAULT_TRANSLATE_TODOS = true;
44795
+ var DEFAULT_TRANSLATE_AGENT = true;
44796
+ var DEFAULT_CONTEXT_ENABLED = true;
44797
+ var DEFAULT_SHOW_TOOL_CALLS = true;
44793
44798
  function readJsonIfExists(path) {
44794
44799
  if (!existsSync2(path))
44795
44800
  return {};
@@ -44835,10 +44840,23 @@ function loadConfig(opts) {
44835
44840
  base_url: layered.base_url ?? DEFAULT_BASE_URL,
44836
44841
  model: layered.model ?? DEFAULT_MODEL,
44837
44842
  timeout_seconds: layered.timeout_seconds ?? DEFAULT_TIMEOUT,
44843
+ notify_on_complete: validateBool(layered.notify_on_complete, "notify_on_complete", DEFAULT_NOTIFY_ON_COMPLETE),
44844
+ translate_thinking: validateBool(layered.translate_thinking, "translate_thinking", DEFAULT_TRANSLATE_THINKING),
44845
+ translate_todos: validateBool(layered.translate_todos, "translate_todos", DEFAULT_TRANSLATE_TODOS),
44846
+ translate_agent: validateBool(layered.translate_agent, "translate_agent", DEFAULT_TRANSLATE_AGENT),
44847
+ context_enabled: validateBool(layered.context_enabled, "context_enabled", DEFAULT_CONTEXT_ENABLED),
44848
+ show_tool_calls: validateBool(layered.show_tool_calls, "show_tool_calls", DEFAULT_SHOW_TOOL_CALLS),
44838
44849
  prompts: validatePromptMap(layered.prompts, "prompts"),
44839
44850
  prompt_extensions: validatePromptMap(layered.prompt_extensions, "prompt_extensions")
44840
44851
  };
44841
44852
  }
44853
+ function validateBool(raw, fieldName, fallback) {
44854
+ if (raw === undefined || raw === null)
44855
+ return fallback;
44856
+ if (typeof raw === "boolean")
44857
+ return raw;
44858
+ throw new Error(`${fieldName} must be a boolean (got ${typeof raw})`);
44859
+ }
44842
44860
  function validatePromptMap(raw, fieldName) {
44843
44861
  if (raw === undefined || raw === null)
44844
44862
  return;
@@ -44860,6 +44878,30 @@ function validatePromptMap(raw, fieldName) {
44860
44878
  return Object.keys(out).length > 0 ? out : undefined;
44861
44879
  }
44862
44880
 
44881
+ // src/fs-utils.ts
44882
+ import { mkdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
44883
+ import { dirname } from "node:path";
44884
+ import { randomBytes } from "node:crypto";
44885
+ function writeAtomic(path, content, opts = {}) {
44886
+ const { mode, mkdir = true } = opts;
44887
+ if (mkdir) {
44888
+ mkdirSync(dirname(path), { recursive: true });
44889
+ }
44890
+ const tmp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
44891
+ try {
44892
+ const writeOpts = { encoding: "utf-8" };
44893
+ if (mode !== undefined)
44894
+ writeOpts.mode = mode;
44895
+ writeFileSync(tmp, content, writeOpts);
44896
+ renameSync(tmp, path);
44897
+ } catch (err) {
44898
+ try {
44899
+ unlinkSync(tmp);
44900
+ } catch {}
44901
+ throw err;
44902
+ }
44903
+ }
44904
+
44863
44905
  // src/translate.ts
44864
44906
  var ZH_TO_EN_BASE = `You are a professional translator. The user types Chinese instructions for Claude Code (an AI coding assistant CLI), and your job is to translate the Chinese into natural, professional technical English that Claude Code will read as the user's prompt.
44865
44907
 
@@ -44878,26 +44920,60 @@ Translate only the prose around these.
44878
44920
  Style:
44879
44921
  - Concise. Match the source's length; do not pad with explanations the source does not contain.
44880
44922
  - Imperative voice when the source is imperative ("帮我看一下" → "Take a look at"); declarative otherwise.
44881
- - Silently fix obvious typos, missing words, and ungrammatical fragments.
44923
+ - Silently fix obvious typos, missing words, and ungrammatical fragments. Common patterns from a Chinese IME:
44924
+ · 同音/近音字混用: "在" ↔ "再", "的/地/得", "做/作", "象/像", "因为/应为"
44925
+ · 错别字: "测式" → "测试", "克立" → "克隆", "提价" → "提交"
44926
+ · 漏字 / 多字: "一条录" → "一条记录"
44927
+ · 缺主语或谓语: 句子片段 → 完整祈使句
44928
+ ONLY fix when the intent is unambiguous. If a word could legitimately mean either reading (e.g. "在" really meant 在 not 再), translate AS-IS rather than guess. Conservative repair beats confident wrong-fix.
44882
44929
  - If the input is brief or fragmentary, produce a complete grammatical English sentence reflecting the user's likely intent — but do not invent specifics the user did not imply.
44883
44930
  - Aim for the register of a competent engineer writing a message to a colleague: clear, direct, no fluff, no hedging.`;
44884
- var ZH_TO_EN_WITH_CONTEXT_BASE = `You are a professional translator. The user types Chinese instructions for Claude Code (an AI coding assistant CLI). The PRIOR_REPLY block below is Claude Code's previous English reply — use it ONLY to disambiguate pronouns ("那个" / "你说的"), expand elliptical references ("再加一条" → "add one more entry"), and silently correct obvious typos. Do NOT translate or repeat the prior reply itself.
44931
+ var ZH_TO_EN_WITH_CONTEXT_BASE = `You are a professional translator AND a quick sanity-check filter. The user types Chinese instructions for Claude Code (an AI coding assistant CLI). The PRIOR_REPLY block below is Claude Code's previous English reply — use it to disambiguate pronouns ("那个" / "你说的"), expand elliptical references ("再加一条" → "add one more entry"), silently correct obvious typos, AND detect when the user's input is unlikely to be actionable in this context.
44885
44932
 
44886
- Output ONLY the English translation of the NEW USER INPUT block. No preface, no quotation marks, no commentary, no notes about the translation.
44933
+ Output format (MANDATORY two parts separated by ONE blank line):
44887
44934
 
44888
- Preserve verbatim (NEVER translate):
44935
+ Part 1 first line, status:
44936
+ "OK" — normal: the user's input is clear given the prior reply
44937
+ "WARN: <说明>" — flag a likely problem; ONE Chinese sentence (≤30 字)
44938
+
44939
+ (single blank line)
44940
+
44941
+ Part 2 — the English translation of the NEW USER INPUT, exactly as
44942
+ you would translate it. Even when WARN is set, still translate as
44943
+ faithfully as possible — the user may decide to send it anyway.
44944
+
44945
+ WARN ONLY when confident a Chinese-speaking engineer reading the
44946
+ prior reply + the user's input would also see a problem. Triggers:
44947
+ - The user refers to something not in PRIOR_REPLY ("那个文件" but
44948
+ no file mentioned; "刚才说的方案" but Claude proposed nothing)
44949
+ - The user answers ambiguously to a clear multiple-choice question
44950
+ ("好的" / "随便" when Claude asked "A or B?")
44951
+ - The user's input contradicts the topic of PRIOR_REPLY (different
44952
+ subject, mismatched verb)
44953
+ - The input is garbled enough that even with context the
44954
+ translation is just a guess
44955
+
44956
+ Output OK when in doubt. Spurious warnings slow the user down for
44957
+ nothing; missed warnings just route through Claude Code, which can
44958
+ ask for clarification itself. Do NOT WARN merely because the input
44959
+ is short — short imperatives ("继续", "好") after a clear prior
44960
+ reply are normal.
44961
+
44962
+ Translate only the prose; preserve verbatim:
44889
44963
  - code blocks (fenced or indented), code fragments, anything in backticks
44890
44964
  - identifier names, file paths, glob patterns, URLs
44891
44965
  - command-line snippets, flags, environment variable names, git refs
44892
44966
  - error messages quoted in the source
44893
44967
  - numbers, units, version strings, hex / hash values
44894
44968
 
44895
- Translate only the prose around these.
44896
-
44897
44969
  Style:
44898
44970
  - Concise. Match the new input's length; do not pad with explanations.
44899
44971
  - Imperative voice when the source is imperative; declarative otherwise.
44900
- - Silently fix obvious typos and ungrammatical fragments.
44972
+ - Silently fix obvious typos, missing words, and ungrammatical fragments. Common patterns from a Chinese IME:
44973
+ · 同音/近音字混用: "在" ↔ "再", "的/地/得", "做/作", "象/像"
44974
+ · 错别字: "测式" → "测试", "克立" → "克隆", "提价" → "提交"
44975
+ · 漏字 / 多字: "一条录" → "一条记录"
44976
+ The PRIOR_REPLY block is a strong disambiguation signal — if Claude just asked "Should I commit and push?" and the user types "提价 一下", "提价" is almost certainly "提交" given the context. ONLY fix when the intent is unambiguous; if uncertain, translate AS-IS.
44901
44977
  - If the input is fragmentary, produce a complete grammatical English sentence reflecting the user's likely intent — using the prior reply as the disambiguation source, not as content to add.
44902
44978
  - Aim for the register of a competent engineer writing a message to a colleague.`;
44903
44979
  var EN_TO_ZH_BASE = `You are a professional translator. Claude Code (an AI coding assistant CLI) replies in English to a Chinese-speaking developer; your job is to render the English faithfully into natural, professional Simplified Chinese.
@@ -44958,10 +45034,12 @@ class TranslateError extends Error {
44958
45034
  this.elapsed_ms = opts?.elapsed_ms ?? 0;
44959
45035
  }
44960
45036
  }
44961
- async function translate(text, direction, config, contextText) {
45037
+ async function translate(text, direction, config, options) {
44962
45038
  if (!text.trim()) {
44963
45039
  throw new TranslateError("empty input", "config", { code: "config" });
44964
45040
  }
45041
+ const contextText = options?.contextText;
45042
+ const onDelta = options?.onDelta;
44965
45043
  let systemPrompt;
44966
45044
  let userMsg;
44967
45045
  if (direction === "zh-to-en" && contextText && contextText.trim()) {
@@ -44976,20 +45054,47 @@ ${text}`;
44976
45054
  systemPrompt = resolvePrompt(direction, config);
44977
45055
  userMsg = text;
44978
45056
  }
44979
- const body = {
45057
+ const baseBody = {
44980
45058
  model: config.model,
44981
45059
  messages: [
44982
45060
  { role: "system", content: systemPrompt },
44983
45061
  { role: "user", content: userMsg }
44984
45062
  ],
44985
- temperature: 0.2,
44986
- stream: false
45063
+ temperature: 0.2
44987
45064
  };
44988
45065
  const url = config.base_url.replace(/\/$/, "") + "/chat/completions";
44989
45066
  const controller = new AbortController;
44990
45067
  const timer = setTimeout(() => controller.abort(), config.timeout_seconds * 1000);
45068
+ const onExternalAbort = () => controller.abort();
45069
+ if (options?.signal) {
45070
+ if (options.signal.aborted)
45071
+ controller.abort();
45072
+ else
45073
+ options.signal.addEventListener("abort", onExternalAbort, { once: true });
45074
+ }
44991
45075
  const startedAt = performance.now();
44992
45076
  const elapsed = () => Math.round(performance.now() - startedAt);
45077
+ const wantParse = options?.parseConcernHeader === true;
45078
+ try {
45079
+ const result2 = onDelta ? await runStreaming(url, baseBody, config, controller.signal, onDelta, elapsed) : await runNonStreaming(url, baseBody, config, controller.signal, elapsed);
45080
+ if (!wantParse)
45081
+ return result2;
45082
+ const parsed = parseConcernHeader(result2.content);
45083
+ if (parsed.concern === null && parsed.translation === result2.content) {
45084
+ return result2;
45085
+ }
45086
+ return {
45087
+ ...result2,
45088
+ content: parsed.translation,
45089
+ ...parsed.concern !== null ? { concern: parsed.concern } : {}
45090
+ };
45091
+ } finally {
45092
+ clearTimeout(timer);
45093
+ options?.signal?.removeEventListener("abort", onExternalAbort);
45094
+ }
45095
+ }
45096
+ async function runNonStreaming(url, baseBody, config, signal, elapsed) {
45097
+ const body = { ...baseBody, stream: false };
44993
45098
  let resp;
44994
45099
  try {
44995
45100
  resp = await fetch(url, {
@@ -44999,12 +45104,11 @@ ${text}`;
44999
45104
  Authorization: `Bearer ${config.api_key}`
45000
45105
  },
45001
45106
  body: JSON.stringify(body),
45002
- signal: controller.signal
45107
+ signal
45003
45108
  });
45004
45109
  } catch (err) {
45005
- clearTimeout(timer);
45006
45110
  const msg = err instanceof Error ? err.message : String(err);
45007
- if (msg.includes("aborted")) {
45111
+ if (isAbortLike(msg)) {
45008
45112
  throw new TranslateError(`request timed out after ${config.timeout_seconds}s`, "network", { code: "timeout", elapsed_ms: elapsed() });
45009
45113
  }
45010
45114
  throw new TranslateError(`network error: ${msg}`, "network", {
@@ -45012,7 +45116,6 @@ ${text}`;
45012
45116
  elapsed_ms: elapsed()
45013
45117
  });
45014
45118
  }
45015
- clearTimeout(timer);
45016
45119
  let json;
45017
45120
  try {
45018
45121
  json = await resp.json();
@@ -45055,6 +45158,163 @@ ${text}`;
45055
45158
  const usage = parseUsage(usageRaw);
45056
45159
  return { content, elapsed_ms: elapsed(), usage };
45057
45160
  }
45161
+ async function runStreaming(url, baseBody, config, signal, onDelta, elapsed) {
45162
+ const body = {
45163
+ ...baseBody,
45164
+ stream: true,
45165
+ stream_options: { include_usage: true }
45166
+ };
45167
+ let resp;
45168
+ try {
45169
+ resp = await fetch(url, {
45170
+ method: "POST",
45171
+ headers: {
45172
+ "Content-Type": "application/json",
45173
+ Accept: "text/event-stream",
45174
+ Authorization: `Bearer ${config.api_key}`
45175
+ },
45176
+ body: JSON.stringify(body),
45177
+ signal
45178
+ });
45179
+ } catch (err) {
45180
+ const msg = err instanceof Error ? err.message : String(err);
45181
+ if (isAbortLike(msg)) {
45182
+ throw new TranslateError(`request timed out after ${config.timeout_seconds}s`, "network", { code: "timeout", elapsed_ms: elapsed() });
45183
+ }
45184
+ throw new TranslateError(`network error: ${msg}`, "network", {
45185
+ code: "network",
45186
+ elapsed_ms: elapsed()
45187
+ });
45188
+ }
45189
+ if (!resp.ok) {
45190
+ let detail = `HTTP ${resp.status} ${resp.statusText}`;
45191
+ try {
45192
+ const text = await resp.text();
45193
+ try {
45194
+ const j = JSON.parse(text);
45195
+ const e = j.error;
45196
+ const msg = typeof e === "string" ? e : e?.message ?? null;
45197
+ if (msg)
45198
+ detail = msg;
45199
+ } catch {
45200
+ if (text)
45201
+ detail = text.slice(0, 200);
45202
+ }
45203
+ } catch {}
45204
+ throw new TranslateError(`API error: ${detail}`, "api", {
45205
+ code: `HTTP ${resp.status}`,
45206
+ elapsed_ms: elapsed()
45207
+ });
45208
+ }
45209
+ if (!resp.body) {
45210
+ throw new TranslateError("response has no body", "parse", {
45211
+ code: "parse",
45212
+ elapsed_ms: elapsed()
45213
+ });
45214
+ }
45215
+ const reader = resp.body.getReader();
45216
+ const decoder = new TextDecoder("utf-8");
45217
+ let buf = "";
45218
+ let content = "";
45219
+ let usage = null;
45220
+ let inlineError = null;
45221
+ try {
45222
+ while (true) {
45223
+ const { done, value } = await reader.read();
45224
+ if (done)
45225
+ break;
45226
+ buf += decoder.decode(value, { stream: true });
45227
+ for (;; ) {
45228
+ const m = /\r?\n\r?\n/.exec(buf);
45229
+ if (!m)
45230
+ break;
45231
+ const event = buf.slice(0, m.index);
45232
+ buf = buf.slice(m.index + m[0].length);
45233
+ for (const rawLine of event.split(/\r?\n/)) {
45234
+ const line = rawLine.trimEnd();
45235
+ if (!line || line.startsWith(":"))
45236
+ continue;
45237
+ if (!line.startsWith("data:"))
45238
+ continue;
45239
+ const payload = line.slice(5).trim();
45240
+ if (!payload || payload === "[DONE]")
45241
+ continue;
45242
+ let chunk2;
45243
+ try {
45244
+ chunk2 = JSON.parse(payload);
45245
+ } catch {
45246
+ continue;
45247
+ }
45248
+ const errObj = chunk2["error"];
45249
+ if (errObj !== undefined && errObj !== null) {
45250
+ inlineError = typeof errObj === "string" ? errObj : errObj.message ?? JSON.stringify(errObj);
45251
+ continue;
45252
+ }
45253
+ if (chunk2["usage"] !== undefined) {
45254
+ const u = parseUsage(chunk2["usage"]);
45255
+ if (u)
45256
+ usage = u;
45257
+ }
45258
+ const choices = chunk2["choices"];
45259
+ if (!Array.isArray(choices) || choices.length === 0)
45260
+ continue;
45261
+ const delta = choices[0].delta;
45262
+ const piece = delta?.content;
45263
+ if (typeof piece === "string" && piece.length > 0) {
45264
+ content += piece;
45265
+ onDelta(piece);
45266
+ }
45267
+ }
45268
+ }
45269
+ }
45270
+ } catch (err) {
45271
+ const msg = err instanceof Error ? err.message : String(err);
45272
+ if (isAbortLike(msg)) {
45273
+ throw new TranslateError(`request timed out after ${config.timeout_seconds}s`, "network", { code: "timeout", elapsed_ms: elapsed() });
45274
+ }
45275
+ throw new TranslateError(`stream read error: ${msg}`, "network", {
45276
+ code: "network",
45277
+ elapsed_ms: elapsed()
45278
+ });
45279
+ }
45280
+ if (inlineError) {
45281
+ throw new TranslateError(`API error: ${inlineError}`, "api", {
45282
+ code: "api",
45283
+ elapsed_ms: elapsed()
45284
+ });
45285
+ }
45286
+ if (content.length === 0) {
45287
+ throw new TranslateError("empty completion content", "parse", {
45288
+ code: "parse",
45289
+ elapsed_ms: elapsed()
45290
+ });
45291
+ }
45292
+ return { content, elapsed_ms: elapsed(), usage };
45293
+ }
45294
+ function isAbortLike(msg) {
45295
+ const m = msg.toLowerCase();
45296
+ return m.includes("abort");
45297
+ }
45298
+ function parseConcernHeader(content) {
45299
+ const newlineIdx = content.indexOf(`
45300
+ `);
45301
+ if (newlineIdx === -1) {
45302
+ return { concern: null, translation: content };
45303
+ }
45304
+ const firstLine = content.slice(0, newlineIdx).trim();
45305
+ let concern;
45306
+ if (firstLine === "OK") {
45307
+ concern = null;
45308
+ } else if (firstLine.startsWith("WARN:")) {
45309
+ concern = firstLine.slice("WARN:".length).trim();
45310
+ if (!concern)
45311
+ concern = null;
45312
+ } else {
45313
+ return { concern: null, translation: content };
45314
+ }
45315
+ const rest2 = content.slice(newlineIdx + 1).replace(/^\s+/, "");
45316
+ return { concern, translation: rest2 };
45317
+ }
45058
45318
  function parseUsage(raw) {
45059
45319
  if (!raw || typeof raw !== "object")
45060
45320
  return null;
@@ -45072,8 +45332,8 @@ function parseUsage(raw) {
45072
45332
  }
45073
45333
 
45074
45334
  // src/state.ts
45075
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "node:fs";
45076
- import { dirname, join as join3 } from "node:path";
45335
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
45336
+ import { join as join3 } from "node:path";
45077
45337
  function stateFilePath(projectDir) {
45078
45338
  return join3(projectDir, ".claude", "cache", "cc-zh-watcher-state.json");
45079
45339
  }
@@ -45093,10 +45353,8 @@ function readState(projectDir) {
45093
45353
  }
45094
45354
  }
45095
45355
  function writeState(projectDir, state) {
45096
- const path = stateFilePath(projectDir);
45097
- mkdirSync(dirname(path), { recursive: true });
45098
- writeFileSync(path, JSON.stringify(state, null, 2) + `
45099
- `, "utf-8");
45356
+ writeAtomic(stateFilePath(projectDir), JSON.stringify(state, null, 2) + `
45357
+ `);
45100
45358
  }
45101
45359
 
45102
45360
  // src/transcript.ts
@@ -45131,7 +45389,8 @@ function listSessions(projectDir) {
45131
45389
  last_activity_at: null,
45132
45390
  turn_count: 0,
45133
45391
  first_message: null,
45134
- mtime_ms: stat.mtimeMs
45392
+ mtime_ms: stat.mtimeMs,
45393
+ cwd: null
45135
45394
  });
45136
45395
  }
45137
45396
  }
@@ -45151,6 +45410,7 @@ function parseSessionMeta(path) {
45151
45410
  let last_activity_at = null;
45152
45411
  let turn_count = 0;
45153
45412
  let first_message = null;
45413
+ let cwd2 = null;
45154
45414
  for (const line of lines) {
45155
45415
  if (!line.trim())
45156
45416
  continue;
@@ -45166,6 +45426,9 @@ function parseSessionMeta(path) {
45166
45426
  started_at = ts;
45167
45427
  last_activity_at = ts;
45168
45428
  }
45429
+ if (cwd2 === null && typeof obj["cwd"] === "string") {
45430
+ cwd2 = obj["cwd"];
45431
+ }
45169
45432
  if (obj["type"] === "user") {
45170
45433
  const message = obj["message"];
45171
45434
  const content = message?.content;
@@ -45183,7 +45446,8 @@ function parseSessionMeta(path) {
45183
45446
  last_activity_at,
45184
45447
  turn_count,
45185
45448
  first_message,
45186
- mtime_ms: stat.mtimeMs
45449
+ mtime_ms: stat.mtimeMs,
45450
+ cwd: cwd2
45187
45451
  };
45188
45452
  }
45189
45453
  function extractRealPromptText(content) {
@@ -45248,8 +45512,7 @@ import {
45248
45512
  existsSync as existsSync5,
45249
45513
  mkdirSync as mkdirSync2,
45250
45514
  readFileSync as readFileSync5,
45251
- unlinkSync,
45252
- writeFileSync as writeFileSync2
45515
+ unlinkSync as unlinkSync2
45253
45516
  } from "node:fs";
45254
45517
  import { dirname as dirname2, join as join5 } from "node:path";
45255
45518
  var USER_INPUT_FILENAME = "user-input.md";
@@ -45274,10 +45537,10 @@ function slashCommandPath(projectDir) {
45274
45537
  }
45275
45538
  function writeUserInput(projectDir, englishText) {
45276
45539
  const path = userInputPath(projectDir);
45277
- mkdirSync2(dirname2(path), { recursive: true });
45278
- writeFileSync2(path, englishText.endsWith(`
45540
+ const body = englishText.endsWith(`
45279
45541
  `) ? englishText : englishText + `
45280
- `, "utf-8");
45542
+ `;
45543
+ writeAtomic(path, body);
45281
45544
  return path;
45282
45545
  }
45283
45546
  function appendHistory(projectDir, entry) {
@@ -45289,8 +45552,7 @@ function appendHistory(projectDir, entry) {
45289
45552
  function installSlashCommand(projectDir) {
45290
45553
  const target = slashCommandPath(projectDir);
45291
45554
  const existed = existsSync5(target);
45292
- mkdirSync2(dirname2(target), { recursive: true });
45293
- writeFileSync2(target, SLASH_COMMAND_TEMPLATE, "utf-8");
45555
+ writeAtomic(target, SLASH_COMMAND_TEMPLATE);
45294
45556
  return { installedPath: target, existed };
45295
45557
  }
45296
45558
  function uninstallSlashCommand(projectDir, opts) {
@@ -45298,7 +45560,7 @@ function uninstallSlashCommand(projectDir, opts) {
45298
45560
  const skipped = [];
45299
45561
  const sc = slashCommandPath(projectDir);
45300
45562
  if (existsSync5(sc)) {
45301
- unlinkSync(sc);
45563
+ unlinkSync2(sc);
45302
45564
  removed.push(sc);
45303
45565
  } else {
45304
45566
  skipped.push(sc);
@@ -45306,7 +45568,7 @@ function uninstallSlashCommand(projectDir, opts) {
45306
45568
  if (opts?.wipeCache) {
45307
45569
  for (const p of [userInputPath(projectDir), historyPath(projectDir)]) {
45308
45570
  if (existsSync5(p)) {
45309
- unlinkSync(p);
45571
+ unlinkSync2(p);
45310
45572
  removed.push(p);
45311
45573
  } else {
45312
45574
  skipped.push(p);
@@ -45317,9 +45579,8 @@ function uninstallSlashCommand(projectDir, opts) {
45317
45579
  }
45318
45580
 
45319
45581
  // src/app.tsx
45320
- var import_react29 = __toESM(require_react(), 1);
45321
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
45322
- import { dirname as dirname3 } from "node:path";
45582
+ var import_react30 = __toESM(require_react(), 1);
45583
+ import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
45323
45584
 
45324
45585
  // src/tail.ts
45325
45586
  import {
@@ -45419,54 +45680,337 @@ class TranscriptTail {
45419
45680
  for (const line of lines) {
45420
45681
  if (!line.trim())
45421
45682
  continue;
45422
- const ev = parseAssistantText(line);
45423
- if (ev)
45424
- events.push(ev);
45683
+ events.push(...parseAssistantContent(line));
45425
45684
  }
45426
45685
  for (const ev of events.slice(-n)) {
45427
- this.handlers.onText(ev);
45686
+ this.handlers.onContent(ev);
45428
45687
  }
45429
45688
  } catch (err) {
45430
45689
  this.handlers.onError?.(err);
45431
45690
  }
45432
45691
  }
45433
45692
  tryEmit(line) {
45434
- const ev = parseAssistantText(line);
45435
- if (ev)
45436
- this.handlers.onText(ev);
45693
+ for (const ev of parseAssistantContent(line)) {
45694
+ this.handlers.onContent(ev);
45695
+ }
45437
45696
  }
45438
45697
  }
45439
- function parseAssistantText(line) {
45698
+ function parseAssistantContent(line) {
45440
45699
  let obj;
45441
45700
  try {
45442
45701
  obj = JSON.parse(line);
45443
45702
  } catch {
45444
- return null;
45703
+ return [];
45704
+ }
45705
+ if (obj["type"] === "user") {
45706
+ return parseUserToolResults(obj);
45445
45707
  }
45446
45708
  if (obj["type"] !== "assistant")
45447
- return null;
45709
+ return [];
45448
45710
  const message = obj["message"];
45449
45711
  const content = message?.content;
45450
45712
  if (!Array.isArray(content))
45451
- return null;
45713
+ return [];
45452
45714
  const textParts = [];
45715
+ const thinkingParts = [];
45716
+ const questions = [];
45717
+ const todos = [];
45718
+ const agents = [];
45719
+ const rawTools = [];
45453
45720
  for (const entry of content) {
45454
- if (entry && typeof entry === "object" && entry.type === "text" && typeof entry.text === "string") {
45455
- textParts.push(entry.text);
45721
+ if (!entry || typeof entry !== "object")
45722
+ continue;
45723
+ const e = entry;
45724
+ if (e["type"] === "text" && typeof e["text"] === "string" && e["text"].length > 0) {
45725
+ textParts.push(e["text"]);
45726
+ } else if (e["type"] === "thinking" && typeof e["thinking"] === "string" && e["thinking"].length > 0) {
45727
+ thinkingParts.push(e["thinking"]);
45728
+ } else if (e["type"] === "tool_use") {
45729
+ const toolId = typeof e["id"] === "string" ? e["id"] : null;
45730
+ const name = e["name"];
45731
+ if (!toolId || typeof name !== "string")
45732
+ continue;
45733
+ if (name === "AskUserQuestion") {
45734
+ const payload = parseQuestionInput(e["input"]);
45735
+ if (payload)
45736
+ questions.push({ id: toolId, payload });
45737
+ } else if (name === "TodoWrite") {
45738
+ const payload = parseTodoInput(e["input"]);
45739
+ if (payload)
45740
+ todos.push({ id: toolId, payload });
45741
+ } else if (name === "Agent" || name === "Task") {
45742
+ const payload = parseAgentInput(e["input"]);
45743
+ if (payload)
45744
+ agents.push({ id: toolId, payload });
45745
+ } else {
45746
+ rawTools.push({
45747
+ id: toolId,
45748
+ payload: { name, input: e["input"] }
45749
+ });
45750
+ }
45456
45751
  }
45457
45752
  }
45458
- if (textParts.length === 0)
45459
- return null;
45753
+ if (textParts.length === 0 && thinkingParts.length === 0 && questions.length === 0 && todos.length === 0 && agents.length === 0 && rawTools.length === 0) {
45754
+ return [];
45755
+ }
45460
45756
  const ts = typeof obj["timestamp"] === "string" ? obj["timestamp"] : new Date().toISOString();
45461
45757
  const uuid = typeof obj["uuid"] === "string" ? obj["uuid"] : null;
45462
- const id = uuid ?? `${ts}-${textParts[0].slice(0, 16)}`;
45463
- return {
45464
- timestamp: ts,
45465
- text: textParts.join(`
45758
+ const out = [];
45759
+ if (textParts.length > 0) {
45760
+ const text = textParts.join(`
45466
45761
 
45467
- `),
45468
- id
45469
- };
45762
+ `);
45763
+ out.push({
45764
+ kind: "text",
45765
+ timestamp: ts,
45766
+ text,
45767
+ id: uuid ?? `${ts}-${textParts[0].slice(0, 16)}`
45768
+ });
45769
+ }
45770
+ if (thinkingParts.length > 0) {
45771
+ const text = thinkingParts.join(`
45772
+
45773
+ `);
45774
+ out.push({
45775
+ kind: "thinking",
45776
+ timestamp: ts,
45777
+ text,
45778
+ id: uuid ? `${uuid}#thinking` : `${ts}-thinking-${thinkingParts[0].slice(0, 16)}`
45779
+ });
45780
+ }
45781
+ for (const q of questions) {
45782
+ out.push({
45783
+ kind: "question",
45784
+ timestamp: ts,
45785
+ id: q.id,
45786
+ question: q.payload
45787
+ });
45788
+ }
45789
+ for (const t of todos) {
45790
+ out.push({
45791
+ kind: "todo",
45792
+ timestamp: ts,
45793
+ id: t.id,
45794
+ todo: t.payload
45795
+ });
45796
+ }
45797
+ for (const a of agents) {
45798
+ out.push({
45799
+ kind: "agent",
45800
+ timestamp: ts,
45801
+ id: a.id,
45802
+ agent: a.payload
45803
+ });
45804
+ }
45805
+ for (const t of rawTools) {
45806
+ out.push({
45807
+ kind: "tool_use",
45808
+ timestamp: ts,
45809
+ id: t.id,
45810
+ toolUse: t.payload
45811
+ });
45812
+ }
45813
+ return out;
45814
+ }
45815
+ function parseUserToolResults(obj) {
45816
+ const message = obj["message"];
45817
+ const content = message?.content;
45818
+ if (!Array.isArray(content))
45819
+ return [];
45820
+ const ts = typeof obj["timestamp"] === "string" ? obj["timestamp"] : new Date().toISOString();
45821
+ const out = [];
45822
+ for (const entry of content) {
45823
+ if (!entry || typeof entry !== "object")
45824
+ continue;
45825
+ const e = entry;
45826
+ if (e["type"] !== "tool_result")
45827
+ continue;
45828
+ const toolUseId = typeof e["tool_use_id"] === "string" ? e["tool_use_id"] : null;
45829
+ if (!toolUseId)
45830
+ continue;
45831
+ out.push({
45832
+ kind: "tool_result",
45833
+ timestamp: ts,
45834
+ id: `${toolUseId}#result`,
45835
+ toolResult: {
45836
+ tool_use_id: toolUseId,
45837
+ content: e["content"],
45838
+ isError: e["is_error"] === true
45839
+ }
45840
+ });
45841
+ }
45842
+ return out;
45843
+ }
45844
+ function parseQuestionInput(raw) {
45845
+ if (!raw || typeof raw !== "object")
45846
+ return null;
45847
+ const r = raw;
45848
+ const rawQs = r["questions"];
45849
+ if (!Array.isArray(rawQs) || rawQs.length === 0)
45850
+ return null;
45851
+ const questions = [];
45852
+ for (const rq of rawQs) {
45853
+ if (!rq || typeof rq !== "object")
45854
+ continue;
45855
+ const q = rq;
45856
+ const question = typeof q["question"] === "string" ? q["question"] : "";
45857
+ const header = typeof q["header"] === "string" ? q["header"] : "";
45858
+ const multiSelect = q["multiSelect"] === true;
45859
+ const rawOpts = q["options"];
45860
+ if (!Array.isArray(rawOpts) || rawOpts.length === 0)
45861
+ continue;
45862
+ const options = [];
45863
+ for (const ro of rawOpts) {
45864
+ if (!ro || typeof ro !== "object")
45865
+ continue;
45866
+ const o = ro;
45867
+ const label = typeof o["label"] === "string" ? o["label"] : "";
45868
+ const description = typeof o["description"] === "string" ? o["description"] : "";
45869
+ const preview = typeof o["preview"] === "string" ? o["preview"] : undefined;
45870
+ const opt = { label, description };
45871
+ if (preview !== undefined)
45872
+ opt.preview = preview;
45873
+ options.push(opt);
45874
+ }
45875
+ if (options.length === 0)
45876
+ continue;
45877
+ questions.push({ question, header, multiSelect, options });
45878
+ }
45879
+ if (questions.length === 0)
45880
+ return null;
45881
+ return { questions };
45882
+ }
45883
+ function parseTodoInput(raw) {
45884
+ if (!raw || typeof raw !== "object")
45885
+ return null;
45886
+ const r = raw;
45887
+ const rawTodos = r["todos"];
45888
+ if (!Array.isArray(rawTodos) || rawTodos.length === 0)
45889
+ return null;
45890
+ const todos = [];
45891
+ for (const rt of rawTodos) {
45892
+ if (!rt || typeof rt !== "object")
45893
+ continue;
45894
+ const t = rt;
45895
+ const content = typeof t["content"] === "string" ? t["content"] : "";
45896
+ const activeForm = typeof t["activeForm"] === "string" ? t["activeForm"] : "";
45897
+ const rawStatus = t["status"];
45898
+ const status = rawStatus === "in_progress" || rawStatus === "completed" ? rawStatus : "pending";
45899
+ if (!content && !activeForm)
45900
+ continue;
45901
+ todos.push({ content, activeForm, status });
45902
+ }
45903
+ if (todos.length === 0)
45904
+ return null;
45905
+ return { todos };
45906
+ }
45907
+ function parseAgentInput(raw) {
45908
+ if (!raw || typeof raw !== "object")
45909
+ return null;
45910
+ const r = raw;
45911
+ const description = typeof r["description"] === "string" ? r["description"] : "";
45912
+ const prompt = typeof r["prompt"] === "string" ? r["prompt"] : "";
45913
+ const subagent_type = typeof r["subagent_type"] === "string" ? r["subagent_type"] : "";
45914
+ if (!description && !prompt)
45915
+ return null;
45916
+ return { description, prompt, subagent_type };
45917
+ }
45918
+
45919
+ // src/notify.ts
45920
+ var BEL2 = "\x07";
45921
+ function ringBell() {
45922
+ try {
45923
+ process.stderr.write(BEL2);
45924
+ } catch {}
45925
+ }
45926
+
45927
+ class DebouncedBell {
45928
+ delayMs;
45929
+ playFn;
45930
+ timer = null;
45931
+ constructor(delayMs, playFn = ringBell) {
45932
+ this.delayMs = delayMs;
45933
+ this.playFn = playFn;
45934
+ }
45935
+ schedule() {
45936
+ if (this.timer)
45937
+ clearTimeout(this.timer);
45938
+ this.timer = setTimeout(() => {
45939
+ this.timer = null;
45940
+ try {
45941
+ this.playFn();
45942
+ } catch {}
45943
+ }, this.delayMs);
45944
+ }
45945
+ cancel() {
45946
+ if (this.timer) {
45947
+ clearTimeout(this.timer);
45948
+ this.timer = null;
45949
+ }
45950
+ }
45951
+ get pending() {
45952
+ return this.timer !== null;
45953
+ }
45954
+ }
45955
+
45956
+ // src/queue.ts
45957
+ class SerialQueue {
45958
+ chain = Promise.resolve();
45959
+ pendingCount = 0;
45960
+ enqueue(task) {
45961
+ this.pendingCount += 1;
45962
+ const wrapped = async () => {
45963
+ try {
45964
+ return await task();
45965
+ } finally {
45966
+ this.pendingCount -= 1;
45967
+ }
45968
+ };
45969
+ const next = this.chain.then(wrapped, wrapped);
45970
+ this.chain = next.catch(() => {
45971
+ return;
45972
+ });
45973
+ return next;
45974
+ }
45975
+ get pending() {
45976
+ return this.pendingCount;
45977
+ }
45978
+ }
45979
+
45980
+ // src/lang-detect.ts
45981
+ function cjkRatio(s) {
45982
+ let cjk = 0;
45983
+ let counted = 0;
45984
+ for (const ch of s) {
45985
+ const cp = ch.codePointAt(0);
45986
+ if (cp === undefined)
45987
+ continue;
45988
+ if (cp <= 32 || cp === 127)
45989
+ continue;
45990
+ if (cp >= 33 && cp <= 47)
45991
+ continue;
45992
+ if (cp >= 58 && cp <= 64)
45993
+ continue;
45994
+ if (cp >= 91 && cp <= 96)
45995
+ continue;
45996
+ if (cp >= 123 && cp <= 126)
45997
+ continue;
45998
+ counted += 1;
45999
+ if (cp >= 19968 && cp <= 40959 || cp >= 13312 && cp <= 19903 || cp >= 131072 && cp <= 173791 || cp >= 12288 && cp <= 12351 || cp >= 65280 && cp <= 65519) {
46000
+ cjk += 1;
46001
+ }
46002
+ }
46003
+ return counted === 0 ? 0 : cjk / counted;
46004
+ }
46005
+ function isProbablyChinese(s, threshold = 0.3) {
46006
+ return cjkRatio(s) >= threshold;
46007
+ }
46008
+
46009
+ // src/translatable.ts
46010
+ function seedString(en) {
46011
+ if (!en.trim() || isProbablyChinese(en))
46012
+ return { en, zh: en };
46013
+ return { en, zh: "" };
45470
46014
  }
45471
46015
 
45472
46016
  // src/components/status-bar.tsx
@@ -45491,7 +46035,7 @@ function ContextBadge({
45491
46035
  bold: true
45492
46036
  }, "off"), /* @__PURE__ */ import_react22.default.createElement(Text, {
45493
46037
  dimColor: true
45494
- }, " (Ctrl+T to enable)"));
46038
+ }, " (Ctrl+E toggle)"));
45495
46039
  }
45496
46040
  if (size2 === 0) {
45497
46041
  return /* @__PURE__ */ import_react22.default.createElement(Text, null, "ctx:", " ", /* @__PURE__ */ import_react22.default.createElement(Text, {
@@ -45506,7 +46050,7 @@ function ContextBadge({
45506
46050
  bold: true
45507
46051
  }, "on"), /* @__PURE__ */ import_react22.default.createElement(Text, {
45508
46052
  dimColor: true
45509
- }, " (", size2, " chars · Ctrl+T to disable)"));
46053
+ }, " (", size2, " chars)"));
45510
46054
  }
45511
46055
  function formatElapsed(ms) {
45512
46056
  if (ms < 1000)
@@ -45726,7 +46270,7 @@ function InputPane({
45726
46270
  flexDirection: "column"
45727
46271
  }, /* @__PURE__ */ import_react24.default.createElement(Text, {
45728
46272
  dimColor: true
45729
- }, "中文输入(Enter 翻译 · Ctrl+T 切换 context · Ctrl+E 设置 · Ctrl+P/N 历史 · Ctrl+L 清空 · Ctrl+C 退出)"), /* @__PURE__ */ import_react24.default.createElement(Box_default, null, /* @__PURE__ */ import_react24.default.createElement(Text, {
46273
+ }, "中文输入(Enter 翻译 · Ctrl+E 设置 · Esc 清空 · Ctrl+C 退出)"), /* @__PURE__ */ import_react24.default.createElement(Box_default, null, /* @__PURE__ */ import_react24.default.createElement(Text, {
45730
46274
  color: "green",
45731
46275
  bold: true
45732
46276
  }, ">", " "), disabled ? /* @__PURE__ */ import_react24.default.createElement(Text, {
@@ -45741,7 +46285,13 @@ function InputPane({
45741
46285
 
45742
46286
  // src/components/preview-pane.tsx
45743
46287
  var import_react25 = __toESM(require_react(), 1);
45744
- function PreviewPane({ text, translating, error }) {
46288
+ function PreviewPane({
46289
+ text,
46290
+ translating,
46291
+ streamingText,
46292
+ error,
46293
+ concern
46294
+ }) {
45745
46295
  let body;
45746
46296
  let borderColor;
45747
46297
  if (error) {
@@ -45756,8 +46306,36 @@ function PreviewPane({ text, translating, error }) {
45756
46306
  dimColor: true
45757
46307
  }, "Press Esc to clear and try again."));
45758
46308
  borderColor = "red";
46309
+ } else if (concern) {
46310
+ body = /* @__PURE__ */ import_react25.default.createElement(Box_default, {
46311
+ flexDirection: "column"
46312
+ }, /* @__PURE__ */ import_react25.default.createElement(Text, {
46313
+ color: "yellow",
46314
+ bold: true
46315
+ }, "⚠ 上下文检查发现可能的问题"), /* @__PURE__ */ import_react25.default.createElement(Text, {
46316
+ color: "yellow"
46317
+ }, concern), /* @__PURE__ */ import_react25.default.createElement(Box_default, {
46318
+ marginTop: 1
46319
+ }, /* @__PURE__ */ import_react25.default.createElement(Text, null, /* @__PURE__ */ import_react25.default.createElement(Text, {
46320
+ color: "green",
46321
+ bold: true
46322
+ }, "↳ 翻译已就绪:"), " ", text)), /* @__PURE__ */ import_react25.default.createElement(Text, {
46323
+ dimColor: true
46324
+ }, "Enter 再按一次确认发送 · Esc 取消 · 或直接改输入重译"));
46325
+ borderColor = "yellow";
45759
46326
  } else if (translating) {
45760
- body = /* @__PURE__ */ import_react25.default.createElement(Text, {
46327
+ const partial2 = streamingText ?? "";
46328
+ body = partial2 ? /* @__PURE__ */ import_react25.default.createElement(Box_default, {
46329
+ flexDirection: "column"
46330
+ }, /* @__PURE__ */ import_react25.default.createElement(Text, null, /* @__PURE__ */ import_react25.default.createElement(Text, {
46331
+ color: "yellow",
46332
+ bold: true
46333
+ }, "⏳ [zh→en] streaming:", " "), partial2, /* @__PURE__ */ import_react25.default.createElement(Text, {
46334
+ color: "yellow",
46335
+ bold: true
46336
+ }, "▎")), /* @__PURE__ */ import_react25.default.createElement(Text, {
46337
+ dimColor: true
46338
+ }, "翻译完成后会写入 user-input.md,再切到 Claude Code 输 /i ↵")) : /* @__PURE__ */ import_react25.default.createElement(Text, {
45761
46339
  color: "yellow"
45762
46340
  }, "⏳ translating…");
45763
46341
  borderColor = "yellow";
@@ -45791,44 +46369,532 @@ function PreviewPane({ text, translating, error }) {
45791
46369
 
45792
46370
  // src/components/output-pane.tsx
45793
46371
  var import_react26 = __toESM(require_react(), 1);
45794
- function formatTime(iso) {
45795
- const d = new Date(iso);
45796
- const pad2 = (n) => n.toString().padStart(2, "0");
45797
- return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
46372
+
46373
+ // src/tool-display.ts
46374
+ function summarizeToolInput(name, input) {
46375
+ const i = input && typeof input === "object" && !Array.isArray(input) ? input : {};
46376
+ switch (name) {
46377
+ case "Bash":
46378
+ case "PowerShell":
46379
+ return str(i["command"]);
46380
+ case "Read":
46381
+ return joinNonEmpty(str(i["file_path"]), num(i["limit"]) !== null ? `limit=${num(i["limit"])}` : "", num(i["offset"]) !== null ? `offset=${num(i["offset"])}` : "");
46382
+ case "Edit":
46383
+ case "Write":
46384
+ case "NotebookEdit":
46385
+ return str(i["file_path"]) || str(i["notebook_path"]);
46386
+ case "Glob":
46387
+ return joinNonEmpty(str(i["pattern"]), str(i["path"]) ? `in ${str(i["path"])}` : "");
46388
+ case "Grep":
46389
+ return joinNonEmpty(JSON.stringify(str(i["pattern"])), str(i["path"]) ? `in ${str(i["path"])}` : "");
46390
+ case "WebFetch":
46391
+ return str(i["url"]);
46392
+ case "WebSearch":
46393
+ return JSON.stringify(str(i["query"]));
46394
+ case "ExitWorktree":
46395
+ return str(i["action"]);
46396
+ case "ScheduleWakeup":
46397
+ return joinNonEmpty(num(i["delaySeconds"]) !== null ? `${num(i["delaySeconds"])}s` : "", str(i["reason"]));
46398
+ case "Monitor":
46399
+ return str(i["command"]) || str(i["description"]);
46400
+ case "ToolSearch":
46401
+ return JSON.stringify(str(i["query"]));
46402
+ case "Skill":
46403
+ return joinNonEmpty(str(i["skill"]), str(i["args"]) ? `args=${str(i["args"])}` : "");
46404
+ case "TaskCreate":
46405
+ return str(i["subject"]) || str(i["description"]);
46406
+ case "TaskUpdate":
46407
+ return joinNonEmpty(str(i["taskId"]) ? `id=${str(i["taskId"])}` : "", str(i["status"]), str(i["description"]));
46408
+ case "TaskStop":
46409
+ case "TaskOutput":
46410
+ return str(i["task_id"]);
46411
+ case "TaskList":
46412
+ return "";
46413
+ default:
46414
+ return genericSummary(i);
46415
+ }
45798
46416
  }
45799
- function OutputPane({ entries, pendingCount }) {
45800
- return /* @__PURE__ */ import_react26.default.createElement(import_react26.default.Fragment, null, /* @__PURE__ */ import_react26.default.createElement(Static, {
45801
- items: entries
45802
- }, (entry) => /* @__PURE__ */ import_react26.default.createElement(OutputEntryRow, {
45803
- key: entry.id,
45804
- entry
45805
- })), pendingCount > 0 ? /* @__PURE__ */ import_react26.default.createElement(Box_default, {
45806
- paddingX: 1
45807
- }, /* @__PURE__ */ import_react26.default.createElement(Text, {
45808
- color: "yellow"
45809
- }, "⏳ translating ", pendingCount, " new repl", pendingCount === 1 ? "y" : "ies", "…")) : null);
46417
+ var LINE_LIMIT = 3;
46418
+ function formatToolResult(content) {
46419
+ const text = stringifyResult(content);
46420
+ const all = text.split(`
46421
+ `);
46422
+ while (all.length > 0 && all[all.length - 1].trim() === "")
46423
+ all.pop();
46424
+ if (all.length <= LINE_LIMIT) {
46425
+ return { lines: all, truncated: 0 };
46426
+ }
46427
+ return {
46428
+ lines: all.slice(0, LINE_LIMIT),
46429
+ truncated: all.length - LINE_LIMIT
46430
+ };
45810
46431
  }
45811
- function OutputEntryRow({ entry }) {
45812
- return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46432
+ function str(v) {
46433
+ return typeof v === "string" ? v : "";
46434
+ }
46435
+ function num(v) {
46436
+ return typeof v === "number" ? v : null;
46437
+ }
46438
+ function joinNonEmpty(...parts) {
46439
+ return parts.filter((p) => p && p.length > 0).join(", ");
46440
+ }
46441
+ var GENERIC_KEY_LIMIT = 3;
46442
+ var GENERIC_VALUE_LIMIT = 40;
46443
+ function genericSummary(input) {
46444
+ const keys2 = Object.keys(input).slice(0, GENERIC_KEY_LIMIT);
46445
+ return keys2.map((k) => {
46446
+ const v = input[k];
46447
+ const sv = typeof v === "string" ? v : JSON.stringify(v);
46448
+ const trimmed = sv.length > GENERIC_VALUE_LIMIT ? sv.slice(0, GENERIC_VALUE_LIMIT - 1) + "…" : sv;
46449
+ return `${k}=${trimmed}`;
46450
+ }).join(", ");
46451
+ }
46452
+ function stringifyResult(content) {
46453
+ if (typeof content === "string")
46454
+ return content;
46455
+ if (Array.isArray(content)) {
46456
+ const parts = [];
46457
+ for (const c of content) {
46458
+ if (!c || typeof c !== "object")
46459
+ continue;
46460
+ const block = c;
46461
+ if (block["type"] === "text" && typeof block["text"] === "string") {
46462
+ parts.push(block["text"]);
46463
+ } else if (block["type"] === "image") {
46464
+ parts.push("[image]");
46465
+ } else {
46466
+ parts.push(JSON.stringify(block));
46467
+ }
46468
+ }
46469
+ return parts.join(`
46470
+ `);
46471
+ }
46472
+ if (content === null || content === undefined)
46473
+ return "";
46474
+ try {
46475
+ return JSON.stringify(content);
46476
+ } catch {
46477
+ return String(content);
46478
+ }
46479
+ }
46480
+
46481
+ // src/components/output-pane.tsx
46482
+ function OutputPane({
46483
+ entries,
46484
+ streaming
46485
+ }) {
46486
+ return /* @__PURE__ */ import_react26.default.createElement(import_react26.default.Fragment, null, /* @__PURE__ */ import_react26.default.createElement(Static, {
46487
+ items: entries
46488
+ }, (entry) => /* @__PURE__ */ import_react26.default.createElement(OutputEntryRow, {
46489
+ key: entry.id,
46490
+ entry
46491
+ })), streaming.length > 0 ? /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46492
+ flexDirection: "column"
46493
+ }, streaming.map((s) => /* @__PURE__ */ import_react26.default.createElement(StreamingEntryRow, {
46494
+ key: s.id,
46495
+ entry: s
46496
+ }))) : null);
46497
+ }
46498
+ var BULLET_COLOR = {
46499
+ text: "white",
46500
+ thinking: "magenta",
46501
+ question: "yellow",
46502
+ todo: "blue",
46503
+ agent: "green",
46504
+ tool_use: "cyan",
46505
+ tool_result: "gray"
46506
+ };
46507
+ function Bullet({ kind }) {
46508
+ return /* @__PURE__ */ import_react26.default.createElement(Text, {
46509
+ color: BULLET_COLOR[kind],
46510
+ bold: true
46511
+ }, "●");
46512
+ }
46513
+ function StructuredHeader({
46514
+ kind,
46515
+ agentType
46516
+ }) {
46517
+ if (kind === "question") {
46518
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, null, /* @__PURE__ */ import_react26.default.createElement(Bullet, {
46519
+ kind: "question"
46520
+ }), /* @__PURE__ */ import_react26.default.createElement(Text, {
46521
+ bold: true
46522
+ }, " ❓ 提问:"));
46523
+ }
46524
+ if (kind === "todo") {
46525
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, null, /* @__PURE__ */ import_react26.default.createElement(Bullet, {
46526
+ kind: "todo"
46527
+ }), /* @__PURE__ */ import_react26.default.createElement(Text, {
46528
+ bold: true
46529
+ }, " \uD83D\uDCDD 计划更新:"));
46530
+ }
46531
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, null, /* @__PURE__ */ import_react26.default.createElement(Bullet, {
46532
+ kind: "agent"
46533
+ }), /* @__PURE__ */ import_react26.default.createElement(Text, {
46534
+ bold: true
46535
+ }, " \uD83E\uDD16 子代理", agentType ? ` [${agentType}]` : "", ":"));
46536
+ }
46537
+ function OutputEntryRow({ entry }) {
46538
+ if (entry.kind === "tool_use") {
46539
+ return /* @__PURE__ */ import_react26.default.createElement(ToolUseRow, {
46540
+ toolUse: entry.toolUse
46541
+ });
46542
+ }
46543
+ if (entry.kind === "tool_result") {
46544
+ return /* @__PURE__ */ import_react26.default.createElement(ToolResultRow, {
46545
+ toolResult: entry.toolResult
46546
+ });
46547
+ }
46548
+ if (entry.kind === "question") {
46549
+ return /* @__PURE__ */ import_react26.default.createElement(StructuredFrame, {
46550
+ header: /* @__PURE__ */ import_react26.default.createElement(StructuredHeader, {
46551
+ kind: "question"
46552
+ }),
46553
+ error: entry.state === "error" ? entry.error : undefined,
46554
+ fallback: `(原文 questions: ${entry.englishQuestion.questions.length})`
46555
+ }, /* @__PURE__ */ import_react26.default.createElement(QuestionBody, {
46556
+ payload: entry.chineseQuestion
46557
+ }));
46558
+ }
46559
+ if (entry.kind === "todo") {
46560
+ return /* @__PURE__ */ import_react26.default.createElement(StructuredFrame, {
46561
+ header: /* @__PURE__ */ import_react26.default.createElement(StructuredHeader, {
46562
+ kind: "todo"
46563
+ }),
46564
+ error: entry.state === "error" ? entry.error : undefined,
46565
+ fallback: `(原文 todos: ${entry.englishTodo.todos.length})`
46566
+ }, /* @__PURE__ */ import_react26.default.createElement(TodoBody, {
46567
+ payload: entry.chineseTodo
46568
+ }));
46569
+ }
46570
+ if (entry.kind === "agent") {
46571
+ return /* @__PURE__ */ import_react26.default.createElement(StructuredFrame, {
46572
+ header: /* @__PURE__ */ import_react26.default.createElement(StructuredHeader, {
46573
+ kind: "agent",
46574
+ agentType: entry.englishAgent.subagent_type
46575
+ }),
46576
+ error: entry.state === "error" ? entry.error : undefined,
46577
+ fallback: `(原文 agent: ${entry.englishAgent.subagent_type})`
46578
+ }, /* @__PURE__ */ import_react26.default.createElement(AgentBody, {
46579
+ payload: entry.chineseAgent
46580
+ }));
46581
+ }
46582
+ const dim = entry.kind === "thinking";
46583
+ if (entry.state === "error") {
46584
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46585
+ flexDirection: "column",
46586
+ marginY: 1,
46587
+ paddingX: 1
46588
+ }, /* @__PURE__ */ import_react26.default.createElement(Box_default, null, /* @__PURE__ */ import_react26.default.createElement(Bullet, {
46589
+ kind: entry.kind
46590
+ }), /* @__PURE__ */ import_react26.default.createElement(Text, {
46591
+ color: "red"
46592
+ }, " [translate failed] ", entry.error)), /* @__PURE__ */ import_react26.default.createElement(Text, {
46593
+ dimColor: true
46594
+ }, " (原文) ", entry.english));
46595
+ }
46596
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
45813
46597
  flexDirection: "column",
45814
46598
  marginY: 1,
45815
46599
  paddingX: 1
45816
- }, /* @__PURE__ */ import_react26.default.createElement(Box_default, null, /* @__PURE__ */ import_react26.default.createElement(Text, {
46600
+ }, /* @__PURE__ */ import_react26.default.createElement(Box_default, null, /* @__PURE__ */ import_react26.default.createElement(Bullet, {
46601
+ kind: entry.kind
46602
+ }), /* @__PURE__ */ import_react26.default.createElement(Text, null, " "), dim ? /* @__PURE__ */ import_react26.default.createElement(Text, {
46603
+ dimColor: true,
46604
+ italic: true
46605
+ }, "\uD83D\uDCAD ", entry.chinese) : /* @__PURE__ */ import_react26.default.createElement(Text, null, entry.chinese)));
46606
+ }
46607
+ function StructuredFrame({
46608
+ header,
46609
+ error,
46610
+ fallback,
46611
+ children
46612
+ }) {
46613
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46614
+ flexDirection: "column",
46615
+ marginY: 1,
46616
+ paddingX: 1
46617
+ }, header, error ? /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46618
+ flexDirection: "column"
46619
+ }, /* @__PURE__ */ import_react26.default.createElement(Text, {
46620
+ color: "red"
46621
+ }, " [translate failed] ", error), /* @__PURE__ */ import_react26.default.createElement(Text, {
46622
+ dimColor: true
46623
+ }, " ", fallback)) : children);
46624
+ }
46625
+ function StreamingEntryRow({ entry }) {
46626
+ if (entry.kind === "question") {
46627
+ return /* @__PURE__ */ import_react26.default.createElement(StructuredFrame, {
46628
+ header: /* @__PURE__ */ import_react26.default.createElement(StructuredHeader, {
46629
+ kind: "question"
46630
+ }),
46631
+ fallback: ""
46632
+ }, /* @__PURE__ */ import_react26.default.createElement(QuestionBody, {
46633
+ payload: entry.chineseQuestion,
46634
+ streaming: true
46635
+ }));
46636
+ }
46637
+ if (entry.kind === "todo") {
46638
+ return /* @__PURE__ */ import_react26.default.createElement(StructuredFrame, {
46639
+ header: /* @__PURE__ */ import_react26.default.createElement(StructuredHeader, {
46640
+ kind: "todo"
46641
+ }),
46642
+ fallback: ""
46643
+ }, /* @__PURE__ */ import_react26.default.createElement(TodoBody, {
46644
+ payload: entry.chineseTodo,
46645
+ streaming: true
46646
+ }));
46647
+ }
46648
+ if (entry.kind === "agent") {
46649
+ return /* @__PURE__ */ import_react26.default.createElement(StructuredFrame, {
46650
+ header: /* @__PURE__ */ import_react26.default.createElement(StructuredHeader, {
46651
+ kind: "agent",
46652
+ agentType: entry.englishAgent.subagent_type
46653
+ }),
46654
+ fallback: ""
46655
+ }, /* @__PURE__ */ import_react26.default.createElement(AgentBody, {
46656
+ payload: entry.chineseAgent,
46657
+ streaming: true
46658
+ }));
46659
+ }
46660
+ const empty = entry.chineseSoFar.length === 0;
46661
+ const dim = entry.kind === "thinking";
46662
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46663
+ flexDirection: "column",
46664
+ marginY: 1,
46665
+ paddingX: 1
46666
+ }, /* @__PURE__ */ import_react26.default.createElement(Box_default, null, /* @__PURE__ */ import_react26.default.createElement(Bullet, {
46667
+ kind: entry.kind
46668
+ }), /* @__PURE__ */ import_react26.default.createElement(Text, null, " "), empty ? /* @__PURE__ */ import_react26.default.createElement(Text, {
46669
+ dimColor: true
46670
+ }, dim ? "\uD83D\uDCAD " : "", "翻译中…") : dim ? /* @__PURE__ */ import_react26.default.createElement(Text, {
46671
+ dimColor: true,
46672
+ italic: true
46673
+ }, "\uD83D\uDCAD ", entry.chineseSoFar, /* @__PURE__ */ import_react26.default.createElement(Text, {
46674
+ color: "yellow",
46675
+ bold: true
46676
+ }, "▎")) : /* @__PURE__ */ import_react26.default.createElement(Text, null, entry.chineseSoFar, /* @__PURE__ */ import_react26.default.createElement(Text, {
46677
+ color: "yellow",
46678
+ bold: true
46679
+ }, "▎"))));
46680
+ }
46681
+ var CIRCLED_NUMS = ["①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨"];
46682
+ function optionMarker(i) {
46683
+ return i < CIRCLED_NUMS.length ? CIRCLED_NUMS[i] : `${i + 1}.`;
46684
+ }
46685
+ function fieldText(f) {
46686
+ if (f.zh)
46687
+ return { text: f.zh, pending: false };
46688
+ return { text: f.en, pending: true };
46689
+ }
46690
+ function QuestionBody({
46691
+ payload,
46692
+ streaming = false
46693
+ }) {
46694
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46695
+ flexDirection: "column"
46696
+ }, payload.questions.map((q, qi) => /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46697
+ key: qi,
46698
+ flexDirection: "column",
46699
+ marginTop: qi > 0 ? 1 : 0
46700
+ }, /* @__PURE__ */ import_react26.default.createElement(QuestionLine, {
46701
+ field: q.question,
46702
+ streaming
46703
+ }), q.options.map((opt, oi) => /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46704
+ key: oi,
46705
+ flexDirection: "column",
46706
+ marginLeft: 2,
46707
+ marginTop: 1
46708
+ }, /* @__PURE__ */ import_react26.default.createElement(OptionLabelLine, {
46709
+ marker: optionMarker(oi),
46710
+ field: opt.label,
46711
+ streaming
46712
+ }), /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46713
+ marginLeft: 3
46714
+ }, /* @__PURE__ */ import_react26.default.createElement(OptionDescLine, {
46715
+ field: opt.description,
46716
+ streaming
46717
+ })))))));
46718
+ }
46719
+ function QuestionLine({
46720
+ field,
46721
+ streaming
46722
+ }) {
46723
+ const { text, pending } = fieldText(field);
46724
+ if (pending && streaming) {
46725
+ return /* @__PURE__ */ import_react26.default.createElement(Text, {
46726
+ dimColor: true
46727
+ }, text, " ", /* @__PURE__ */ import_react26.default.createElement(Text, {
46728
+ color: "yellow"
46729
+ }, "⏳"));
46730
+ }
46731
+ return /* @__PURE__ */ import_react26.default.createElement(Text, null, text);
46732
+ }
46733
+ function OptionLabelLine({
46734
+ marker,
46735
+ field,
46736
+ streaming
46737
+ }) {
46738
+ const { text, pending } = fieldText(field);
46739
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, null, /* @__PURE__ */ import_react26.default.createElement(Text, {
45817
46740
  color: "cyan",
45818
46741
  bold: true
45819
- }, "[", formatTime(entry.timestamp), "]"), /* @__PURE__ */ import_react26.default.createElement(Text, {
46742
+ }, marker, " "), pending && streaming ? /* @__PURE__ */ import_react26.default.createElement(Text, {
46743
+ dimColor: true
46744
+ }, /* @__PURE__ */ import_react26.default.createElement(Text, {
46745
+ bold: true
46746
+ }, text), " ", /* @__PURE__ */ import_react26.default.createElement(Text, {
46747
+ color: "yellow"
46748
+ }, "⏳")) : /* @__PURE__ */ import_react26.default.createElement(Text, {
45820
46749
  bold: true
45821
- }, " Claude:")), entry.state === "error" ? /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46750
+ }, text));
46751
+ }
46752
+ function OptionDescLine({
46753
+ field,
46754
+ streaming
46755
+ }) {
46756
+ const { text, pending } = fieldText(field);
46757
+ if (pending && streaming) {
46758
+ return /* @__PURE__ */ import_react26.default.createElement(Text, {
46759
+ dimColor: true
46760
+ }, text, " ", /* @__PURE__ */ import_react26.default.createElement(Text, {
46761
+ color: "yellow"
46762
+ }, "⏳"));
46763
+ }
46764
+ return /* @__PURE__ */ import_react26.default.createElement(Text, null, text);
46765
+ }
46766
+ function statusGlyph(s) {
46767
+ if (s === "completed") {
46768
+ return /* @__PURE__ */ import_react26.default.createElement(Text, {
46769
+ color: "green",
46770
+ bold: true
46771
+ }, "✓");
46772
+ }
46773
+ if (s === "in_progress") {
46774
+ return /* @__PURE__ */ import_react26.default.createElement(Text, {
46775
+ color: "yellow",
46776
+ bold: true
46777
+ }, "⟳");
46778
+ }
46779
+ return /* @__PURE__ */ import_react26.default.createElement(Text, {
46780
+ dimColor: true
46781
+ }, "○");
46782
+ }
46783
+ function TodoBody({
46784
+ payload,
46785
+ streaming = false
46786
+ }) {
46787
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46788
+ flexDirection: "column"
46789
+ }, payload.todos.map((t, i) => {
46790
+ const f = t.status === "in_progress" ? t.activeForm : t.content;
46791
+ const { text, pending } = fieldText(f);
46792
+ const dim = t.status === "completed";
46793
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46794
+ key: i
46795
+ }, /* @__PURE__ */ import_react26.default.createElement(Text, null, " "), statusGlyph(t.status), /* @__PURE__ */ import_react26.default.createElement(Text, null, " "), pending && streaming ? /* @__PURE__ */ import_react26.default.createElement(Text, {
46796
+ dimColor: true
46797
+ }, text, " ", /* @__PURE__ */ import_react26.default.createElement(Text, {
46798
+ color: "yellow"
46799
+ }, "⏳")) : /* @__PURE__ */ import_react26.default.createElement(Text, {
46800
+ dimColor: dim,
46801
+ strikethrough: dim
46802
+ }, text));
46803
+ }));
46804
+ }
46805
+ function AgentBody({
46806
+ payload,
46807
+ streaming = false
46808
+ }) {
46809
+ const desc = fieldText(payload.description);
46810
+ const prompt = fieldText(payload.prompt);
46811
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46812
+ flexDirection: "column"
46813
+ }, /* @__PURE__ */ import_react26.default.createElement(Box_default, null, /* @__PURE__ */ import_react26.default.createElement(Text, {
46814
+ bold: true
46815
+ }, "任务: "), desc.pending && streaming ? /* @__PURE__ */ import_react26.default.createElement(Text, {
46816
+ dimColor: true
46817
+ }, desc.text, " ", /* @__PURE__ */ import_react26.default.createElement(Text, {
46818
+ color: "yellow"
46819
+ }, "⏳")) : /* @__PURE__ */ import_react26.default.createElement(Text, null, desc.text)), /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46820
+ marginTop: 1,
45822
46821
  flexDirection: "column"
45823
46822
  }, /* @__PURE__ */ import_react26.default.createElement(Text, {
45824
- color: "red"
45825
- }, "[translate failed] ", entry.error), /* @__PURE__ */ import_react26.default.createElement(Text, {
46823
+ bold: true
46824
+ }, "指令:"), /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46825
+ marginLeft: 2
46826
+ }, prompt.pending && streaming ? /* @__PURE__ */ import_react26.default.createElement(Text, {
45826
46827
  dimColor: true
45827
- }, "(原文) ", entry.english)) : /* @__PURE__ */ import_react26.default.createElement(Text, null, entry.chinese));
46828
+ }, prompt.text, " ", /* @__PURE__ */ import_react26.default.createElement(Text, {
46829
+ color: "yellow"
46830
+ }, "⏳")) : /* @__PURE__ */ import_react26.default.createElement(Text, null, prompt.text))));
46831
+ }
46832
+ var TOOL_USE_LINE_CAP = 2;
46833
+ function ToolUseRow({ toolUse }) {
46834
+ const summary = summarizeToolInput(toolUse.name, toolUse.input);
46835
+ const allLines = summary.length > 0 ? summary.split(`
46836
+ `) : [""];
46837
+ const truncated = allLines.length > TOOL_USE_LINE_CAP;
46838
+ const lines = truncated ? allLines.slice(0, TOOL_USE_LINE_CAP) : allLines;
46839
+ const closer = truncated ? "…)" : ")";
46840
+ const continuationIndent = " ".repeat(2 + toolUse.name.length + 1);
46841
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46842
+ flexDirection: "column",
46843
+ marginTop: 1,
46844
+ marginBottom: 0,
46845
+ paddingX: 1
46846
+ }, lines.map((line, i) => {
46847
+ const isLast = i === lines.length - 1;
46848
+ if (i === 0) {
46849
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46850
+ key: i
46851
+ }, /* @__PURE__ */ import_react26.default.createElement(Text, {
46852
+ color: "cyan",
46853
+ bold: true
46854
+ }, "●"), /* @__PURE__ */ import_react26.default.createElement(Text, {
46855
+ bold: true
46856
+ }, " ", toolUse.name, "(", line, isLast ? closer : ""));
46857
+ }
46858
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46859
+ key: i
46860
+ }, /* @__PURE__ */ import_react26.default.createElement(Text, null, continuationIndent), /* @__PURE__ */ import_react26.default.createElement(Text, {
46861
+ bold: true
46862
+ }, line, isLast ? closer : ""));
46863
+ }));
46864
+ }
46865
+ function ToolResultRow({
46866
+ toolResult
46867
+ }) {
46868
+ const r = formatToolResult(toolResult.content);
46869
+ const errorColor = toolResult.isError ? "red" : undefined;
46870
+ if (r.lines.length === 0) {
46871
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46872
+ marginY: 0,
46873
+ paddingX: 1
46874
+ }, /* @__PURE__ */ import_react26.default.createElement(Text, {
46875
+ color: errorColor ?? "gray"
46876
+ }, " ⎿ "), /* @__PURE__ */ import_react26.default.createElement(Text, {
46877
+ dimColor: true
46878
+ }, "(no output)"));
46879
+ }
46880
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46881
+ flexDirection: "column",
46882
+ marginY: 0,
46883
+ paddingX: 1
46884
+ }, r.lines.map((line, i) => /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46885
+ key: i
46886
+ }, /* @__PURE__ */ import_react26.default.createElement(Text, {
46887
+ color: i === 0 ? errorColor ?? "gray" : undefined
46888
+ }, i === 0 ? " ⎿ " : " "), /* @__PURE__ */ import_react26.default.createElement(Text, {
46889
+ dimColor: true,
46890
+ color: errorColor
46891
+ }, line))), r.truncated > 0 ? /* @__PURE__ */ import_react26.default.createElement(Box_default, null, /* @__PURE__ */ import_react26.default.createElement(Text, null, " "), /* @__PURE__ */ import_react26.default.createElement(Text, {
46892
+ dimColor: true
46893
+ }, "… +", r.truncated, " lines")) : null);
45828
46894
  }
45829
46895
 
45830
46896
  // src/components/settings-modal.tsx
45831
- var import_react28 = __toESM(require_react(), 1);
46897
+ var import_react29 = __toESM(require_react(), 1);
45832
46898
 
45833
46899
  // src/components/multiline-input.tsx
45834
46900
  var import_react27 = __toESM(require_react(), 1);
@@ -46033,6 +47099,125 @@ function MultilineDisplay({
46033
47099
  }, lines.length, " line", lines.length === 1 ? "" : "s", " · ", charCount, " chars"))));
46034
47100
  }
46035
47101
 
47102
+ // src/components/session-picker.tsx
47103
+ var import_react28 = __toESM(require_react(), 1);
47104
+ function shortId2(id) {
47105
+ if (id.length <= 12)
47106
+ return id;
47107
+ return id.slice(0, 8) + "..." + id.slice(-4);
47108
+ }
47109
+ function SessionPicker({ sessions, onPick, onCancel }) {
47110
+ const [cursor, setCursor] = import_react28.useState(0);
47111
+ const [search, setSearch] = import_react28.useState("");
47112
+ const [searching, setSearching] = import_react28.useState(false);
47113
+ const filtered = search ? sessions.filter((s) => {
47114
+ const haystack = [
47115
+ s.id,
47116
+ s.first_message ?? "",
47117
+ s.started_at ?? "",
47118
+ s.last_activity_at ?? ""
47119
+ ].join(" ").toLowerCase();
47120
+ return haystack.includes(search.toLowerCase());
47121
+ }) : sessions;
47122
+ use_input_default((input, key) => {
47123
+ if (searching) {
47124
+ if (key.escape) {
47125
+ setSearching(false);
47126
+ setSearch("");
47127
+ return;
47128
+ }
47129
+ if (key.return) {
47130
+ setSearching(false);
47131
+ return;
47132
+ }
47133
+ if (key.backspace || key.delete) {
47134
+ setSearch((s) => s.slice(0, -1));
47135
+ return;
47136
+ }
47137
+ if (input && !key.ctrl && !key.meta) {
47138
+ setSearch((s) => s + input);
47139
+ }
47140
+ return;
47141
+ }
47142
+ if (key.escape) {
47143
+ onCancel();
47144
+ return;
47145
+ }
47146
+ if (key.upArrow) {
47147
+ setCursor((c) => Math.max(0, c - 1));
47148
+ return;
47149
+ }
47150
+ if (key.downArrow) {
47151
+ setCursor((c) => Math.min(filtered.length - 1, c + 1));
47152
+ return;
47153
+ }
47154
+ if (key.return) {
47155
+ const target = filtered[cursor];
47156
+ if (target)
47157
+ onPick(target);
47158
+ return;
47159
+ }
47160
+ if (input === "/") {
47161
+ setSearching(true);
47162
+ }
47163
+ });
47164
+ if (sessions.length === 0) {
47165
+ return /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47166
+ flexDirection: "column",
47167
+ padding: 1
47168
+ }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47169
+ color: "yellow"
47170
+ }, "No Claude Code sessions found in this project."), /* @__PURE__ */ import_react28.default.createElement(Text, {
47171
+ dimColor: true
47172
+ }, "Start `claude` in this project first, then run `cc-zh-watcher` again."), /* @__PURE__ */ import_react28.default.createElement(Text, {
47173
+ dimColor: true
47174
+ }, "Press Esc to exit."));
47175
+ }
47176
+ const visible = filtered.slice(0, 8);
47177
+ return /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47178
+ flexDirection: "column"
47179
+ }, /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47180
+ borderStyle: "round",
47181
+ borderColor: "cyan",
47182
+ flexDirection: "column",
47183
+ paddingX: 1
47184
+ }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47185
+ bold: true,
47186
+ color: "cyan"
47187
+ }, "Select a Claude Code session to watch"), /* @__PURE__ */ import_react28.default.createElement(Text, {
47188
+ dimColor: true
47189
+ }, "project: ", process.cwd(), " · ", sessions.length, " session", sessions.length === 1 ? "" : "s"), /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47190
+ marginTop: 1,
47191
+ flexDirection: "column"
47192
+ }, visible.map((s, i) => {
47193
+ const selected = i === cursor;
47194
+ return /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47195
+ key: s.id,
47196
+ flexDirection: "column",
47197
+ marginBottom: 1
47198
+ }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47199
+ color: selected ? "cyan" : undefined,
47200
+ bold: selected
47201
+ }, selected ? "⮕ " : " ", i + 1, ". ", shortId2(s.id), " [active", " ", relativeTime(s.last_activity_at), " · ", s.turn_count, " turn", s.turn_count === 1 ? "" : "s", "]"), /* @__PURE__ */ import_react28.default.createElement(Text, {
47202
+ dimColor: true
47203
+ }, " started: ", formatLocalTime(s.started_at)), s.first_message ? /* @__PURE__ */ import_react28.default.createElement(Text, {
47204
+ dimColor: true
47205
+ }, " first msg: ", JSON.stringify(s.first_message)) : null);
47206
+ }), filtered.length > visible.length ? /* @__PURE__ */ import_react28.default.createElement(Text, {
47207
+ dimColor: true
47208
+ }, "... and ", filtered.length - visible.length, " more (use / to search)") : null), /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47209
+ marginTop: 1
47210
+ }, searching ? /* @__PURE__ */ import_react28.default.createElement(Text, null, /* @__PURE__ */ import_react28.default.createElement(Text, {
47211
+ color: "yellow"
47212
+ }, "/"), search, /* @__PURE__ */ import_react28.default.createElement(Text, {
47213
+ color: "yellow"
47214
+ }, "_"), /* @__PURE__ */ import_react28.default.createElement(Text, {
47215
+ dimColor: true
47216
+ }, " (Esc clear · Enter accept)")) : /* @__PURE__ */ import_react28.default.createElement(Text, {
47217
+ dimColor: true
47218
+ }, "[↑/↓] move [Enter] select [/] search [Esc] cancel"))));
47219
+ }
47220
+
46036
47221
  // src/components/settings-modal.tsx
46037
47222
  var FIELD_KEYS = [
46038
47223
  "api_key",
@@ -46051,8 +47236,16 @@ var PROMPT_LABEL = {
46051
47236
  "zh-to-en-with-context": "zh-to-en-w/ctx",
46052
47237
  "en-to-zh": "en-to-zh"
46053
47238
  };
46054
- var ROW_API_END = FIELD_KEYS.length;
46055
- var ROW_USER_END = ROW_API_END + PROMPT_KEYS.length;
47239
+ var ROW_SESSION_END = 1;
47240
+ var ROW_API_END = ROW_SESSION_END + FIELD_KEYS.length;
47241
+ var ROW_NOTIFY_END = ROW_API_END + 1;
47242
+ var ROW_THINKING_END = ROW_API_END + 2;
47243
+ var ROW_TODOS_END = ROW_API_END + 3;
47244
+ var ROW_AGENT_END = ROW_API_END + 4;
47245
+ var ROW_CONTEXT_END = ROW_API_END + 5;
47246
+ var TOGGLE_ROW_COUNT = 6;
47247
+ var ROW_TOGGLES_END = ROW_API_END + TOGGLE_ROW_COUNT;
47248
+ var ROW_USER_END = ROW_TOGGLES_END + PROMPT_KEYS.length;
46056
47249
  var ROW_DEFAULT_END = ROW_USER_END + PROMPT_KEYS.length;
46057
47250
  var TOTAL_ROWS = ROW_DEFAULT_END;
46058
47251
  function maskApiKey(key) {
@@ -46078,24 +47271,33 @@ function previewDefault(text, width) {
46078
47271
  }
46079
47272
  function SettingsModal({
46080
47273
  initialConfig,
46081
- onSave
47274
+ onSave,
47275
+ currentSession,
47276
+ projectDir,
47277
+ onSessionChange
46082
47278
  }) {
46083
- const [fields, setFields] = import_react28.useState(() => ({
47279
+ const [fields, setFields] = import_react29.useState(() => ({
46084
47280
  api_key: initialConfig.api_key,
46085
47281
  base_url: initialConfig.base_url,
46086
47282
  model: initialConfig.model,
46087
47283
  timeout_seconds: String(initialConfig.timeout_seconds),
47284
+ notify_on_complete: initialConfig.notify_on_complete,
47285
+ translate_thinking: initialConfig.translate_thinking,
47286
+ translate_todos: initialConfig.translate_todos,
47287
+ translate_agent: initialConfig.translate_agent,
47288
+ context_enabled: initialConfig.context_enabled,
47289
+ show_tool_calls: initialConfig.show_tool_calls,
46088
47290
  prompts: {
46089
47291
  "zh-to-en": initialConfig.prompts?.["zh-to-en"] ?? "",
46090
47292
  "zh-to-en-with-context": initialConfig.prompts?.["zh-to-en-with-context"] ?? "",
46091
47293
  "en-to-zh": initialConfig.prompts?.["en-to-zh"] ?? ""
46092
47294
  }
46093
47295
  }));
46094
- const [cursor, setCursor] = import_react28.useState(0);
46095
- const [target, setTarget] = import_react28.useState(null);
46096
- const [editBuffer, setEditBuffer] = import_react28.useState("");
46097
- const [error, setError] = import_react28.useState(null);
46098
- const [saving, setSaving] = import_react28.useState(false);
47296
+ const [cursor, setCursor] = import_react29.useState(0);
47297
+ const [target, setTarget] = import_react29.useState(null);
47298
+ const [editBuffer, setEditBuffer] = import_react29.useState("");
47299
+ const [error, setError] = import_react29.useState(null);
47300
+ const [saving, setSaving] = import_react29.useState(false);
46099
47301
  function commitScalar(idx, value) {
46100
47302
  const fk = FIELD_KEYS[idx];
46101
47303
  if (!fk)
@@ -46153,17 +47355,52 @@ function SettingsModal({
46153
47355
  return;
46154
47356
  }
46155
47357
  if (key.return) {
47358
+ if (cursor < ROW_SESSION_END) {
47359
+ setTarget({ kind: "session-pick" });
47360
+ setError(null);
47361
+ return;
47362
+ }
46156
47363
  if (cursor < ROW_API_END) {
46157
- const fk = FIELD_KEYS[cursor];
47364
+ const fk = FIELD_KEYS[cursor - ROW_SESSION_END];
46158
47365
  if (!fk)
46159
47366
  return;
46160
47367
  setEditBuffer(fields[fk]);
46161
- setTarget({ kind: "scalar", index: cursor });
47368
+ setTarget({ kind: "scalar", index: cursor - ROW_SESSION_END });
47369
+ setError(null);
47370
+ return;
47371
+ }
47372
+ if (cursor < ROW_NOTIFY_END) {
47373
+ setFields((f) => ({ ...f, notify_on_complete: !f.notify_on_complete }));
47374
+ setError(null);
47375
+ return;
47376
+ }
47377
+ if (cursor < ROW_THINKING_END) {
47378
+ setFields((f) => ({ ...f, translate_thinking: !f.translate_thinking }));
47379
+ setError(null);
47380
+ return;
47381
+ }
47382
+ if (cursor < ROW_TODOS_END) {
47383
+ setFields((f) => ({ ...f, translate_todos: !f.translate_todos }));
47384
+ setError(null);
47385
+ return;
47386
+ }
47387
+ if (cursor < ROW_AGENT_END) {
47388
+ setFields((f) => ({ ...f, translate_agent: !f.translate_agent }));
47389
+ setError(null);
47390
+ return;
47391
+ }
47392
+ if (cursor < ROW_CONTEXT_END) {
47393
+ setFields((f) => ({ ...f, context_enabled: !f.context_enabled }));
47394
+ setError(null);
47395
+ return;
47396
+ }
47397
+ if (cursor < ROW_TOGGLES_END) {
47398
+ setFields((f) => ({ ...f, show_tool_calls: !f.show_tool_calls }));
46162
47399
  setError(null);
46163
47400
  return;
46164
47401
  }
46165
47402
  if (cursor < ROW_USER_END) {
46166
- const idx2 = cursor - ROW_API_END;
47403
+ const idx2 = cursor - ROW_TOGGLES_END;
46167
47404
  setTarget({ kind: "user-prompt", rowIndex: idx2 });
46168
47405
  setError(null);
46169
47406
  return;
@@ -46190,9 +47427,9 @@ function SettingsModal({
46190
47427
  const direction = PROMPT_KEYS[target.rowIndex];
46191
47428
  if (!direction) {
46192
47429
  setTarget(null);
46193
- return /* @__PURE__ */ import_react28.default.createElement(Box_default, null);
47430
+ return /* @__PURE__ */ import_react29.default.createElement(Box_default, null);
46194
47431
  }
46195
- return /* @__PURE__ */ import_react28.default.createElement(MultilineInput, {
47432
+ return /* @__PURE__ */ import_react29.default.createElement(MultilineInput, {
46196
47433
  title: `user.${direction}`,
46197
47434
  initial: fields.prompts[direction],
46198
47435
  onSubmit: (value) => {
@@ -46206,138 +47443,347 @@ function SettingsModal({
46206
47443
  const direction = PROMPT_KEYS[target.rowIndex];
46207
47444
  if (!direction) {
46208
47445
  setTarget(null);
46209
- return /* @__PURE__ */ import_react28.default.createElement(Box_default, null);
47446
+ return /* @__PURE__ */ import_react29.default.createElement(Box_default, null);
46210
47447
  }
46211
- return /* @__PURE__ */ import_react28.default.createElement(MultilineDisplay, {
47448
+ return /* @__PURE__ */ import_react29.default.createElement(MultilineDisplay, {
46212
47449
  title: `default.${direction}`,
46213
47450
  text: basePrompt(direction),
46214
47451
  onClose: () => setTarget(null)
46215
47452
  });
46216
47453
  }
46217
- return /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47454
+ if (target?.kind === "session-pick") {
47455
+ const sessions = listSessions(projectDir);
47456
+ return /* @__PURE__ */ import_react29.default.createElement(SessionPicker, {
47457
+ sessions,
47458
+ onPick: (s) => {
47459
+ setTarget(null);
47460
+ if (s.id !== currentSession.id) {
47461
+ onSessionChange(s);
47462
+ }
47463
+ },
47464
+ onCancel: () => setTarget(null)
47465
+ });
47466
+ }
47467
+ return /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46218
47468
  flexDirection: "column",
46219
47469
  padding: 1
46220
- }, /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47470
+ }, /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46221
47471
  borderStyle: "round",
46222
47472
  borderColor: "cyan",
46223
47473
  paddingX: 1,
46224
47474
  flexDirection: "column"
46225
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47475
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46226
47476
  bold: true,
46227
47477
  color: "cyan"
46228
- }, "Settings"), /* @__PURE__ */ import_react28.default.createElement(Text, {
47478
+ }, "Settings"), /* @__PURE__ */ import_react29.default.createElement(Text, {
46229
47479
  dimColor: true
46230
- }, "↑/↓ select · Enter edit · Esc save & close"), /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47480
+ }, "↑/↓ select · Enter edit · Esc save & close"), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46231
47481
  marginTop: 1,
46232
47482
  flexDirection: "column"
46233
- }, FIELD_KEYS.map((fk, i) => {
46234
- const selected = cursor === i;
47483
+ }, (() => {
47484
+ const selected = cursor === 0;
47485
+ const sid = currentSession.id;
47486
+ const short = sid.length > 12 ? sid.slice(0, 8) + "…" + sid.slice(-4) : sid;
47487
+ const turns = currentSession.turn_count;
47488
+ const active = relativeTime(currentSession.last_activity_at);
47489
+ return /* @__PURE__ */ import_react29.default.createElement(Box_default, null, /* @__PURE__ */ import_react29.default.createElement(Text, {
47490
+ color: selected ? "cyan" : undefined,
47491
+ bold: selected
47492
+ }, selected ? "▸ " : " "), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
47493
+ width: 20
47494
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
47495
+ color: selected ? "cyan" : undefined
47496
+ }, "session:")), /* @__PURE__ */ import_react29.default.createElement(Text, null, short, " ", /* @__PURE__ */ import_react29.default.createElement(Text, {
47497
+ dimColor: true
47498
+ }, "(", turns, " turn", turns === 1 ? "" : "s", " · active ", active, "; Enter 切换)")));
47499
+ })(), FIELD_KEYS.map((fk, i) => {
47500
+ const rowIdx = ROW_SESSION_END + i;
47501
+ const selected = cursor === rowIdx;
46235
47502
  const editingThis = selected && target?.kind === "scalar" && target.index === i;
46236
- return /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47503
+ return /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46237
47504
  key: fk
46238
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47505
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46239
47506
  color: selected ? "cyan" : undefined,
46240
47507
  bold: selected
46241
- }, selected ? "▸ " : " "), /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47508
+ }, selected ? "▸ " : " "), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46242
47509
  width: 20
46243
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47510
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46244
47511
  color: selected ? "cyan" : undefined
46245
- }, FIELD_LABELS[fk], ":")), editingThis ? /* @__PURE__ */ import_react28.default.createElement(build_default, {
47512
+ }, FIELD_LABELS[fk], ":")), editingThis ? /* @__PURE__ */ import_react29.default.createElement(build_default, {
46246
47513
  value: editBuffer,
46247
47514
  onChange: setEditBuffer,
46248
47515
  onSubmit: handleScalarSubmit
46249
- }) : /* @__PURE__ */ import_react28.default.createElement(Text, null, fk === "api_key" ? maskApiKey(fields[fk]) : fields[fk]));
46250
- }), /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47516
+ }) : /* @__PURE__ */ import_react29.default.createElement(Text, null, fk === "api_key" ? maskApiKey(fields[fk]) : fields[fk]));
47517
+ }), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46251
47518
  marginTop: 1
46252
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47519
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
47520
+ dimColor: true
47521
+ }, "── Toggles ──")), /* @__PURE__ */ import_react29.default.createElement(ToggleRow, {
47522
+ rowIdx: ROW_API_END,
47523
+ cursor,
47524
+ label: "notify_on_complete",
47525
+ on: fields.notify_on_complete,
47526
+ hintOn: "(终端响铃;Enter 切换)",
47527
+ hintOff: "(已关闭;Enter 切换)"
47528
+ }), /* @__PURE__ */ import_react29.default.createElement(ToggleRow, {
47529
+ rowIdx: ROW_NOTIFY_END,
47530
+ cursor,
47531
+ label: "translate_thinking",
47532
+ on: fields.translate_thinking,
47533
+ hintOn: "(译 thinking 块,多花 API;Enter 切换)",
47534
+ hintOff: "(默认关;Enter 切换)"
47535
+ }), /* @__PURE__ */ import_react29.default.createElement(ToggleRow, {
47536
+ rowIdx: ROW_THINKING_END,
47537
+ cursor,
47538
+ label: "translate_todos",
47539
+ on: fields.translate_todos,
47540
+ hintOn: "(译 TodoWrite 计划;Enter 切换)",
47541
+ hintOff: "(已关闭;Enter 切换)"
47542
+ }), /* @__PURE__ */ import_react29.default.createElement(ToggleRow, {
47543
+ rowIdx: ROW_TODOS_END,
47544
+ cursor,
47545
+ label: "translate_agent",
47546
+ on: fields.translate_agent,
47547
+ hintOn: "(译 Agent/Task 子代理;Enter 切换)",
47548
+ hintOff: "(已关闭;Enter 切换)"
47549
+ }), /* @__PURE__ */ import_react29.default.createElement(ToggleRow, {
47550
+ rowIdx: ROW_AGENT_END,
47551
+ cursor,
47552
+ label: "context_enabled",
47553
+ on: fields.context_enabled,
47554
+ hintOn: "(zh→en 用 Claude 上一条作 context;Enter 切换)",
47555
+ hintOff: "(字面翻译不带 context;Enter 切换)"
47556
+ }), /* @__PURE__ */ import_react29.default.createElement(ToggleRow, {
47557
+ rowIdx: ROW_CONTEXT_END,
47558
+ cursor,
47559
+ label: "show_tool_calls",
47560
+ on: fields.show_tool_calls,
47561
+ hintOn: "(显示原始 tool 调用 + 结果(CC 同款);Enter 切换)",
47562
+ hintOff: "(只看翻译内容;Enter 切换)"
47563
+ }), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
47564
+ marginTop: 1
47565
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46253
47566
  dimColor: true
46254
47567
  }, "── User prompt (empty = use default) ──")), PROMPT_KEYS.map((dir, i) => {
46255
- const rowIdx = ROW_API_END + i;
47568
+ const rowIdx = ROW_TOGGLES_END + i;
46256
47569
  const selected = cursor === rowIdx;
46257
47570
  const value = fields.prompts[dir];
46258
- return /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47571
+ return /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46259
47572
  key: `user-${dir}`
46260
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47573
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46261
47574
  color: selected ? "cyan" : undefined,
46262
47575
  bold: selected
46263
- }, selected ? "▸ " : " "), /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47576
+ }, selected ? "▸ " : " "), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46264
47577
  width: 20
46265
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47578
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46266
47579
  color: selected ? "cyan" : undefined
46267
- }, "user.", PROMPT_LABEL[dir], ":")), /* @__PURE__ */ import_react28.default.createElement(Text, {
47580
+ }, "user.", PROMPT_LABEL[dir], ":")), /* @__PURE__ */ import_react29.default.createElement(Text, {
46268
47581
  dimColor: !value
46269
47582
  }, previewPrompt(value, 60)));
46270
- }), /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47583
+ }), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46271
47584
  marginTop: 1
46272
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47585
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46273
47586
  dimColor: true
46274
47587
  }, "── Default prompt (read-only) ──")), PROMPT_KEYS.map((dir, i) => {
46275
47588
  const rowIdx = ROW_USER_END + i;
46276
47589
  const selected = cursor === rowIdx;
46277
- return /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47590
+ return /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46278
47591
  key: `default-${dir}`
46279
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47592
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46280
47593
  color: selected ? "cyan" : undefined,
46281
47594
  bold: selected
46282
- }, selected ? "▸ " : " "), /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47595
+ }, selected ? "▸ " : " "), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46283
47596
  width: 20
46284
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47597
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46285
47598
  color: selected ? "cyan" : undefined,
46286
47599
  dimColor: true
46287
- }, "default.", PROMPT_LABEL[dir], ":")), /* @__PURE__ */ import_react28.default.createElement(Text, {
47600
+ }, "default.", PROMPT_LABEL[dir], ":")), /* @__PURE__ */ import_react29.default.createElement(Text, {
46288
47601
  dimColor: true
46289
47602
  }, previewDefault(basePrompt(dir), 60)));
46290
- })), error ? /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47603
+ })), error ? /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46291
47604
  marginTop: 1
46292
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47605
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46293
47606
  color: "red"
46294
- }, "✗ ", error)) : null, saving ? /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47607
+ }, "✗ ", error)) : null, saving ? /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46295
47608
  marginTop: 1
46296
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47609
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46297
47610
  color: "yellow"
46298
47611
  }, "⏳ saving…")) : null));
46299
47612
  }
47613
+ function ToggleRow({
47614
+ rowIdx,
47615
+ cursor,
47616
+ label,
47617
+ on,
47618
+ hintOn,
47619
+ hintOff
47620
+ }) {
47621
+ const selected = cursor === rowIdx;
47622
+ return /* @__PURE__ */ import_react29.default.createElement(Box_default, null, /* @__PURE__ */ import_react29.default.createElement(Text, {
47623
+ color: selected ? "cyan" : undefined,
47624
+ bold: selected
47625
+ }, selected ? "▸ " : " "), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
47626
+ width: 22
47627
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
47628
+ color: selected ? "cyan" : undefined
47629
+ }, label, ":")), /* @__PURE__ */ import_react29.default.createElement(Text, {
47630
+ color: on ? "green" : "gray",
47631
+ bold: true
47632
+ }, on ? "on" : "off"), /* @__PURE__ */ import_react29.default.createElement(Text, {
47633
+ dimColor: true
47634
+ }, " ", on ? hintOn : hintOff));
47635
+ }
46300
47636
 
46301
47637
  // src/app.tsx
46302
47638
  function App2({
46303
- session,
47639
+ session: initialSession,
46304
47640
  config: initialConfig,
46305
47641
  projectDir
46306
47642
  }) {
46307
47643
  const { exit } = use_app_default();
46308
- const [config, setConfig] = import_react29.useState(() => initialConfig);
46309
- const [showSettings, setShowSettings] = import_react29.useState(false);
46310
- const [appState, setAppState] = import_react29.useState("idle");
46311
- const [chineseInput, setChineseInput] = import_react29.useState("");
46312
- const [lastEnglish, setLastEnglish] = import_react29.useState("");
46313
- const [error, setError] = import_react29.useState(null);
46314
- const [statusLine, setStatusLine] = import_react29.useState(undefined);
46315
- const [outputEntries, setOutputEntries] = import_react29.useState([]);
46316
- const [pendingOutputCount, setPendingOutputCount] = import_react29.useState(0);
46317
- const [tailError, setTailError] = import_react29.useState(null);
46318
- const tailRef = import_react29.useRef(null);
46319
- const seenIdsRef = import_react29.useRef(new Set);
46320
- const [lastAssistantText, setLastAssistantText] = import_react29.useState("");
46321
- const [contextEnabled, setContextEnabled] = import_react29.useState(true);
46322
- const [lastRequest, setLastRequest] = import_react29.useState(null);
46323
- const [sessionMetrics, setSessionMetrics] = import_react29.useState({
47644
+ const [session, setSession] = import_react30.useState(() => initialSession);
47645
+ const [config, setConfig] = import_react30.useState(() => initialConfig);
47646
+ const [showSettings, setShowSettings] = import_react30.useState(false);
47647
+ const [appState, setAppState] = import_react30.useState("idle");
47648
+ const [chineseInput, setChineseInput] = import_react30.useState("");
47649
+ const [lastEnglish, setLastEnglish] = import_react30.useState("");
47650
+ const [error, setError] = import_react30.useState(null);
47651
+ const [statusLine, setStatusLine] = import_react30.useState(undefined);
47652
+ const [outputEntries, setOutputEntries] = import_react30.useState([]);
47653
+ const [streamingOutputs, setStreamingOutputs] = import_react30.useState([]);
47654
+ const [tailError, setTailError] = import_react30.useState(null);
47655
+ const [streamingEnglish, setStreamingEnglish] = import_react30.useState("");
47656
+ const [pendingConcern, setPendingConcern] = import_react30.useState(null);
47657
+ const translatedInputRef = import_react30.useRef("");
47658
+ const tailRef = import_react30.useRef(null);
47659
+ const seenIdsRef = import_react30.useRef(new Set);
47660
+ const enToZhQueueRef = import_react30.useRef(null);
47661
+ if (enToZhQueueRef.current === null) {
47662
+ enToZhQueueRef.current = new SerialQueue;
47663
+ }
47664
+ const notifyEnabledRef = import_react30.useRef(initialConfig.notify_on_complete);
47665
+ import_react30.useEffect(() => {
47666
+ notifyEnabledRef.current = config.notify_on_complete;
47667
+ }, [config.notify_on_complete]);
47668
+ const translateThinkingRef = import_react30.useRef(initialConfig.translate_thinking);
47669
+ import_react30.useEffect(() => {
47670
+ translateThinkingRef.current = config.translate_thinking;
47671
+ }, [config.translate_thinking]);
47672
+ const translateTodosRef = import_react30.useRef(initialConfig.translate_todos);
47673
+ import_react30.useEffect(() => {
47674
+ translateTodosRef.current = config.translate_todos;
47675
+ }, [config.translate_todos]);
47676
+ const translateAgentRef = import_react30.useRef(initialConfig.translate_agent);
47677
+ import_react30.useEffect(() => {
47678
+ translateAgentRef.current = config.translate_agent;
47679
+ }, [config.translate_agent]);
47680
+ const showToolCallsRef = import_react30.useRef(initialConfig.show_tool_calls);
47681
+ import_react30.useEffect(() => {
47682
+ showToolCallsRef.current = config.show_tool_calls;
47683
+ }, [config.show_tool_calls]);
47684
+ const bellRef = import_react30.useRef(null);
47685
+ if (bellRef.current === null) {
47686
+ bellRef.current = new DebouncedBell(2000, () => {
47687
+ if (notifyEnabledRef.current)
47688
+ ringBell();
47689
+ });
47690
+ }
47691
+ import_react30.useEffect(() => {
47692
+ const bell = bellRef.current;
47693
+ return () => {
47694
+ bell?.cancel();
47695
+ };
47696
+ }, []);
47697
+ const [lastAssistantText, setLastAssistantText] = import_react30.useState("");
47698
+ const [lastRequest, setLastRequest] = import_react30.useState(null);
47699
+ const [sessionMetrics, setSessionMetrics] = import_react30.useState({
46324
47700
  started_at: Date.now(),
46325
47701
  total_calls: 0,
46326
47702
  successful_calls: 0,
46327
47703
  error_count: 0,
46328
47704
  total_tokens: 0
46329
47705
  });
46330
- const historyRef = import_react29.useRef([]);
46331
- const historyCursorRef = import_react29.useRef(-1);
46332
- const HISTORY_MAX = 50;
46333
- import_react29.useEffect(() => {
47706
+ import_react30.useEffect(() => {
46334
47707
  const tail2 = new TranscriptTail(session.path, {
46335
- onText: (ev) => {
47708
+ onContent: (ev) => {
46336
47709
  if (seenIdsRef.current.has(ev.id))
46337
47710
  return;
46338
47711
  seenIdsRef.current.add(ev.id);
46339
- setLastAssistantText(ev.text);
46340
- translateAndAppend(ev);
47712
+ bellRef.current?.schedule();
47713
+ if (ev.kind === "text") {
47714
+ setLastAssistantText(ev.text);
47715
+ }
47716
+ if (ev.kind === "thinking" && !translateThinkingRef.current) {
47717
+ return;
47718
+ }
47719
+ if (ev.kind === "question") {
47720
+ const seed = seedTranslatedPayload(ev.question);
47721
+ setStreamingOutputs((arr) => [
47722
+ ...arr,
47723
+ {
47724
+ id: ev.id,
47725
+ timestamp: ev.timestamp,
47726
+ kind: "question",
47727
+ englishQuestion: ev.question,
47728
+ chineseQuestion: seed
47729
+ }
47730
+ ]);
47731
+ enToZhQueueRef.current.enqueue(() => translateQuestionAndAppend(ev, seed));
47732
+ return;
47733
+ }
47734
+ if (ev.kind === "todo") {
47735
+ if (!translateTodosRef.current)
47736
+ return;
47737
+ const seed = seedTranslatedTodos(ev.todo);
47738
+ setStreamingOutputs((arr) => [
47739
+ ...arr,
47740
+ {
47741
+ id: ev.id,
47742
+ timestamp: ev.timestamp,
47743
+ kind: "todo",
47744
+ englishTodo: ev.todo,
47745
+ chineseTodo: seed
47746
+ }
47747
+ ]);
47748
+ enToZhQueueRef.current.enqueue(() => translateTodoAndAppend(ev, seed));
47749
+ return;
47750
+ }
47751
+ if (ev.kind === "agent") {
47752
+ if (!translateAgentRef.current)
47753
+ return;
47754
+ const seed = seedTranslatedAgent(ev.agent);
47755
+ setStreamingOutputs((arr) => [
47756
+ ...arr,
47757
+ {
47758
+ id: ev.id,
47759
+ timestamp: ev.timestamp,
47760
+ kind: "agent",
47761
+ englishAgent: ev.agent,
47762
+ chineseAgent: seed
47763
+ }
47764
+ ]);
47765
+ enToZhQueueRef.current.enqueue(() => translateAgentAndAppend(ev, seed));
47766
+ return;
47767
+ }
47768
+ if (ev.kind === "tool_use" || ev.kind === "tool_result") {
47769
+ if (!showToolCallsRef.current)
47770
+ return;
47771
+ enToZhQueueRef.current.enqueue(async () => {
47772
+ appendRawToolEntry(ev);
47773
+ });
47774
+ return;
47775
+ }
47776
+ setStreamingOutputs((arr) => [
47777
+ ...arr,
47778
+ {
47779
+ id: ev.id,
47780
+ timestamp: ev.timestamp,
47781
+ kind: ev.kind,
47782
+ english: ev.text,
47783
+ chineseSoFar: ""
47784
+ }
47785
+ ]);
47786
+ enToZhQueueRef.current.enqueue(() => translateAndAppend(ev));
46341
47787
  },
46342
47788
  onError: (err) => {
46343
47789
  setTailError(err.message);
@@ -46355,46 +47801,334 @@ function App2({
46355
47801
  };
46356
47802
  }, [session.path]);
46357
47803
  async function translateAndAppend(ev) {
46358
- setPendingOutputCount((c) => c + 1);
46359
47804
  markRequestStarted("en-to-zh");
46360
47805
  try {
46361
- const result2 = await translate(ev.text, "en-to-zh", config);
47806
+ const result2 = await translate(ev.text, "en-to-zh", config, {
47807
+ onDelta: (delta) => {
47808
+ setStreamingOutputs((arr) => arr.map((e) => e.id === ev.id && (e.kind === "text" || e.kind === "thinking") ? { ...e, chineseSoFar: e.chineseSoFar + delta } : e));
47809
+ }
47810
+ });
46362
47811
  appendHistory(projectDir, {
46363
47812
  timestamp: new Date().toISOString(),
46364
47813
  direction: "en-to-zh",
46365
47814
  input: ev.text,
46366
47815
  output: result2.content
46367
47816
  });
47817
+ setStreamingOutputs((arr) => arr.filter((e) => e.id !== ev.id));
47818
+ setOutputEntries((entries) => [
47819
+ ...entries,
47820
+ {
47821
+ id: ev.id,
47822
+ timestamp: ev.timestamp,
47823
+ kind: ev.kind,
47824
+ english: ev.text,
47825
+ chinese: result2.content,
47826
+ state: "translated"
47827
+ }
47828
+ ]);
47829
+ markRequestSuccess("en-to-zh", result2.elapsed_ms, result2.usage?.total_tokens ?? null);
47830
+ } catch (err) {
47831
+ const te = err instanceof TranslateError ? err : null;
47832
+ const msg = te?.message ?? err.message;
47833
+ setStreamingOutputs((arr) => arr.filter((e) => e.id !== ev.id));
47834
+ setOutputEntries((entries) => [
47835
+ ...entries,
47836
+ {
47837
+ id: ev.id,
47838
+ timestamp: ev.timestamp,
47839
+ kind: ev.kind,
47840
+ english: ev.text,
47841
+ chinese: "",
47842
+ state: "error",
47843
+ error: msg
47844
+ }
47845
+ ]);
47846
+ markRequestError("en-to-zh", te?.elapsed_ms ?? 0, te?.code ?? "error", msg);
47847
+ }
47848
+ }
47849
+ async function translateQuestionAndAppend(ev, seed) {
47850
+ markRequestStarted("en-to-zh");
47851
+ let totalElapsed = 0;
47852
+ let totalTokens = 0;
47853
+ const pending = [];
47854
+ for (let qi = 0;qi < seed.questions.length; qi += 1) {
47855
+ const q = seed.questions[qi];
47856
+ if (!q.question.zh) {
47857
+ pending.push({
47858
+ en: q.question.en,
47859
+ patch: (zh, cur) => patchField(cur, qi, "question", null, zh)
47860
+ });
47861
+ }
47862
+ for (let oi = 0;oi < q.options.length; oi += 1) {
47863
+ const opt = q.options[oi];
47864
+ if (!opt.label.zh) {
47865
+ pending.push({
47866
+ en: opt.label.en,
47867
+ patch: (zh, cur) => patchField(cur, qi, "label", oi, zh)
47868
+ });
47869
+ }
47870
+ if (!opt.description.zh) {
47871
+ pending.push({
47872
+ en: opt.description.en,
47873
+ patch: (zh, cur) => patchField(cur, qi, "description", oi, zh)
47874
+ });
47875
+ }
47876
+ }
47877
+ }
47878
+ try {
47879
+ for (const p of pending) {
47880
+ const result2 = await translate(p.en, "en-to-zh", config);
47881
+ totalElapsed += result2.elapsed_ms;
47882
+ totalTokens += result2.usage?.total_tokens ?? 0;
47883
+ appendHistory(projectDir, {
47884
+ timestamp: new Date().toISOString(),
47885
+ direction: "en-to-zh",
47886
+ input: p.en,
47887
+ output: result2.content
47888
+ });
47889
+ setStreamingOutputs((arr) => arr.map((e) => e.id === ev.id && e.kind === "question" ? { ...e, chineseQuestion: p.patch(result2.content, e.chineseQuestion) } : e));
47890
+ }
47891
+ setStreamingOutputs((arr) => {
47892
+ const finalEntry = arr.find((e) => e.id === ev.id && e.kind === "question");
47893
+ if (finalEntry && finalEntry.kind === "question") {
47894
+ setOutputEntries((entries) => [
47895
+ ...entries,
47896
+ {
47897
+ id: ev.id,
47898
+ timestamp: ev.timestamp,
47899
+ kind: "question",
47900
+ englishQuestion: ev.question,
47901
+ chineseQuestion: finalEntry.chineseQuestion,
47902
+ state: "translated"
47903
+ }
47904
+ ]);
47905
+ }
47906
+ return arr.filter((e) => e.id !== ev.id);
47907
+ });
47908
+ markRequestSuccess("en-to-zh", totalElapsed, totalTokens);
47909
+ } catch (err) {
47910
+ const te = err instanceof TranslateError ? err : null;
47911
+ const msg = te?.message ?? err.message;
47912
+ setStreamingOutputs((arr) => arr.filter((e) => e.id !== ev.id));
47913
+ setOutputEntries((entries) => [
47914
+ ...entries,
47915
+ {
47916
+ id: ev.id,
47917
+ timestamp: ev.timestamp,
47918
+ kind: "question",
47919
+ englishQuestion: ev.question,
47920
+ chineseQuestion: seed,
47921
+ state: "error",
47922
+ error: msg
47923
+ }
47924
+ ]);
47925
+ markRequestError("en-to-zh", totalElapsed + (te?.elapsed_ms ?? 0), te?.code ?? "error", msg);
47926
+ }
47927
+ }
47928
+ async function translateTodoAndAppend(ev, seed) {
47929
+ markRequestStarted("en-to-zh");
47930
+ let totalElapsed = 0;
47931
+ let totalTokens = 0;
47932
+ const pending = [];
47933
+ for (let i = 0;i < seed.todos.length; i += 1) {
47934
+ const t = seed.todos[i];
47935
+ const which = t.status === "in_progress" ? "activeForm" : "content";
47936
+ const f = which === "activeForm" ? t.activeForm : t.content;
47937
+ if (!f.zh) {
47938
+ pending.push({
47939
+ en: f.en,
47940
+ patch: (zh, cur) => patchTodoField(cur, i, which, zh)
47941
+ });
47942
+ }
47943
+ }
47944
+ try {
47945
+ for (const p of pending) {
47946
+ const result2 = await translate(p.en, "en-to-zh", config);
47947
+ totalElapsed += result2.elapsed_ms;
47948
+ totalTokens += result2.usage?.total_tokens ?? 0;
47949
+ appendHistory(projectDir, {
47950
+ timestamp: new Date().toISOString(),
47951
+ direction: "en-to-zh",
47952
+ input: p.en,
47953
+ output: result2.content
47954
+ });
47955
+ setStreamingOutputs((arr) => arr.map((e) => e.id === ev.id && e.kind === "todo" ? { ...e, chineseTodo: p.patch(result2.content, e.chineseTodo) } : e));
47956
+ }
47957
+ setStreamingOutputs((arr) => {
47958
+ const finalEntry = arr.find((e) => e.id === ev.id && e.kind === "todo");
47959
+ if (finalEntry && finalEntry.kind === "todo") {
47960
+ setOutputEntries((entries) => [
47961
+ ...entries,
47962
+ {
47963
+ id: ev.id,
47964
+ timestamp: ev.timestamp,
47965
+ kind: "todo",
47966
+ englishTodo: ev.todo,
47967
+ chineseTodo: finalEntry.chineseTodo,
47968
+ state: "translated"
47969
+ }
47970
+ ]);
47971
+ }
47972
+ return arr.filter((e) => e.id !== ev.id);
47973
+ });
47974
+ markRequestSuccess("en-to-zh", totalElapsed, totalTokens);
47975
+ } catch (err) {
47976
+ const te = err instanceof TranslateError ? err : null;
47977
+ const msg = te?.message ?? err.message;
47978
+ setStreamingOutputs((arr) => arr.filter((e) => e.id !== ev.id));
46368
47979
  setOutputEntries((entries) => [
46369
47980
  ...entries,
46370
47981
  {
46371
47982
  id: ev.id,
46372
47983
  timestamp: ev.timestamp,
46373
- english: ev.text,
46374
- chinese: result2.content,
46375
- state: "translated"
47984
+ kind: "todo",
47985
+ englishTodo: ev.todo,
47986
+ chineseTodo: seed,
47987
+ state: "error",
47988
+ error: msg
46376
47989
  }
46377
47990
  ]);
46378
- markRequestSuccess("en-to-zh", result2.elapsed_ms, result2.usage?.total_tokens ?? null);
47991
+ markRequestError("en-to-zh", totalElapsed + (te?.elapsed_ms ?? 0), te?.code ?? "error", msg);
47992
+ }
47993
+ }
47994
+ async function translateAgentAndAppend(ev, seed) {
47995
+ markRequestStarted("en-to-zh");
47996
+ let totalElapsed = 0;
47997
+ let totalTokens = 0;
47998
+ try {
47999
+ if (!seed.description.zh) {
48000
+ const result2 = await translate(seed.description.en, "en-to-zh", config);
48001
+ totalElapsed += result2.elapsed_ms;
48002
+ totalTokens += result2.usage?.total_tokens ?? 0;
48003
+ appendHistory(projectDir, {
48004
+ timestamp: new Date().toISOString(),
48005
+ direction: "en-to-zh",
48006
+ input: seed.description.en,
48007
+ output: result2.content
48008
+ });
48009
+ setStreamingOutputs((arr) => arr.map((e) => e.id === ev.id && e.kind === "agent" ? {
48010
+ ...e,
48011
+ chineseAgent: {
48012
+ ...e.chineseAgent,
48013
+ description: { en: e.chineseAgent.description.en, zh: result2.content }
48014
+ }
48015
+ } : e));
48016
+ }
48017
+ if (!seed.prompt.zh) {
48018
+ const result2 = await translate(seed.prompt.en, "en-to-zh", config, {
48019
+ onDelta: (delta) => {
48020
+ setStreamingOutputs((arr) => arr.map((e) => e.id === ev.id && e.kind === "agent" ? {
48021
+ ...e,
48022
+ chineseAgent: {
48023
+ ...e.chineseAgent,
48024
+ prompt: {
48025
+ en: e.chineseAgent.prompt.en,
48026
+ zh: e.chineseAgent.prompt.zh + delta
48027
+ }
48028
+ }
48029
+ } : e));
48030
+ }
48031
+ });
48032
+ totalElapsed += result2.elapsed_ms;
48033
+ totalTokens += result2.usage?.total_tokens ?? 0;
48034
+ appendHistory(projectDir, {
48035
+ timestamp: new Date().toISOString(),
48036
+ direction: "en-to-zh",
48037
+ input: seed.prompt.en,
48038
+ output: result2.content
48039
+ });
48040
+ }
48041
+ setStreamingOutputs((arr) => {
48042
+ const finalEntry = arr.find((e) => e.id === ev.id && e.kind === "agent");
48043
+ if (finalEntry && finalEntry.kind === "agent") {
48044
+ setOutputEntries((entries) => [
48045
+ ...entries,
48046
+ {
48047
+ id: ev.id,
48048
+ timestamp: ev.timestamp,
48049
+ kind: "agent",
48050
+ englishAgent: ev.agent,
48051
+ chineseAgent: finalEntry.chineseAgent,
48052
+ state: "translated"
48053
+ }
48054
+ ]);
48055
+ }
48056
+ return arr.filter((e) => e.id !== ev.id);
48057
+ });
48058
+ markRequestSuccess("en-to-zh", totalElapsed, totalTokens);
46379
48059
  } catch (err) {
46380
48060
  const te = err instanceof TranslateError ? err : null;
46381
48061
  const msg = te?.message ?? err.message;
48062
+ setStreamingOutputs((arr) => arr.filter((e) => e.id !== ev.id));
46382
48063
  setOutputEntries((entries) => [
46383
48064
  ...entries,
46384
48065
  {
46385
48066
  id: ev.id,
46386
48067
  timestamp: ev.timestamp,
46387
- english: ev.text,
46388
- chinese: "",
48068
+ kind: "agent",
48069
+ englishAgent: ev.agent,
48070
+ chineseAgent: seed,
46389
48071
  state: "error",
46390
48072
  error: msg
46391
48073
  }
46392
48074
  ]);
46393
- markRequestError("en-to-zh", te?.elapsed_ms ?? 0, te?.code ?? "error", msg);
46394
- } finally {
46395
- setPendingOutputCount((c) => Math.max(0, c - 1));
48075
+ markRequestError("en-to-zh", totalElapsed + (te?.elapsed_ms ?? 0), te?.code ?? "error", msg);
48076
+ }
48077
+ }
48078
+ function appendRawToolEntry(ev) {
48079
+ if (ev.kind === "tool_use") {
48080
+ setOutputEntries((entries) => [
48081
+ ...entries,
48082
+ {
48083
+ id: ev.id,
48084
+ timestamp: ev.timestamp,
48085
+ kind: "tool_use",
48086
+ toolUse: ev.toolUse,
48087
+ state: "translated"
48088
+ }
48089
+ ]);
48090
+ } else {
48091
+ setOutputEntries((entries) => [
48092
+ ...entries,
48093
+ {
48094
+ id: ev.id,
48095
+ timestamp: ev.timestamp,
48096
+ kind: "tool_result",
48097
+ toolResult: ev.toolResult,
48098
+ state: "translated"
48099
+ }
48100
+ ]);
46396
48101
  }
46397
48102
  }
48103
+ function handleSessionChange(next) {
48104
+ try {
48105
+ writeState(projectDir, {
48106
+ selected_session_id: next.id,
48107
+ selected_at: new Date().toISOString(),
48108
+ transcript_path: next.path
48109
+ });
48110
+ } catch (err) {
48111
+ setStatusLine(`✗ failed to persist session pick: ${err.message}`);
48112
+ }
48113
+ seenIdsRef.current.clear();
48114
+ setLastAssistantText("");
48115
+ bellRef.current?.cancel();
48116
+ const shortPrev = session.id.slice(0, 8);
48117
+ const shortNext = next.id.slice(0, 8);
48118
+ setOutputEntries((entries) => [
48119
+ ...entries,
48120
+ {
48121
+ id: `__divider_${Date.now()}`,
48122
+ timestamp: new Date().toISOString(),
48123
+ kind: "text",
48124
+ english: "",
48125
+ chinese: `已切换 session: ${shortPrev}… → ${shortNext}…`,
48126
+ state: "translated"
48127
+ }
48128
+ ]);
48129
+ setSession(next);
48130
+ setStatusLine(`✓ switched to session ${shortNext}…`);
48131
+ }
46398
48132
  async function saveSettingsFromModal(s) {
46399
48133
  const path = userConfigPath();
46400
48134
  let existing = {};
@@ -46410,7 +48144,13 @@ function App2({
46410
48144
  api_key: s.api_key,
46411
48145
  base_url: s.base_url,
46412
48146
  model: s.model,
46413
- timeout_seconds: Number(s.timeout_seconds)
48147
+ timeout_seconds: Number(s.timeout_seconds),
48148
+ notify_on_complete: s.notify_on_complete,
48149
+ translate_thinking: s.translate_thinking,
48150
+ translate_todos: s.translate_todos,
48151
+ translate_agent: s.translate_agent,
48152
+ context_enabled: s.context_enabled,
48153
+ show_tool_calls: s.show_tool_calls
46414
48154
  };
46415
48155
  const promptsClean = stripEmpty(s.prompts);
46416
48156
  if (promptsClean)
@@ -46418,9 +48158,8 @@ function App2({
46418
48158
  else
46419
48159
  delete merged["prompts"];
46420
48160
  try {
46421
- mkdirSync3(dirname3(path), { recursive: true });
46422
- writeFileSync3(path, JSON.stringify(merged, null, 2) + `
46423
- `, "utf-8");
48161
+ writeAtomic(path, JSON.stringify(merged, null, 2) + `
48162
+ `, { mode: 384 });
46424
48163
  } catch (err) {
46425
48164
  setStatusLine(`✗ save failed: ${err.message}`);
46426
48165
  setShowSettings(false);
@@ -46487,63 +48226,89 @@ function App2({
46487
48226
  setError(null);
46488
48227
  setStatusLine(undefined);
46489
48228
  setAppState("idle");
46490
- historyCursorRef.current = -1;
48229
+ setPendingConcern(null);
48230
+ translatedInputRef.current = "";
46491
48231
  return;
46492
48232
  }
46493
- if (key.ctrl && input === "l") {
48233
+ });
48234
+ function commitTranslation(trimmedInput, en) {
48235
+ try {
48236
+ const path = writeUserInput(projectDir, en);
48237
+ appendHistory(projectDir, {
48238
+ timestamp: new Date().toISOString(),
48239
+ direction: "zh-to-en",
48240
+ input: trimmedInput,
48241
+ output: en
48242
+ });
48243
+ setStreamingEnglish("");
48244
+ setLastEnglish(en);
48245
+ setStatusLine(`✓ wrote ${path} — switch to Claude Code, type /i ↵`);
46494
48246
  setChineseInput("");
46495
- historyCursorRef.current = -1;
46496
- return;
46497
- }
46498
- if (key.ctrl && input === "t") {
46499
- setContextEnabled((c) => !c);
46500
- return;
46501
- }
46502
- if (key.ctrl && input === "p") {
46503
- const h = historyRef.current;
46504
- if (h.length === 0)
46505
- return;
46506
- let cur = historyCursorRef.current;
46507
- if (cur === -1)
46508
- cur = h.length - 1;
46509
- else if (cur > 0)
46510
- cur -= 1;
46511
- historyCursorRef.current = cur;
46512
- setChineseInput(h[cur] ?? "");
46513
- return;
46514
- }
46515
- if (key.ctrl && input === "n") {
46516
- const h = historyRef.current;
46517
- const cur = historyCursorRef.current;
46518
- if (h.length === 0 || cur === -1)
46519
- return;
46520
- if (cur < h.length - 1) {
46521
- const next = cur + 1;
46522
- historyCursorRef.current = next;
46523
- setChineseInput(h[next] ?? "");
46524
- } else {
46525
- historyCursorRef.current = -1;
46526
- setChineseInput("");
46527
- }
46528
- return;
48247
+ setPendingConcern(null);
48248
+ translatedInputRef.current = "";
48249
+ setAppState("idle");
48250
+ } catch (err) {
48251
+ const msg = err instanceof Error ? err.message : String(err);
48252
+ setError(`failed to write user-input.md: ${msg}`);
48253
+ setAppState("error");
46529
48254
  }
46530
- });
48255
+ }
46531
48256
  async function handleSubmit() {
46532
48257
  const trimmed = chineseInput.trim();
46533
48258
  if (!trimmed)
46534
48259
  return;
46535
48260
  if (appState !== "idle")
46536
48261
  return;
48262
+ if (pendingConcern && trimmed === translatedInputRef.current) {
48263
+ commitTranslation(trimmed, lastEnglish);
48264
+ return;
48265
+ }
48266
+ if (pendingConcern) {
48267
+ setPendingConcern(null);
48268
+ translatedInputRef.current = "";
48269
+ }
46537
48270
  setAppState("translating");
46538
48271
  setError(null);
46539
48272
  setStatusLine(undefined);
46540
48273
  setLastEnglish("");
48274
+ setStreamingEnglish("");
48275
+ const ctx = config.context_enabled && lastAssistantText.trim() ? lastAssistantText : undefined;
48276
+ const useContext7 = ctx !== undefined;
48277
+ let buf = "";
48278
+ let translationStart = -1;
46541
48279
  let en;
48280
+ let concern = null;
46542
48281
  markRequestStarted("zh-to-en");
46543
48282
  try {
46544
- const ctx = contextEnabled && lastAssistantText.trim() ? lastAssistantText : undefined;
46545
- const result2 = await translate(trimmed, "zh-to-en", config, ctx);
48283
+ const result2 = await translate(trimmed, "zh-to-en", config, {
48284
+ contextText: ctx,
48285
+ parseConcernHeader: useContext7,
48286
+ onDelta: (delta) => {
48287
+ buf += delta;
48288
+ if (!useContext7) {
48289
+ setStreamingEnglish(buf);
48290
+ return;
48291
+ }
48292
+ if (translationStart === -1) {
48293
+ const nl = buf.indexOf(`
48294
+ `);
48295
+ if (nl === -1)
48296
+ return;
48297
+ const firstLine = buf.slice(0, nl).trim();
48298
+ if (firstLine === "OK" || firstLine.startsWith("WARN:")) {
48299
+ let i = nl + 1;
48300
+ while (i < buf.length && /\s/.test(buf[i]))
48301
+ i += 1;
48302
+ translationStart = i;
48303
+ } else {
48304
+ translationStart = 0;
48305
+ }
48306
+ }
48307
+ setStreamingEnglish(buf.slice(translationStart));
48308
+ }
48309
+ });
46546
48310
  en = result2.content;
48311
+ concern = result2.concern ?? null;
46547
48312
  markRequestSuccess("zh-to-en", result2.elapsed_ms, result2.usage?.total_tokens ?? null);
46548
48313
  } catch (err) {
46549
48314
  const te = err instanceof TranslateError ? err : null;
@@ -46551,34 +48316,20 @@ function App2({
46551
48316
  markRequestError("zh-to-en", te?.elapsed_ms ?? 0, te?.code ?? "error", msg);
46552
48317
  setError(msg);
46553
48318
  setAppState("error");
48319
+ setStreamingEnglish("");
46554
48320
  return;
46555
48321
  }
46556
- try {
46557
- const path = writeUserInput(projectDir, en);
46558
- appendHistory(projectDir, {
46559
- timestamp: new Date().toISOString(),
46560
- direction: "zh-to-en",
46561
- input: trimmed,
46562
- output: en
46563
- });
46564
- const h = historyRef.current;
46565
- if (h[h.length - 1] !== trimmed) {
46566
- h.push(trimmed);
46567
- if (h.length > HISTORY_MAX)
46568
- h.shift();
46569
- }
46570
- historyCursorRef.current = -1;
48322
+ if (concern) {
48323
+ setPendingConcern(concern);
46571
48324
  setLastEnglish(en);
46572
- setStatusLine(`✓ wrote ${path} — switch to Claude Code, type /i ↵`);
46573
- setChineseInput("");
48325
+ setStreamingEnglish("");
48326
+ translatedInputRef.current = trimmed;
46574
48327
  setAppState("idle");
46575
- } catch (err) {
46576
- const msg = err instanceof Error ? err.message : String(err);
46577
- setError(`failed to write user-input.md: ${msg}`);
46578
- setAppState("error");
48328
+ return;
46579
48329
  }
48330
+ commitTranslation(trimmed, en);
46580
48331
  }
46581
- import_react29.useEffect(() => {
48332
+ import_react30.useEffect(() => {
46582
48333
  if (statusLine && appState === "idle") {
46583
48334
  const t = setTimeout(() => setStatusLine(undefined), 8000);
46584
48335
  return () => clearTimeout(t);
@@ -46588,35 +48339,40 @@ function App2({
46588
48339
  const inputDisabled = appState === "translating";
46589
48340
  const combinedStatus = statusLine ?? (tailError ? `tail error: ${tailError}` : undefined);
46590
48341
  if (showSettings) {
46591
- return /* @__PURE__ */ import_react29.default.createElement(SettingsModal, {
48342
+ return /* @__PURE__ */ import_react30.default.createElement(SettingsModal, {
46592
48343
  initialConfig: config,
46593
- onSave: saveSettingsFromModal
48344
+ onSave: saveSettingsFromModal,
48345
+ currentSession: session,
48346
+ projectDir,
48347
+ onSessionChange: handleSessionChange
46594
48348
  });
46595
48349
  }
46596
- return /* @__PURE__ */ import_react29.default.createElement(import_react29.default.Fragment, null, /* @__PURE__ */ import_react29.default.createElement(OutputPane, {
48350
+ return /* @__PURE__ */ import_react30.default.createElement(import_react30.default.Fragment, null, /* @__PURE__ */ import_react30.default.createElement(OutputPane, {
46597
48351
  entries: outputEntries,
46598
- pendingCount: pendingOutputCount
46599
- }), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
48352
+ streaming: streamingOutputs
48353
+ }), /* @__PURE__ */ import_react30.default.createElement(Box_default, {
46600
48354
  flexDirection: "column"
46601
- }, /* @__PURE__ */ import_react29.default.createElement(StatusBar, {
48355
+ }, /* @__PURE__ */ import_react30.default.createElement(StatusBar, {
46602
48356
  session,
46603
48357
  appState,
46604
48358
  model: config.model,
46605
48359
  statusLine: combinedStatus,
46606
- contextEnabled,
48360
+ contextEnabled: config.context_enabled,
46607
48361
  contextSize: lastAssistantText.length,
46608
48362
  lastRequest,
46609
48363
  sessionMetrics
46610
- }), /* @__PURE__ */ import_react29.default.createElement(InputPane, {
48364
+ }), /* @__PURE__ */ import_react30.default.createElement(InputPane, {
46611
48365
  value: chineseInput,
46612
48366
  onChange: setChineseInput,
46613
48367
  onSubmit: () => void handleSubmit(),
46614
48368
  disabled: inputDisabled,
46615
48369
  placeholder: inputDisabled ? "" : "输入中文,回车直接翻译并发送…"
46616
- }), /* @__PURE__ */ import_react29.default.createElement(PreviewPane, {
48370
+ }), /* @__PURE__ */ import_react30.default.createElement(PreviewPane, {
46617
48371
  text: appState === "idle" ? lastEnglish : "",
46618
48372
  translating: appState === "translating",
46619
- error: appState === "error" ? error : null
48373
+ streamingText: streamingEnglish,
48374
+ error: appState === "error" ? error : null,
48375
+ concern: pendingConcern
46620
48376
  })));
46621
48377
  }
46622
48378
  function stripEmpty(map2) {
@@ -46627,128 +48383,144 @@ function stripEmpty(map2) {
46627
48383
  }
46628
48384
  return Object.keys(out).length > 0 ? out : undefined;
46629
48385
  }
46630
-
46631
- // src/components/session-picker.tsx
46632
- var import_react30 = __toESM(require_react(), 1);
46633
- function shortId2(id) {
46634
- if (id.length <= 12)
46635
- return id;
46636
- return id.slice(0, 8) + "..." + id.slice(-4);
48386
+ function seedTranslatedPayload(raw) {
48387
+ const seedOption = (o) => {
48388
+ const out = {
48389
+ label: seedString(o.label),
48390
+ description: seedString(o.description)
48391
+ };
48392
+ if (o.preview !== undefined)
48393
+ out.preview = o.preview;
48394
+ return out;
48395
+ };
48396
+ return {
48397
+ questions: raw.questions.map((q) => ({
48398
+ question: seedString(q.question),
48399
+ header: q.header,
48400
+ multiSelect: q.multiSelect,
48401
+ options: q.options.map(seedOption)
48402
+ }))
48403
+ };
46637
48404
  }
46638
- function SessionPicker({ sessions, onPick, onCancel }) {
46639
- const [cursor, setCursor] = import_react30.useState(0);
46640
- const [search, setSearch] = import_react30.useState("");
46641
- const [searching, setSearching] = import_react30.useState(false);
46642
- const filtered = search ? sessions.filter((s) => {
46643
- const haystack = [
46644
- s.id,
46645
- s.first_message ?? "",
46646
- s.started_at ?? "",
46647
- s.last_activity_at ?? ""
46648
- ].join(" ").toLowerCase();
46649
- return haystack.includes(search.toLowerCase());
46650
- }) : sessions;
46651
- use_input_default((input, key) => {
46652
- if (searching) {
46653
- if (key.escape) {
46654
- setSearching(false);
46655
- setSearch("");
46656
- return;
46657
- }
46658
- if (key.return) {
46659
- setSearching(false);
46660
- return;
46661
- }
46662
- if (key.backspace || key.delete) {
46663
- setSearch((s) => s.slice(0, -1));
46664
- return;
46665
- }
46666
- if (input && !key.ctrl && !key.meta) {
46667
- setSearch((s) => s + input);
48405
+ function seedTranslatedTodos(raw) {
48406
+ return {
48407
+ todos: raw.todos.map((t) => ({
48408
+ content: seedString(t.content),
48409
+ activeForm: seedString(t.activeForm),
48410
+ status: t.status
48411
+ }))
48412
+ };
48413
+ }
48414
+ function seedTranslatedAgent(raw) {
48415
+ return {
48416
+ description: seedString(raw.description),
48417
+ prompt: seedString(raw.prompt),
48418
+ subagent_type: raw.subagent_type
48419
+ };
48420
+ }
48421
+ function patchField(payload, qi, which, oi, zh) {
48422
+ return {
48423
+ questions: payload.questions.map((q, i) => {
48424
+ if (i !== qi)
48425
+ return q;
48426
+ if (which === "question") {
48427
+ return { ...q, question: { ...q.question, zh } };
46668
48428
  }
46669
- return;
46670
- }
46671
- if (key.escape) {
46672
- onCancel();
46673
- return;
46674
- }
46675
- if (key.upArrow) {
46676
- setCursor((c) => Math.max(0, c - 1));
46677
- return;
46678
- }
46679
- if (key.downArrow) {
46680
- setCursor((c) => Math.min(filtered.length - 1, c + 1));
46681
- return;
46682
- }
46683
- if (key.return) {
46684
- const target = filtered[cursor];
46685
- if (target)
46686
- onPick(target);
46687
- return;
46688
- }
46689
- if (input === "/") {
46690
- setSearching(true);
46691
- }
46692
- });
46693
- if (sessions.length === 0) {
46694
- return /* @__PURE__ */ import_react30.default.createElement(Box_default, {
46695
- flexDirection: "column",
46696
- padding: 1
46697
- }, /* @__PURE__ */ import_react30.default.createElement(Text, {
46698
- color: "yellow"
46699
- }, "No Claude Code sessions found in this project."), /* @__PURE__ */ import_react30.default.createElement(Text, {
46700
- dimColor: true
46701
- }, "Start `claude` in this project first, then run `cc-zh-watcher` again."), /* @__PURE__ */ import_react30.default.createElement(Text, {
46702
- dimColor: true
46703
- }, "Press Esc to exit."));
46704
- }
46705
- const visible = filtered.slice(0, 8);
46706
- return /* @__PURE__ */ import_react30.default.createElement(Box_default, {
46707
- flexDirection: "column"
46708
- }, /* @__PURE__ */ import_react30.default.createElement(Box_default, {
46709
- borderStyle: "round",
46710
- borderColor: "cyan",
46711
- flexDirection: "column",
46712
- paddingX: 1
46713
- }, /* @__PURE__ */ import_react30.default.createElement(Text, {
46714
- bold: true,
46715
- color: "cyan"
46716
- }, "Select a Claude Code session to watch"), /* @__PURE__ */ import_react30.default.createElement(Text, {
46717
- dimColor: true
46718
- }, "project: ", process.cwd(), " · ", sessions.length, " session", sessions.length === 1 ? "" : "s"), /* @__PURE__ */ import_react30.default.createElement(Box_default, {
46719
- marginTop: 1,
46720
- flexDirection: "column"
46721
- }, visible.map((s, i) => {
46722
- const selected = i === cursor;
46723
- return /* @__PURE__ */ import_react30.default.createElement(Box_default, {
46724
- key: s.id,
46725
- flexDirection: "column",
46726
- marginBottom: 1
46727
- }, /* @__PURE__ */ import_react30.default.createElement(Text, {
46728
- color: selected ? "cyan" : undefined,
46729
- bold: selected
46730
- }, selected ? "⮕ " : " ", i + 1, ". ", shortId2(s.id), " [active", " ", relativeTime(s.last_activity_at), " · ", s.turn_count, " turn", s.turn_count === 1 ? "" : "s", "]"), /* @__PURE__ */ import_react30.default.createElement(Text, {
46731
- dimColor: true
46732
- }, " started: ", formatLocalTime(s.started_at)), s.first_message ? /* @__PURE__ */ import_react30.default.createElement(Text, {
46733
- dimColor: true
46734
- }, " first msg: ", JSON.stringify(s.first_message)) : null);
46735
- }), filtered.length > visible.length ? /* @__PURE__ */ import_react30.default.createElement(Text, {
46736
- dimColor: true
46737
- }, "... and ", filtered.length - visible.length, " more (use / to search)") : null), /* @__PURE__ */ import_react30.default.createElement(Box_default, {
46738
- marginTop: 1
46739
- }, searching ? /* @__PURE__ */ import_react30.default.createElement(Text, null, /* @__PURE__ */ import_react30.default.createElement(Text, {
46740
- color: "yellow"
46741
- }, "/"), search, /* @__PURE__ */ import_react30.default.createElement(Text, {
46742
- color: "yellow"
46743
- }, "_"), /* @__PURE__ */ import_react30.default.createElement(Text, {
46744
- dimColor: true
46745
- }, " (Esc clear · Enter accept)")) : /* @__PURE__ */ import_react30.default.createElement(Text, {
46746
- dimColor: true
46747
- }, "[↑/↓] move [Enter] select [/] search [Esc] cancel"))));
48429
+ return {
48430
+ ...q,
48431
+ options: q.options.map((o, j) => {
48432
+ if (j !== oi)
48433
+ return o;
48434
+ if (which === "label")
48435
+ return { ...o, label: { ...o.label, zh } };
48436
+ return { ...o, description: { ...o.description, zh } };
48437
+ })
48438
+ };
48439
+ })
48440
+ };
48441
+ }
48442
+ function patchTodoField(payload, i, which, zh) {
48443
+ return {
48444
+ todos: payload.todos.map((t, j) => {
48445
+ if (j !== i)
48446
+ return t;
48447
+ if (which === "content")
48448
+ return { ...t, content: { ...t.content, zh } };
48449
+ return { ...t, activeForm: { ...t.activeForm, zh } };
48450
+ })
48451
+ };
46748
48452
  }
48453
+ // package.json
48454
+ var package_default = {
48455
+ name: "cc-zh-watcher",
48456
+ version: "0.2.1",
48457
+ description: "Side-channel Chinese ↔ English bridge for Claude Code (single-watcher TUI). Type Chinese, Claude sees English; Claude replies in English, you see Chinese.",
48458
+ type: "module",
48459
+ bin: {
48460
+ "cc-zh-watcher": "dist/cli.js"
48461
+ },
48462
+ main: "./dist/cli.js",
48463
+ files: [
48464
+ "dist/cli.js",
48465
+ "README.md",
48466
+ "LICENSE"
48467
+ ],
48468
+ scripts: {
48469
+ dev: "bun run src/cli.ts",
48470
+ build: "bun build src/cli.ts --outfile dist/cli.js --target node",
48471
+ compile: "bun build src/cli.ts --compile --outfile dist/cc-zh-watcher",
48472
+ "compile:linux-x64": "bun build src/cli.ts --compile --target=bun-linux-x64 --outfile dist/cc-zh-watcher-linux-x64",
48473
+ "compile:linux-arm64": "bun build src/cli.ts --compile --target=bun-linux-arm64 --outfile dist/cc-zh-watcher-linux-arm64",
48474
+ "compile:darwin-x64": "bun build src/cli.ts --compile --target=bun-darwin-x64 --outfile dist/cc-zh-watcher-darwin-x64",
48475
+ "compile:darwin-arm64": "bun build src/cli.ts --compile --target=bun-darwin-arm64 --outfile dist/cc-zh-watcher-darwin-arm64",
48476
+ "compile:windows-x64": "bun build src/cli.ts --compile --target=bun-windows-x64 --outfile dist/cc-zh-watcher-windows-x64.exe",
48477
+ "compile:all": "bun run compile:linux-x64 && bun run compile:linux-arm64 && bun run compile:darwin-x64 && bun run compile:darwin-arm64 && bun run compile:windows-x64",
48478
+ test: "bun test",
48479
+ typecheck: "tsc --noEmit",
48480
+ prepublishOnly: "bun run typecheck && bun test && bun run build",
48481
+ "install:local": "bash scripts/install-local.sh",
48482
+ "uninstall:local": "npm uninstall -g cc-zh-watcher"
48483
+ },
48484
+ keywords: [
48485
+ "claude-code",
48486
+ "claude",
48487
+ "translation",
48488
+ "chinese",
48489
+ "i18n",
48490
+ "tui",
48491
+ "ink",
48492
+ "cli",
48493
+ "openai",
48494
+ "deepseek"
48495
+ ],
48496
+ author: "storm <xm4763@gmail.com>",
48497
+ license: "MIT",
48498
+ repository: {
48499
+ type: "git",
48500
+ url: "git+https://github.com/CodingForMoney/cc-zh-watcher.git"
48501
+ },
48502
+ homepage: "https://github.com/CodingForMoney/cc-zh-watcher/#readme",
48503
+ bugs: {
48504
+ url: "https://github.com/CodingForMoney/cc-zh-watcher/issues"
48505
+ },
48506
+ engines: {
48507
+ node: ">=20.0.0"
48508
+ },
48509
+ devDependencies: {
48510
+ "@types/node": "^20.14.0",
48511
+ "@types/react": "^18.3.0",
48512
+ "bun-types": "^1.3.13",
48513
+ commander: "^12.1.0",
48514
+ ink: "^5.0.1",
48515
+ "ink-text-input": "^6.0.0",
48516
+ react: "^18.3.1",
48517
+ "react-devtools-core": "^7.0.1",
48518
+ typescript: "^5.5.0"
48519
+ }
48520
+ };
46749
48521
 
46750
48522
  // src/cli.ts
46751
- var VERSION = "0.1.0";
48523
+ var VERSION = package_default.version;
46752
48524
  function fail(msg, code = 1) {
46753
48525
  process.stderr.write(`error: ${msg}
46754
48526
  `);
@@ -46817,37 +48589,42 @@ async function resolveSession(opts) {
46817
48589
  return picked;
46818
48590
  }
46819
48591
  async function runWatch(opts) {
46820
- const projectDir = process.cwd();
48592
+ const cwd2 = process.cwd();
46821
48593
  let config;
46822
48594
  try {
46823
- config = loadConfig({ projectDir, explicitPath: opts.config });
48595
+ config = loadConfig({ projectDir: cwd2, explicitPath: opts.config });
46824
48596
  } catch (err) {
46825
- fail(`${err.message}`);
48597
+ const msg = err.message;
48598
+ if (msg.includes("no api_key configured")) {
48599
+ await runInlineConfigure();
48600
+ try {
48601
+ config = loadConfig({ projectDir: cwd2, explicitPath: opts.config });
48602
+ } catch (err2) {
48603
+ fail(err2.message);
48604
+ }
48605
+ } else {
48606
+ fail(msg);
48607
+ }
46826
48608
  }
46827
48609
  const session = await resolveSession({
46828
48610
  pick: opts.pick,
46829
48611
  sessionPrefix: opts.session,
46830
- projectDir
48612
+ projectDir: cwd2
46831
48613
  });
46832
48614
  if (!session) {
46833
48615
  fail("no session selected", 0);
46834
48616
  }
48617
+ const projectDir = session.cwd ?? cwd2;
46835
48618
  if (!existsSync8(slashCommandPath(projectDir))) {
46836
- process.stderr.write(`note: /i slash command not installed in this project. ` + `Run \`cc-zh-watcher install\` first, or you'll have nothing to type in Claude Code.
48619
+ const accepted = await promptInstallSlashCommand(projectDir);
48620
+ if (!accepted) {
48621
+ process.stderr.write(`note: /i not installed — you'll need to manually paste the
48622
+ ` + ` translated English into Claude Code each turn.
46837
48623
  `);
48624
+ }
46838
48625
  }
46839
48626
  render_default(import_react31.default.createElement(App2, { session, config, projectDir }));
46840
48627
  }
46841
- function runInstall() {
46842
- const projectDir = process.cwd();
46843
- try {
46844
- const { installedPath, existed } = installSlashCommand(projectDir);
46845
- ok(`${existed ? "✓ replaced" : "✓ installed"} ${installedPath}
46846
- ` + ` /i in Claude Code now reads .claude/cache/user-input.md`);
46847
- } catch (err) {
46848
- fail(err.message);
46849
- }
46850
- }
46851
48628
  async function runUninstall(opts) {
46852
48629
  const projectDir = process.cwd();
46853
48630
  let wipe = opts.wipeCache;
@@ -46863,18 +48640,18 @@ async function runUninstall(opts) {
46863
48640
  for (const s of skipped)
46864
48641
  ok(` (not present) ${s}`);
46865
48642
  }
46866
- async function runConfigure() {
48643
+ async function runInlineConfigure() {
46867
48644
  const path = userConfigPath();
46868
48645
  let existing = {};
46869
48646
  if (existsSync8(path)) {
46870
48647
  try {
46871
48648
  existing = JSON.parse(readFileSync8(path, "utf-8"));
46872
- ok(`existing config at ${path}:`);
46873
- ok(` base_url: ${existing["base_url"] ?? "(unset)"}`);
46874
- ok(` model: ${existing["model"] ?? "(unset)"}`);
46875
- ok(` api_key: ${existing["api_key"] ? "(set)" : "(unset)"}`);
46876
48649
  } catch {}
46877
48650
  }
48651
+ ok("");
48652
+ ok("─ First-time setup ─────────────────────────────────────────");
48653
+ ok("No API endpoint configured yet. Set one up now (Ctrl+C to abort).");
48654
+ ok("");
46878
48655
  const rl = createInterface({ input, output });
46879
48656
  const ask = async (prompt, fallback) => {
46880
48657
  const ans = (await rl.question(prompt)).trim();
@@ -46894,10 +48671,31 @@ async function runConfigure() {
46894
48671
  model,
46895
48672
  timeout_seconds: Number(timeoutStr) || 30
46896
48673
  };
46897
- mkdirSync4(dirname4(path), { recursive: true });
46898
- writeFileSync4(path, JSON.stringify(next, null, 2) + `
46899
- `, "utf-8");
48674
+ writeAtomic(path, JSON.stringify(next, null, 2) + `
48675
+ `, { mode: 384 });
46900
48676
  ok(`✓ wrote ${path}`);
48677
+ ok("");
48678
+ }
48679
+ async function promptInstallSlashCommand(projectDir) {
48680
+ const target = slashCommandPath(projectDir);
48681
+ const rl = createInterface({ input, output });
48682
+ const ans = (await rl.question(`
48683
+ /i slash command not yet installed in this project.
48684
+ ` + ` project: ${projectDir}
48685
+ ` + ` will write: ${target}
48686
+ ` + `Install now? [Y/n] `)).trim().toLowerCase();
48687
+ rl.close();
48688
+ if (ans === "n" || ans === "no")
48689
+ return false;
48690
+ try {
48691
+ installSlashCommand(projectDir);
48692
+ ok(`✓ installed ${target}`);
48693
+ return true;
48694
+ } catch (err) {
48695
+ process.stderr.write(`✗ install failed: ${err.message}
48696
+ `);
48697
+ return false;
48698
+ }
46901
48699
  }
46902
48700
  function runPromptsShow(opts) {
46903
48701
  let config = null;
@@ -47009,17 +48807,15 @@ function runSessionsList() {
47009
48807
  state file: ${stateFilePath(projectDir)}`);
47010
48808
  }
47011
48809
  var program2 = new Command;
47012
- program2.name("cc-zh-watcher").version(VERSION);
48810
+ program2.name("cc-zh-watcher").version(VERSION, "-v, --version", "output the version number");
47013
48811
  program2.description("Side-channel Chinese ↔ English bridge for Claude Code").option("--pick", "force session picker (switch session)").option("--session <id>", "select session by uuid or unique prefix").option("--config <path>", "use a specific config file").action(async (opts) => {
47014
48812
  await runWatch(opts);
47015
48813
  });
47016
- program2.command("install").description("install /i slash command into <project>/.claude/commands/").action(runInstall);
47017
48814
  program2.command("uninstall").description("remove /i (optionally wipe cache files)").option("--wipe-cache", "also delete user-input.md + history.jsonl").option("--keep-cache", "keep cache files").action(async (opts) => {
47018
48815
  await runUninstall({
47019
48816
  wipeCache: opts.wipeCache ? true : opts.keepCache ? false : undefined
47020
48817
  });
47021
48818
  });
47022
- program2.command("configure").description("interactively create / edit user-global config").action(runConfigure);
47023
48819
  program2.command("sessions").description("list all sessions in this project").action(runSessionsList);
47024
48820
  var promptsCmd = program2.command("prompts").description("inspect / introspect the translation prompts");
47025
48821
  promptsCmd.command("show [direction]").description("print the active prompt for a direction (or all if omitted)").option("--base", "show the built-in base, ignoring overrides/extensions").action((direction, opts) => {