cc-zh-watcher 0.1.0 → 0.2.0

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 +2180 -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,527 @@ 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 = 5;
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
+ function ToolUseRow({ toolUse }) {
46833
+ const summary = summarizeToolInput(toolUse.name, toolUse.input);
46834
+ const lines = summary.length > 0 ? summary.split(`
46835
+ `) : [""];
46836
+ const continuationIndent = " ".repeat(2 + toolUse.name.length + 1);
46837
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46838
+ flexDirection: "column",
46839
+ marginY: 1,
46840
+ paddingX: 1
46841
+ }, lines.map((line, i) => {
46842
+ const isLast = i === lines.length - 1;
46843
+ if (i === 0) {
46844
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46845
+ key: i
46846
+ }, /* @__PURE__ */ import_react26.default.createElement(Text, {
46847
+ color: "cyan",
46848
+ bold: true
46849
+ }, "●"), /* @__PURE__ */ import_react26.default.createElement(Text, {
46850
+ bold: true
46851
+ }, " ", toolUse.name, "(", line, isLast ? ")" : ""));
46852
+ }
46853
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46854
+ key: i
46855
+ }, /* @__PURE__ */ import_react26.default.createElement(Text, null, continuationIndent), /* @__PURE__ */ import_react26.default.createElement(Text, {
46856
+ bold: true
46857
+ }, line, isLast ? ")" : ""));
46858
+ }));
46859
+ }
46860
+ function ToolResultRow({
46861
+ toolResult
46862
+ }) {
46863
+ const r = formatToolResult(toolResult.content);
46864
+ const errorColor = toolResult.isError ? "red" : undefined;
46865
+ if (r.lines.length === 0) {
46866
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46867
+ marginY: 0,
46868
+ paddingX: 1
46869
+ }, /* @__PURE__ */ import_react26.default.createElement(Text, {
46870
+ color: errorColor ?? "gray"
46871
+ }, " ⎿ "), /* @__PURE__ */ import_react26.default.createElement(Text, {
46872
+ dimColor: true
46873
+ }, "(no output)"));
46874
+ }
46875
+ return /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46876
+ flexDirection: "column",
46877
+ marginY: 0,
46878
+ paddingX: 1
46879
+ }, r.lines.map((line, i) => /* @__PURE__ */ import_react26.default.createElement(Box_default, {
46880
+ key: i
46881
+ }, /* @__PURE__ */ import_react26.default.createElement(Text, {
46882
+ color: i === 0 ? errorColor ?? "gray" : undefined
46883
+ }, i === 0 ? " ⎿ " : " "), /* @__PURE__ */ import_react26.default.createElement(Text, {
46884
+ dimColor: true,
46885
+ color: errorColor
46886
+ }, 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, {
46887
+ dimColor: true
46888
+ }, "… +", r.truncated, " lines")) : null);
45828
46889
  }
45829
46890
 
45830
46891
  // src/components/settings-modal.tsx
45831
- var import_react28 = __toESM(require_react(), 1);
46892
+ var import_react29 = __toESM(require_react(), 1);
45832
46893
 
45833
46894
  // src/components/multiline-input.tsx
45834
46895
  var import_react27 = __toESM(require_react(), 1);
@@ -46033,6 +47094,125 @@ function MultilineDisplay({
46033
47094
  }, lines.length, " line", lines.length === 1 ? "" : "s", " · ", charCount, " chars"))));
46034
47095
  }
46035
47096
 
47097
+ // src/components/session-picker.tsx
47098
+ var import_react28 = __toESM(require_react(), 1);
47099
+ function shortId2(id) {
47100
+ if (id.length <= 12)
47101
+ return id;
47102
+ return id.slice(0, 8) + "..." + id.slice(-4);
47103
+ }
47104
+ function SessionPicker({ sessions, onPick, onCancel }) {
47105
+ const [cursor, setCursor] = import_react28.useState(0);
47106
+ const [search, setSearch] = import_react28.useState("");
47107
+ const [searching, setSearching] = import_react28.useState(false);
47108
+ const filtered = search ? sessions.filter((s) => {
47109
+ const haystack = [
47110
+ s.id,
47111
+ s.first_message ?? "",
47112
+ s.started_at ?? "",
47113
+ s.last_activity_at ?? ""
47114
+ ].join(" ").toLowerCase();
47115
+ return haystack.includes(search.toLowerCase());
47116
+ }) : sessions;
47117
+ use_input_default((input, key) => {
47118
+ if (searching) {
47119
+ if (key.escape) {
47120
+ setSearching(false);
47121
+ setSearch("");
47122
+ return;
47123
+ }
47124
+ if (key.return) {
47125
+ setSearching(false);
47126
+ return;
47127
+ }
47128
+ if (key.backspace || key.delete) {
47129
+ setSearch((s) => s.slice(0, -1));
47130
+ return;
47131
+ }
47132
+ if (input && !key.ctrl && !key.meta) {
47133
+ setSearch((s) => s + input);
47134
+ }
47135
+ return;
47136
+ }
47137
+ if (key.escape) {
47138
+ onCancel();
47139
+ return;
47140
+ }
47141
+ if (key.upArrow) {
47142
+ setCursor((c) => Math.max(0, c - 1));
47143
+ return;
47144
+ }
47145
+ if (key.downArrow) {
47146
+ setCursor((c) => Math.min(filtered.length - 1, c + 1));
47147
+ return;
47148
+ }
47149
+ if (key.return) {
47150
+ const target = filtered[cursor];
47151
+ if (target)
47152
+ onPick(target);
47153
+ return;
47154
+ }
47155
+ if (input === "/") {
47156
+ setSearching(true);
47157
+ }
47158
+ });
47159
+ if (sessions.length === 0) {
47160
+ return /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47161
+ flexDirection: "column",
47162
+ padding: 1
47163
+ }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47164
+ color: "yellow"
47165
+ }, "No Claude Code sessions found in this project."), /* @__PURE__ */ import_react28.default.createElement(Text, {
47166
+ dimColor: true
47167
+ }, "Start `claude` in this project first, then run `cc-zh-watcher` again."), /* @__PURE__ */ import_react28.default.createElement(Text, {
47168
+ dimColor: true
47169
+ }, "Press Esc to exit."));
47170
+ }
47171
+ const visible = filtered.slice(0, 8);
47172
+ return /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47173
+ flexDirection: "column"
47174
+ }, /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47175
+ borderStyle: "round",
47176
+ borderColor: "cyan",
47177
+ flexDirection: "column",
47178
+ paddingX: 1
47179
+ }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47180
+ bold: true,
47181
+ color: "cyan"
47182
+ }, "Select a Claude Code session to watch"), /* @__PURE__ */ import_react28.default.createElement(Text, {
47183
+ dimColor: true
47184
+ }, "project: ", process.cwd(), " · ", sessions.length, " session", sessions.length === 1 ? "" : "s"), /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47185
+ marginTop: 1,
47186
+ flexDirection: "column"
47187
+ }, visible.map((s, i) => {
47188
+ const selected = i === cursor;
47189
+ return /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47190
+ key: s.id,
47191
+ flexDirection: "column",
47192
+ marginBottom: 1
47193
+ }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47194
+ color: selected ? "cyan" : undefined,
47195
+ bold: selected
47196
+ }, 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, {
47197
+ dimColor: true
47198
+ }, " started: ", formatLocalTime(s.started_at)), s.first_message ? /* @__PURE__ */ import_react28.default.createElement(Text, {
47199
+ dimColor: true
47200
+ }, " first msg: ", JSON.stringify(s.first_message)) : null);
47201
+ }), filtered.length > visible.length ? /* @__PURE__ */ import_react28.default.createElement(Text, {
47202
+ dimColor: true
47203
+ }, "... and ", filtered.length - visible.length, " more (use / to search)") : null), /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47204
+ marginTop: 1
47205
+ }, searching ? /* @__PURE__ */ import_react28.default.createElement(Text, null, /* @__PURE__ */ import_react28.default.createElement(Text, {
47206
+ color: "yellow"
47207
+ }, "/"), search, /* @__PURE__ */ import_react28.default.createElement(Text, {
47208
+ color: "yellow"
47209
+ }, "_"), /* @__PURE__ */ import_react28.default.createElement(Text, {
47210
+ dimColor: true
47211
+ }, " (Esc clear · Enter accept)")) : /* @__PURE__ */ import_react28.default.createElement(Text, {
47212
+ dimColor: true
47213
+ }, "[↑/↓] move [Enter] select [/] search [Esc] cancel"))));
47214
+ }
47215
+
46036
47216
  // src/components/settings-modal.tsx
46037
47217
  var FIELD_KEYS = [
46038
47218
  "api_key",
@@ -46051,8 +47231,16 @@ var PROMPT_LABEL = {
46051
47231
  "zh-to-en-with-context": "zh-to-en-w/ctx",
46052
47232
  "en-to-zh": "en-to-zh"
46053
47233
  };
46054
- var ROW_API_END = FIELD_KEYS.length;
46055
- var ROW_USER_END = ROW_API_END + PROMPT_KEYS.length;
47234
+ var ROW_SESSION_END = 1;
47235
+ var ROW_API_END = ROW_SESSION_END + FIELD_KEYS.length;
47236
+ var ROW_NOTIFY_END = ROW_API_END + 1;
47237
+ var ROW_THINKING_END = ROW_API_END + 2;
47238
+ var ROW_TODOS_END = ROW_API_END + 3;
47239
+ var ROW_AGENT_END = ROW_API_END + 4;
47240
+ var ROW_CONTEXT_END = ROW_API_END + 5;
47241
+ var TOGGLE_ROW_COUNT = 6;
47242
+ var ROW_TOGGLES_END = ROW_API_END + TOGGLE_ROW_COUNT;
47243
+ var ROW_USER_END = ROW_TOGGLES_END + PROMPT_KEYS.length;
46056
47244
  var ROW_DEFAULT_END = ROW_USER_END + PROMPT_KEYS.length;
46057
47245
  var TOTAL_ROWS = ROW_DEFAULT_END;
46058
47246
  function maskApiKey(key) {
@@ -46078,24 +47266,33 @@ function previewDefault(text, width) {
46078
47266
  }
46079
47267
  function SettingsModal({
46080
47268
  initialConfig,
46081
- onSave
47269
+ onSave,
47270
+ currentSession,
47271
+ projectDir,
47272
+ onSessionChange
46082
47273
  }) {
46083
- const [fields, setFields] = import_react28.useState(() => ({
47274
+ const [fields, setFields] = import_react29.useState(() => ({
46084
47275
  api_key: initialConfig.api_key,
46085
47276
  base_url: initialConfig.base_url,
46086
47277
  model: initialConfig.model,
46087
47278
  timeout_seconds: String(initialConfig.timeout_seconds),
47279
+ notify_on_complete: initialConfig.notify_on_complete,
47280
+ translate_thinking: initialConfig.translate_thinking,
47281
+ translate_todos: initialConfig.translate_todos,
47282
+ translate_agent: initialConfig.translate_agent,
47283
+ context_enabled: initialConfig.context_enabled,
47284
+ show_tool_calls: initialConfig.show_tool_calls,
46088
47285
  prompts: {
46089
47286
  "zh-to-en": initialConfig.prompts?.["zh-to-en"] ?? "",
46090
47287
  "zh-to-en-with-context": initialConfig.prompts?.["zh-to-en-with-context"] ?? "",
46091
47288
  "en-to-zh": initialConfig.prompts?.["en-to-zh"] ?? ""
46092
47289
  }
46093
47290
  }));
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);
47291
+ const [cursor, setCursor] = import_react29.useState(0);
47292
+ const [target, setTarget] = import_react29.useState(null);
47293
+ const [editBuffer, setEditBuffer] = import_react29.useState("");
47294
+ const [error, setError] = import_react29.useState(null);
47295
+ const [saving, setSaving] = import_react29.useState(false);
46099
47296
  function commitScalar(idx, value) {
46100
47297
  const fk = FIELD_KEYS[idx];
46101
47298
  if (!fk)
@@ -46153,17 +47350,52 @@ function SettingsModal({
46153
47350
  return;
46154
47351
  }
46155
47352
  if (key.return) {
47353
+ if (cursor < ROW_SESSION_END) {
47354
+ setTarget({ kind: "session-pick" });
47355
+ setError(null);
47356
+ return;
47357
+ }
46156
47358
  if (cursor < ROW_API_END) {
46157
- const fk = FIELD_KEYS[cursor];
47359
+ const fk = FIELD_KEYS[cursor - ROW_SESSION_END];
46158
47360
  if (!fk)
46159
47361
  return;
46160
47362
  setEditBuffer(fields[fk]);
46161
- setTarget({ kind: "scalar", index: cursor });
47363
+ setTarget({ kind: "scalar", index: cursor - ROW_SESSION_END });
47364
+ setError(null);
47365
+ return;
47366
+ }
47367
+ if (cursor < ROW_NOTIFY_END) {
47368
+ setFields((f) => ({ ...f, notify_on_complete: !f.notify_on_complete }));
47369
+ setError(null);
47370
+ return;
47371
+ }
47372
+ if (cursor < ROW_THINKING_END) {
47373
+ setFields((f) => ({ ...f, translate_thinking: !f.translate_thinking }));
47374
+ setError(null);
47375
+ return;
47376
+ }
47377
+ if (cursor < ROW_TODOS_END) {
47378
+ setFields((f) => ({ ...f, translate_todos: !f.translate_todos }));
47379
+ setError(null);
47380
+ return;
47381
+ }
47382
+ if (cursor < ROW_AGENT_END) {
47383
+ setFields((f) => ({ ...f, translate_agent: !f.translate_agent }));
47384
+ setError(null);
47385
+ return;
47386
+ }
47387
+ if (cursor < ROW_CONTEXT_END) {
47388
+ setFields((f) => ({ ...f, context_enabled: !f.context_enabled }));
47389
+ setError(null);
47390
+ return;
47391
+ }
47392
+ if (cursor < ROW_TOGGLES_END) {
47393
+ setFields((f) => ({ ...f, show_tool_calls: !f.show_tool_calls }));
46162
47394
  setError(null);
46163
47395
  return;
46164
47396
  }
46165
47397
  if (cursor < ROW_USER_END) {
46166
- const idx2 = cursor - ROW_API_END;
47398
+ const idx2 = cursor - ROW_TOGGLES_END;
46167
47399
  setTarget({ kind: "user-prompt", rowIndex: idx2 });
46168
47400
  setError(null);
46169
47401
  return;
@@ -46190,9 +47422,9 @@ function SettingsModal({
46190
47422
  const direction = PROMPT_KEYS[target.rowIndex];
46191
47423
  if (!direction) {
46192
47424
  setTarget(null);
46193
- return /* @__PURE__ */ import_react28.default.createElement(Box_default, null);
47425
+ return /* @__PURE__ */ import_react29.default.createElement(Box_default, null);
46194
47426
  }
46195
- return /* @__PURE__ */ import_react28.default.createElement(MultilineInput, {
47427
+ return /* @__PURE__ */ import_react29.default.createElement(MultilineInput, {
46196
47428
  title: `user.${direction}`,
46197
47429
  initial: fields.prompts[direction],
46198
47430
  onSubmit: (value) => {
@@ -46206,138 +47438,347 @@ function SettingsModal({
46206
47438
  const direction = PROMPT_KEYS[target.rowIndex];
46207
47439
  if (!direction) {
46208
47440
  setTarget(null);
46209
- return /* @__PURE__ */ import_react28.default.createElement(Box_default, null);
47441
+ return /* @__PURE__ */ import_react29.default.createElement(Box_default, null);
46210
47442
  }
46211
- return /* @__PURE__ */ import_react28.default.createElement(MultilineDisplay, {
47443
+ return /* @__PURE__ */ import_react29.default.createElement(MultilineDisplay, {
46212
47444
  title: `default.${direction}`,
46213
47445
  text: basePrompt(direction),
46214
47446
  onClose: () => setTarget(null)
46215
47447
  });
46216
47448
  }
46217
- return /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47449
+ if (target?.kind === "session-pick") {
47450
+ const sessions = listSessions(projectDir);
47451
+ return /* @__PURE__ */ import_react29.default.createElement(SessionPicker, {
47452
+ sessions,
47453
+ onPick: (s) => {
47454
+ setTarget(null);
47455
+ if (s.id !== currentSession.id) {
47456
+ onSessionChange(s);
47457
+ }
47458
+ },
47459
+ onCancel: () => setTarget(null)
47460
+ });
47461
+ }
47462
+ return /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46218
47463
  flexDirection: "column",
46219
47464
  padding: 1
46220
- }, /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47465
+ }, /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46221
47466
  borderStyle: "round",
46222
47467
  borderColor: "cyan",
46223
47468
  paddingX: 1,
46224
47469
  flexDirection: "column"
46225
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47470
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46226
47471
  bold: true,
46227
47472
  color: "cyan"
46228
- }, "Settings"), /* @__PURE__ */ import_react28.default.createElement(Text, {
47473
+ }, "Settings"), /* @__PURE__ */ import_react29.default.createElement(Text, {
46229
47474
  dimColor: true
46230
- }, "↑/↓ select · Enter edit · Esc save & close"), /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47475
+ }, "↑/↓ select · Enter edit · Esc save & close"), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46231
47476
  marginTop: 1,
46232
47477
  flexDirection: "column"
46233
- }, FIELD_KEYS.map((fk, i) => {
46234
- const selected = cursor === i;
47478
+ }, (() => {
47479
+ const selected = cursor === 0;
47480
+ const sid = currentSession.id;
47481
+ const short = sid.length > 12 ? sid.slice(0, 8) + "…" + sid.slice(-4) : sid;
47482
+ const turns = currentSession.turn_count;
47483
+ const active = relativeTime(currentSession.last_activity_at);
47484
+ return /* @__PURE__ */ import_react29.default.createElement(Box_default, null, /* @__PURE__ */ import_react29.default.createElement(Text, {
47485
+ color: selected ? "cyan" : undefined,
47486
+ bold: selected
47487
+ }, selected ? "▸ " : " "), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
47488
+ width: 20
47489
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
47490
+ color: selected ? "cyan" : undefined
47491
+ }, "session:")), /* @__PURE__ */ import_react29.default.createElement(Text, null, short, " ", /* @__PURE__ */ import_react29.default.createElement(Text, {
47492
+ dimColor: true
47493
+ }, "(", turns, " turn", turns === 1 ? "" : "s", " · active ", active, "; Enter 切换)")));
47494
+ })(), FIELD_KEYS.map((fk, i) => {
47495
+ const rowIdx = ROW_SESSION_END + i;
47496
+ const selected = cursor === rowIdx;
46235
47497
  const editingThis = selected && target?.kind === "scalar" && target.index === i;
46236
- return /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47498
+ return /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46237
47499
  key: fk
46238
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47500
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46239
47501
  color: selected ? "cyan" : undefined,
46240
47502
  bold: selected
46241
- }, selected ? "▸ " : " "), /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47503
+ }, selected ? "▸ " : " "), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46242
47504
  width: 20
46243
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47505
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46244
47506
  color: selected ? "cyan" : undefined
46245
- }, FIELD_LABELS[fk], ":")), editingThis ? /* @__PURE__ */ import_react28.default.createElement(build_default, {
47507
+ }, FIELD_LABELS[fk], ":")), editingThis ? /* @__PURE__ */ import_react29.default.createElement(build_default, {
46246
47508
  value: editBuffer,
46247
47509
  onChange: setEditBuffer,
46248
47510
  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, {
47511
+ }) : /* @__PURE__ */ import_react29.default.createElement(Text, null, fk === "api_key" ? maskApiKey(fields[fk]) : fields[fk]));
47512
+ }), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46251
47513
  marginTop: 1
46252
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47514
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
47515
+ dimColor: true
47516
+ }, "── Toggles ──")), /* @__PURE__ */ import_react29.default.createElement(ToggleRow, {
47517
+ rowIdx: ROW_API_END,
47518
+ cursor,
47519
+ label: "notify_on_complete",
47520
+ on: fields.notify_on_complete,
47521
+ hintOn: "(终端响铃;Enter 切换)",
47522
+ hintOff: "(已关闭;Enter 切换)"
47523
+ }), /* @__PURE__ */ import_react29.default.createElement(ToggleRow, {
47524
+ rowIdx: ROW_NOTIFY_END,
47525
+ cursor,
47526
+ label: "translate_thinking",
47527
+ on: fields.translate_thinking,
47528
+ hintOn: "(译 thinking 块,多花 API;Enter 切换)",
47529
+ hintOff: "(默认关;Enter 切换)"
47530
+ }), /* @__PURE__ */ import_react29.default.createElement(ToggleRow, {
47531
+ rowIdx: ROW_THINKING_END,
47532
+ cursor,
47533
+ label: "translate_todos",
47534
+ on: fields.translate_todos,
47535
+ hintOn: "(译 TodoWrite 计划;Enter 切换)",
47536
+ hintOff: "(已关闭;Enter 切换)"
47537
+ }), /* @__PURE__ */ import_react29.default.createElement(ToggleRow, {
47538
+ rowIdx: ROW_TODOS_END,
47539
+ cursor,
47540
+ label: "translate_agent",
47541
+ on: fields.translate_agent,
47542
+ hintOn: "(译 Agent/Task 子代理;Enter 切换)",
47543
+ hintOff: "(已关闭;Enter 切换)"
47544
+ }), /* @__PURE__ */ import_react29.default.createElement(ToggleRow, {
47545
+ rowIdx: ROW_AGENT_END,
47546
+ cursor,
47547
+ label: "context_enabled",
47548
+ on: fields.context_enabled,
47549
+ hintOn: "(zh→en 用 Claude 上一条作 context;Enter 切换)",
47550
+ hintOff: "(字面翻译不带 context;Enter 切换)"
47551
+ }), /* @__PURE__ */ import_react29.default.createElement(ToggleRow, {
47552
+ rowIdx: ROW_CONTEXT_END,
47553
+ cursor,
47554
+ label: "show_tool_calls",
47555
+ on: fields.show_tool_calls,
47556
+ hintOn: "(显示原始 tool 调用 + 结果(CC 同款);Enter 切换)",
47557
+ hintOff: "(只看翻译内容;Enter 切换)"
47558
+ }), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
47559
+ marginTop: 1
47560
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46253
47561
  dimColor: true
46254
47562
  }, "── User prompt (empty = use default) ──")), PROMPT_KEYS.map((dir, i) => {
46255
- const rowIdx = ROW_API_END + i;
47563
+ const rowIdx = ROW_TOGGLES_END + i;
46256
47564
  const selected = cursor === rowIdx;
46257
47565
  const value = fields.prompts[dir];
46258
- return /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47566
+ return /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46259
47567
  key: `user-${dir}`
46260
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47568
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46261
47569
  color: selected ? "cyan" : undefined,
46262
47570
  bold: selected
46263
- }, selected ? "▸ " : " "), /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47571
+ }, selected ? "▸ " : " "), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46264
47572
  width: 20
46265
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47573
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46266
47574
  color: selected ? "cyan" : undefined
46267
- }, "user.", PROMPT_LABEL[dir], ":")), /* @__PURE__ */ import_react28.default.createElement(Text, {
47575
+ }, "user.", PROMPT_LABEL[dir], ":")), /* @__PURE__ */ import_react29.default.createElement(Text, {
46268
47576
  dimColor: !value
46269
47577
  }, previewPrompt(value, 60)));
46270
- }), /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47578
+ }), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46271
47579
  marginTop: 1
46272
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47580
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46273
47581
  dimColor: true
46274
47582
  }, "── Default prompt (read-only) ──")), PROMPT_KEYS.map((dir, i) => {
46275
47583
  const rowIdx = ROW_USER_END + i;
46276
47584
  const selected = cursor === rowIdx;
46277
- return /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47585
+ return /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46278
47586
  key: `default-${dir}`
46279
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47587
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46280
47588
  color: selected ? "cyan" : undefined,
46281
47589
  bold: selected
46282
- }, selected ? "▸ " : " "), /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47590
+ }, selected ? "▸ " : " "), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46283
47591
  width: 20
46284
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47592
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46285
47593
  color: selected ? "cyan" : undefined,
46286
47594
  dimColor: true
46287
- }, "default.", PROMPT_LABEL[dir], ":")), /* @__PURE__ */ import_react28.default.createElement(Text, {
47595
+ }, "default.", PROMPT_LABEL[dir], ":")), /* @__PURE__ */ import_react29.default.createElement(Text, {
46288
47596
  dimColor: true
46289
47597
  }, previewDefault(basePrompt(dir), 60)));
46290
- })), error ? /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47598
+ })), error ? /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46291
47599
  marginTop: 1
46292
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47600
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46293
47601
  color: "red"
46294
- }, "✗ ", error)) : null, saving ? /* @__PURE__ */ import_react28.default.createElement(Box_default, {
47602
+ }, "✗ ", error)) : null, saving ? /* @__PURE__ */ import_react29.default.createElement(Box_default, {
46295
47603
  marginTop: 1
46296
- }, /* @__PURE__ */ import_react28.default.createElement(Text, {
47604
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
46297
47605
  color: "yellow"
46298
47606
  }, "⏳ saving…")) : null));
46299
47607
  }
47608
+ function ToggleRow({
47609
+ rowIdx,
47610
+ cursor,
47611
+ label,
47612
+ on,
47613
+ hintOn,
47614
+ hintOff
47615
+ }) {
47616
+ const selected = cursor === rowIdx;
47617
+ return /* @__PURE__ */ import_react29.default.createElement(Box_default, null, /* @__PURE__ */ import_react29.default.createElement(Text, {
47618
+ color: selected ? "cyan" : undefined,
47619
+ bold: selected
47620
+ }, selected ? "▸ " : " "), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
47621
+ width: 22
47622
+ }, /* @__PURE__ */ import_react29.default.createElement(Text, {
47623
+ color: selected ? "cyan" : undefined
47624
+ }, label, ":")), /* @__PURE__ */ import_react29.default.createElement(Text, {
47625
+ color: on ? "green" : "gray",
47626
+ bold: true
47627
+ }, on ? "on" : "off"), /* @__PURE__ */ import_react29.default.createElement(Text, {
47628
+ dimColor: true
47629
+ }, " ", on ? hintOn : hintOff));
47630
+ }
46300
47631
 
46301
47632
  // src/app.tsx
46302
47633
  function App2({
46303
- session,
47634
+ session: initialSession,
46304
47635
  config: initialConfig,
46305
47636
  projectDir
46306
47637
  }) {
46307
47638
  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({
47639
+ const [session, setSession] = import_react30.useState(() => initialSession);
47640
+ const [config, setConfig] = import_react30.useState(() => initialConfig);
47641
+ const [showSettings, setShowSettings] = import_react30.useState(false);
47642
+ const [appState, setAppState] = import_react30.useState("idle");
47643
+ const [chineseInput, setChineseInput] = import_react30.useState("");
47644
+ const [lastEnglish, setLastEnglish] = import_react30.useState("");
47645
+ const [error, setError] = import_react30.useState(null);
47646
+ const [statusLine, setStatusLine] = import_react30.useState(undefined);
47647
+ const [outputEntries, setOutputEntries] = import_react30.useState([]);
47648
+ const [streamingOutputs, setStreamingOutputs] = import_react30.useState([]);
47649
+ const [tailError, setTailError] = import_react30.useState(null);
47650
+ const [streamingEnglish, setStreamingEnglish] = import_react30.useState("");
47651
+ const [pendingConcern, setPendingConcern] = import_react30.useState(null);
47652
+ const translatedInputRef = import_react30.useRef("");
47653
+ const tailRef = import_react30.useRef(null);
47654
+ const seenIdsRef = import_react30.useRef(new Set);
47655
+ const enToZhQueueRef = import_react30.useRef(null);
47656
+ if (enToZhQueueRef.current === null) {
47657
+ enToZhQueueRef.current = new SerialQueue;
47658
+ }
47659
+ const notifyEnabledRef = import_react30.useRef(initialConfig.notify_on_complete);
47660
+ import_react30.useEffect(() => {
47661
+ notifyEnabledRef.current = config.notify_on_complete;
47662
+ }, [config.notify_on_complete]);
47663
+ const translateThinkingRef = import_react30.useRef(initialConfig.translate_thinking);
47664
+ import_react30.useEffect(() => {
47665
+ translateThinkingRef.current = config.translate_thinking;
47666
+ }, [config.translate_thinking]);
47667
+ const translateTodosRef = import_react30.useRef(initialConfig.translate_todos);
47668
+ import_react30.useEffect(() => {
47669
+ translateTodosRef.current = config.translate_todos;
47670
+ }, [config.translate_todos]);
47671
+ const translateAgentRef = import_react30.useRef(initialConfig.translate_agent);
47672
+ import_react30.useEffect(() => {
47673
+ translateAgentRef.current = config.translate_agent;
47674
+ }, [config.translate_agent]);
47675
+ const showToolCallsRef = import_react30.useRef(initialConfig.show_tool_calls);
47676
+ import_react30.useEffect(() => {
47677
+ showToolCallsRef.current = config.show_tool_calls;
47678
+ }, [config.show_tool_calls]);
47679
+ const bellRef = import_react30.useRef(null);
47680
+ if (bellRef.current === null) {
47681
+ bellRef.current = new DebouncedBell(2000, () => {
47682
+ if (notifyEnabledRef.current)
47683
+ ringBell();
47684
+ });
47685
+ }
47686
+ import_react30.useEffect(() => {
47687
+ const bell = bellRef.current;
47688
+ return () => {
47689
+ bell?.cancel();
47690
+ };
47691
+ }, []);
47692
+ const [lastAssistantText, setLastAssistantText] = import_react30.useState("");
47693
+ const [lastRequest, setLastRequest] = import_react30.useState(null);
47694
+ const [sessionMetrics, setSessionMetrics] = import_react30.useState({
46324
47695
  started_at: Date.now(),
46325
47696
  total_calls: 0,
46326
47697
  successful_calls: 0,
46327
47698
  error_count: 0,
46328
47699
  total_tokens: 0
46329
47700
  });
46330
- const historyRef = import_react29.useRef([]);
46331
- const historyCursorRef = import_react29.useRef(-1);
46332
- const HISTORY_MAX = 50;
46333
- import_react29.useEffect(() => {
47701
+ import_react30.useEffect(() => {
46334
47702
  const tail2 = new TranscriptTail(session.path, {
46335
- onText: (ev) => {
47703
+ onContent: (ev) => {
46336
47704
  if (seenIdsRef.current.has(ev.id))
46337
47705
  return;
46338
47706
  seenIdsRef.current.add(ev.id);
46339
- setLastAssistantText(ev.text);
46340
- translateAndAppend(ev);
47707
+ bellRef.current?.schedule();
47708
+ if (ev.kind === "text") {
47709
+ setLastAssistantText(ev.text);
47710
+ }
47711
+ if (ev.kind === "thinking" && !translateThinkingRef.current) {
47712
+ return;
47713
+ }
47714
+ if (ev.kind === "question") {
47715
+ const seed = seedTranslatedPayload(ev.question);
47716
+ setStreamingOutputs((arr) => [
47717
+ ...arr,
47718
+ {
47719
+ id: ev.id,
47720
+ timestamp: ev.timestamp,
47721
+ kind: "question",
47722
+ englishQuestion: ev.question,
47723
+ chineseQuestion: seed
47724
+ }
47725
+ ]);
47726
+ enToZhQueueRef.current.enqueue(() => translateQuestionAndAppend(ev, seed));
47727
+ return;
47728
+ }
47729
+ if (ev.kind === "todo") {
47730
+ if (!translateTodosRef.current)
47731
+ return;
47732
+ const seed = seedTranslatedTodos(ev.todo);
47733
+ setStreamingOutputs((arr) => [
47734
+ ...arr,
47735
+ {
47736
+ id: ev.id,
47737
+ timestamp: ev.timestamp,
47738
+ kind: "todo",
47739
+ englishTodo: ev.todo,
47740
+ chineseTodo: seed
47741
+ }
47742
+ ]);
47743
+ enToZhQueueRef.current.enqueue(() => translateTodoAndAppend(ev, seed));
47744
+ return;
47745
+ }
47746
+ if (ev.kind === "agent") {
47747
+ if (!translateAgentRef.current)
47748
+ return;
47749
+ const seed = seedTranslatedAgent(ev.agent);
47750
+ setStreamingOutputs((arr) => [
47751
+ ...arr,
47752
+ {
47753
+ id: ev.id,
47754
+ timestamp: ev.timestamp,
47755
+ kind: "agent",
47756
+ englishAgent: ev.agent,
47757
+ chineseAgent: seed
47758
+ }
47759
+ ]);
47760
+ enToZhQueueRef.current.enqueue(() => translateAgentAndAppend(ev, seed));
47761
+ return;
47762
+ }
47763
+ if (ev.kind === "tool_use" || ev.kind === "tool_result") {
47764
+ if (!showToolCallsRef.current)
47765
+ return;
47766
+ enToZhQueueRef.current.enqueue(async () => {
47767
+ appendRawToolEntry(ev);
47768
+ });
47769
+ return;
47770
+ }
47771
+ setStreamingOutputs((arr) => [
47772
+ ...arr,
47773
+ {
47774
+ id: ev.id,
47775
+ timestamp: ev.timestamp,
47776
+ kind: ev.kind,
47777
+ english: ev.text,
47778
+ chineseSoFar: ""
47779
+ }
47780
+ ]);
47781
+ enToZhQueueRef.current.enqueue(() => translateAndAppend(ev));
46341
47782
  },
46342
47783
  onError: (err) => {
46343
47784
  setTailError(err.message);
@@ -46355,46 +47796,334 @@ function App2({
46355
47796
  };
46356
47797
  }, [session.path]);
46357
47798
  async function translateAndAppend(ev) {
46358
- setPendingOutputCount((c) => c + 1);
46359
47799
  markRequestStarted("en-to-zh");
46360
47800
  try {
46361
- const result2 = await translate(ev.text, "en-to-zh", config);
47801
+ const result2 = await translate(ev.text, "en-to-zh", config, {
47802
+ onDelta: (delta) => {
47803
+ setStreamingOutputs((arr) => arr.map((e) => e.id === ev.id && (e.kind === "text" || e.kind === "thinking") ? { ...e, chineseSoFar: e.chineseSoFar + delta } : e));
47804
+ }
47805
+ });
46362
47806
  appendHistory(projectDir, {
46363
47807
  timestamp: new Date().toISOString(),
46364
47808
  direction: "en-to-zh",
46365
47809
  input: ev.text,
46366
47810
  output: result2.content
46367
47811
  });
47812
+ setStreamingOutputs((arr) => arr.filter((e) => e.id !== ev.id));
47813
+ setOutputEntries((entries) => [
47814
+ ...entries,
47815
+ {
47816
+ id: ev.id,
47817
+ timestamp: ev.timestamp,
47818
+ kind: ev.kind,
47819
+ english: ev.text,
47820
+ chinese: result2.content,
47821
+ state: "translated"
47822
+ }
47823
+ ]);
47824
+ markRequestSuccess("en-to-zh", result2.elapsed_ms, result2.usage?.total_tokens ?? null);
47825
+ } catch (err) {
47826
+ const te = err instanceof TranslateError ? err : null;
47827
+ const msg = te?.message ?? err.message;
47828
+ setStreamingOutputs((arr) => arr.filter((e) => e.id !== ev.id));
47829
+ setOutputEntries((entries) => [
47830
+ ...entries,
47831
+ {
47832
+ id: ev.id,
47833
+ timestamp: ev.timestamp,
47834
+ kind: ev.kind,
47835
+ english: ev.text,
47836
+ chinese: "",
47837
+ state: "error",
47838
+ error: msg
47839
+ }
47840
+ ]);
47841
+ markRequestError("en-to-zh", te?.elapsed_ms ?? 0, te?.code ?? "error", msg);
47842
+ }
47843
+ }
47844
+ async function translateQuestionAndAppend(ev, seed) {
47845
+ markRequestStarted("en-to-zh");
47846
+ let totalElapsed = 0;
47847
+ let totalTokens = 0;
47848
+ const pending = [];
47849
+ for (let qi = 0;qi < seed.questions.length; qi += 1) {
47850
+ const q = seed.questions[qi];
47851
+ if (!q.question.zh) {
47852
+ pending.push({
47853
+ en: q.question.en,
47854
+ patch: (zh, cur) => patchField(cur, qi, "question", null, zh)
47855
+ });
47856
+ }
47857
+ for (let oi = 0;oi < q.options.length; oi += 1) {
47858
+ const opt = q.options[oi];
47859
+ if (!opt.label.zh) {
47860
+ pending.push({
47861
+ en: opt.label.en,
47862
+ patch: (zh, cur) => patchField(cur, qi, "label", oi, zh)
47863
+ });
47864
+ }
47865
+ if (!opt.description.zh) {
47866
+ pending.push({
47867
+ en: opt.description.en,
47868
+ patch: (zh, cur) => patchField(cur, qi, "description", oi, zh)
47869
+ });
47870
+ }
47871
+ }
47872
+ }
47873
+ try {
47874
+ for (const p of pending) {
47875
+ const result2 = await translate(p.en, "en-to-zh", config);
47876
+ totalElapsed += result2.elapsed_ms;
47877
+ totalTokens += result2.usage?.total_tokens ?? 0;
47878
+ appendHistory(projectDir, {
47879
+ timestamp: new Date().toISOString(),
47880
+ direction: "en-to-zh",
47881
+ input: p.en,
47882
+ output: result2.content
47883
+ });
47884
+ setStreamingOutputs((arr) => arr.map((e) => e.id === ev.id && e.kind === "question" ? { ...e, chineseQuestion: p.patch(result2.content, e.chineseQuestion) } : e));
47885
+ }
47886
+ setStreamingOutputs((arr) => {
47887
+ const finalEntry = arr.find((e) => e.id === ev.id && e.kind === "question");
47888
+ if (finalEntry && finalEntry.kind === "question") {
47889
+ setOutputEntries((entries) => [
47890
+ ...entries,
47891
+ {
47892
+ id: ev.id,
47893
+ timestamp: ev.timestamp,
47894
+ kind: "question",
47895
+ englishQuestion: ev.question,
47896
+ chineseQuestion: finalEntry.chineseQuestion,
47897
+ state: "translated"
47898
+ }
47899
+ ]);
47900
+ }
47901
+ return arr.filter((e) => e.id !== ev.id);
47902
+ });
47903
+ markRequestSuccess("en-to-zh", totalElapsed, totalTokens);
47904
+ } catch (err) {
47905
+ const te = err instanceof TranslateError ? err : null;
47906
+ const msg = te?.message ?? err.message;
47907
+ setStreamingOutputs((arr) => arr.filter((e) => e.id !== ev.id));
47908
+ setOutputEntries((entries) => [
47909
+ ...entries,
47910
+ {
47911
+ id: ev.id,
47912
+ timestamp: ev.timestamp,
47913
+ kind: "question",
47914
+ englishQuestion: ev.question,
47915
+ chineseQuestion: seed,
47916
+ state: "error",
47917
+ error: msg
47918
+ }
47919
+ ]);
47920
+ markRequestError("en-to-zh", totalElapsed + (te?.elapsed_ms ?? 0), te?.code ?? "error", msg);
47921
+ }
47922
+ }
47923
+ async function translateTodoAndAppend(ev, seed) {
47924
+ markRequestStarted("en-to-zh");
47925
+ let totalElapsed = 0;
47926
+ let totalTokens = 0;
47927
+ const pending = [];
47928
+ for (let i = 0;i < seed.todos.length; i += 1) {
47929
+ const t = seed.todos[i];
47930
+ const which = t.status === "in_progress" ? "activeForm" : "content";
47931
+ const f = which === "activeForm" ? t.activeForm : t.content;
47932
+ if (!f.zh) {
47933
+ pending.push({
47934
+ en: f.en,
47935
+ patch: (zh, cur) => patchTodoField(cur, i, which, zh)
47936
+ });
47937
+ }
47938
+ }
47939
+ try {
47940
+ for (const p of pending) {
47941
+ const result2 = await translate(p.en, "en-to-zh", config);
47942
+ totalElapsed += result2.elapsed_ms;
47943
+ totalTokens += result2.usage?.total_tokens ?? 0;
47944
+ appendHistory(projectDir, {
47945
+ timestamp: new Date().toISOString(),
47946
+ direction: "en-to-zh",
47947
+ input: p.en,
47948
+ output: result2.content
47949
+ });
47950
+ setStreamingOutputs((arr) => arr.map((e) => e.id === ev.id && e.kind === "todo" ? { ...e, chineseTodo: p.patch(result2.content, e.chineseTodo) } : e));
47951
+ }
47952
+ setStreamingOutputs((arr) => {
47953
+ const finalEntry = arr.find((e) => e.id === ev.id && e.kind === "todo");
47954
+ if (finalEntry && finalEntry.kind === "todo") {
47955
+ setOutputEntries((entries) => [
47956
+ ...entries,
47957
+ {
47958
+ id: ev.id,
47959
+ timestamp: ev.timestamp,
47960
+ kind: "todo",
47961
+ englishTodo: ev.todo,
47962
+ chineseTodo: finalEntry.chineseTodo,
47963
+ state: "translated"
47964
+ }
47965
+ ]);
47966
+ }
47967
+ return arr.filter((e) => e.id !== ev.id);
47968
+ });
47969
+ markRequestSuccess("en-to-zh", totalElapsed, totalTokens);
47970
+ } catch (err) {
47971
+ const te = err instanceof TranslateError ? err : null;
47972
+ const msg = te?.message ?? err.message;
47973
+ setStreamingOutputs((arr) => arr.filter((e) => e.id !== ev.id));
46368
47974
  setOutputEntries((entries) => [
46369
47975
  ...entries,
46370
47976
  {
46371
47977
  id: ev.id,
46372
47978
  timestamp: ev.timestamp,
46373
- english: ev.text,
46374
- chinese: result2.content,
46375
- state: "translated"
47979
+ kind: "todo",
47980
+ englishTodo: ev.todo,
47981
+ chineseTodo: seed,
47982
+ state: "error",
47983
+ error: msg
46376
47984
  }
46377
47985
  ]);
46378
- markRequestSuccess("en-to-zh", result2.elapsed_ms, result2.usage?.total_tokens ?? null);
47986
+ markRequestError("en-to-zh", totalElapsed + (te?.elapsed_ms ?? 0), te?.code ?? "error", msg);
47987
+ }
47988
+ }
47989
+ async function translateAgentAndAppend(ev, seed) {
47990
+ markRequestStarted("en-to-zh");
47991
+ let totalElapsed = 0;
47992
+ let totalTokens = 0;
47993
+ try {
47994
+ if (!seed.description.zh) {
47995
+ const result2 = await translate(seed.description.en, "en-to-zh", config);
47996
+ totalElapsed += result2.elapsed_ms;
47997
+ totalTokens += result2.usage?.total_tokens ?? 0;
47998
+ appendHistory(projectDir, {
47999
+ timestamp: new Date().toISOString(),
48000
+ direction: "en-to-zh",
48001
+ input: seed.description.en,
48002
+ output: result2.content
48003
+ });
48004
+ setStreamingOutputs((arr) => arr.map((e) => e.id === ev.id && e.kind === "agent" ? {
48005
+ ...e,
48006
+ chineseAgent: {
48007
+ ...e.chineseAgent,
48008
+ description: { en: e.chineseAgent.description.en, zh: result2.content }
48009
+ }
48010
+ } : e));
48011
+ }
48012
+ if (!seed.prompt.zh) {
48013
+ const result2 = await translate(seed.prompt.en, "en-to-zh", config, {
48014
+ onDelta: (delta) => {
48015
+ setStreamingOutputs((arr) => arr.map((e) => e.id === ev.id && e.kind === "agent" ? {
48016
+ ...e,
48017
+ chineseAgent: {
48018
+ ...e.chineseAgent,
48019
+ prompt: {
48020
+ en: e.chineseAgent.prompt.en,
48021
+ zh: e.chineseAgent.prompt.zh + delta
48022
+ }
48023
+ }
48024
+ } : e));
48025
+ }
48026
+ });
48027
+ totalElapsed += result2.elapsed_ms;
48028
+ totalTokens += result2.usage?.total_tokens ?? 0;
48029
+ appendHistory(projectDir, {
48030
+ timestamp: new Date().toISOString(),
48031
+ direction: "en-to-zh",
48032
+ input: seed.prompt.en,
48033
+ output: result2.content
48034
+ });
48035
+ }
48036
+ setStreamingOutputs((arr) => {
48037
+ const finalEntry = arr.find((e) => e.id === ev.id && e.kind === "agent");
48038
+ if (finalEntry && finalEntry.kind === "agent") {
48039
+ setOutputEntries((entries) => [
48040
+ ...entries,
48041
+ {
48042
+ id: ev.id,
48043
+ timestamp: ev.timestamp,
48044
+ kind: "agent",
48045
+ englishAgent: ev.agent,
48046
+ chineseAgent: finalEntry.chineseAgent,
48047
+ state: "translated"
48048
+ }
48049
+ ]);
48050
+ }
48051
+ return arr.filter((e) => e.id !== ev.id);
48052
+ });
48053
+ markRequestSuccess("en-to-zh", totalElapsed, totalTokens);
46379
48054
  } catch (err) {
46380
48055
  const te = err instanceof TranslateError ? err : null;
46381
48056
  const msg = te?.message ?? err.message;
48057
+ setStreamingOutputs((arr) => arr.filter((e) => e.id !== ev.id));
46382
48058
  setOutputEntries((entries) => [
46383
48059
  ...entries,
46384
48060
  {
46385
48061
  id: ev.id,
46386
48062
  timestamp: ev.timestamp,
46387
- english: ev.text,
46388
- chinese: "",
48063
+ kind: "agent",
48064
+ englishAgent: ev.agent,
48065
+ chineseAgent: seed,
46389
48066
  state: "error",
46390
48067
  error: msg
46391
48068
  }
46392
48069
  ]);
46393
- markRequestError("en-to-zh", te?.elapsed_ms ?? 0, te?.code ?? "error", msg);
46394
- } finally {
46395
- setPendingOutputCount((c) => Math.max(0, c - 1));
48070
+ markRequestError("en-to-zh", totalElapsed + (te?.elapsed_ms ?? 0), te?.code ?? "error", msg);
48071
+ }
48072
+ }
48073
+ function appendRawToolEntry(ev) {
48074
+ if (ev.kind === "tool_use") {
48075
+ setOutputEntries((entries) => [
48076
+ ...entries,
48077
+ {
48078
+ id: ev.id,
48079
+ timestamp: ev.timestamp,
48080
+ kind: "tool_use",
48081
+ toolUse: ev.toolUse,
48082
+ state: "translated"
48083
+ }
48084
+ ]);
48085
+ } else {
48086
+ setOutputEntries((entries) => [
48087
+ ...entries,
48088
+ {
48089
+ id: ev.id,
48090
+ timestamp: ev.timestamp,
48091
+ kind: "tool_result",
48092
+ toolResult: ev.toolResult,
48093
+ state: "translated"
48094
+ }
48095
+ ]);
46396
48096
  }
46397
48097
  }
48098
+ function handleSessionChange(next) {
48099
+ try {
48100
+ writeState(projectDir, {
48101
+ selected_session_id: next.id,
48102
+ selected_at: new Date().toISOString(),
48103
+ transcript_path: next.path
48104
+ });
48105
+ } catch (err) {
48106
+ setStatusLine(`✗ failed to persist session pick: ${err.message}`);
48107
+ }
48108
+ seenIdsRef.current.clear();
48109
+ setLastAssistantText("");
48110
+ bellRef.current?.cancel();
48111
+ const shortPrev = session.id.slice(0, 8);
48112
+ const shortNext = next.id.slice(0, 8);
48113
+ setOutputEntries((entries) => [
48114
+ ...entries,
48115
+ {
48116
+ id: `__divider_${Date.now()}`,
48117
+ timestamp: new Date().toISOString(),
48118
+ kind: "text",
48119
+ english: "",
48120
+ chinese: `已切换 session: ${shortPrev}… → ${shortNext}…`,
48121
+ state: "translated"
48122
+ }
48123
+ ]);
48124
+ setSession(next);
48125
+ setStatusLine(`✓ switched to session ${shortNext}…`);
48126
+ }
46398
48127
  async function saveSettingsFromModal(s) {
46399
48128
  const path = userConfigPath();
46400
48129
  let existing = {};
@@ -46410,7 +48139,13 @@ function App2({
46410
48139
  api_key: s.api_key,
46411
48140
  base_url: s.base_url,
46412
48141
  model: s.model,
46413
- timeout_seconds: Number(s.timeout_seconds)
48142
+ timeout_seconds: Number(s.timeout_seconds),
48143
+ notify_on_complete: s.notify_on_complete,
48144
+ translate_thinking: s.translate_thinking,
48145
+ translate_todos: s.translate_todos,
48146
+ translate_agent: s.translate_agent,
48147
+ context_enabled: s.context_enabled,
48148
+ show_tool_calls: s.show_tool_calls
46414
48149
  };
46415
48150
  const promptsClean = stripEmpty(s.prompts);
46416
48151
  if (promptsClean)
@@ -46418,9 +48153,8 @@ function App2({
46418
48153
  else
46419
48154
  delete merged["prompts"];
46420
48155
  try {
46421
- mkdirSync3(dirname3(path), { recursive: true });
46422
- writeFileSync3(path, JSON.stringify(merged, null, 2) + `
46423
- `, "utf-8");
48156
+ writeAtomic(path, JSON.stringify(merged, null, 2) + `
48157
+ `, { mode: 384 });
46424
48158
  } catch (err) {
46425
48159
  setStatusLine(`✗ save failed: ${err.message}`);
46426
48160
  setShowSettings(false);
@@ -46487,63 +48221,89 @@ function App2({
46487
48221
  setError(null);
46488
48222
  setStatusLine(undefined);
46489
48223
  setAppState("idle");
46490
- historyCursorRef.current = -1;
48224
+ setPendingConcern(null);
48225
+ translatedInputRef.current = "";
46491
48226
  return;
46492
48227
  }
46493
- if (key.ctrl && input === "l") {
48228
+ });
48229
+ function commitTranslation(trimmedInput, en) {
48230
+ try {
48231
+ const path = writeUserInput(projectDir, en);
48232
+ appendHistory(projectDir, {
48233
+ timestamp: new Date().toISOString(),
48234
+ direction: "zh-to-en",
48235
+ input: trimmedInput,
48236
+ output: en
48237
+ });
48238
+ setStreamingEnglish("");
48239
+ setLastEnglish(en);
48240
+ setStatusLine(`✓ wrote ${path} — switch to Claude Code, type /i ↵`);
46494
48241
  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;
48242
+ setPendingConcern(null);
48243
+ translatedInputRef.current = "";
48244
+ setAppState("idle");
48245
+ } catch (err) {
48246
+ const msg = err instanceof Error ? err.message : String(err);
48247
+ setError(`failed to write user-input.md: ${msg}`);
48248
+ setAppState("error");
46529
48249
  }
46530
- });
48250
+ }
46531
48251
  async function handleSubmit() {
46532
48252
  const trimmed = chineseInput.trim();
46533
48253
  if (!trimmed)
46534
48254
  return;
46535
48255
  if (appState !== "idle")
46536
48256
  return;
48257
+ if (pendingConcern && trimmed === translatedInputRef.current) {
48258
+ commitTranslation(trimmed, lastEnglish);
48259
+ return;
48260
+ }
48261
+ if (pendingConcern) {
48262
+ setPendingConcern(null);
48263
+ translatedInputRef.current = "";
48264
+ }
46537
48265
  setAppState("translating");
46538
48266
  setError(null);
46539
48267
  setStatusLine(undefined);
46540
48268
  setLastEnglish("");
48269
+ setStreamingEnglish("");
48270
+ const ctx = config.context_enabled && lastAssistantText.trim() ? lastAssistantText : undefined;
48271
+ const useContext7 = ctx !== undefined;
48272
+ let buf = "";
48273
+ let translationStart = -1;
46541
48274
  let en;
48275
+ let concern = null;
46542
48276
  markRequestStarted("zh-to-en");
46543
48277
  try {
46544
- const ctx = contextEnabled && lastAssistantText.trim() ? lastAssistantText : undefined;
46545
- const result2 = await translate(trimmed, "zh-to-en", config, ctx);
48278
+ const result2 = await translate(trimmed, "zh-to-en", config, {
48279
+ contextText: ctx,
48280
+ parseConcernHeader: useContext7,
48281
+ onDelta: (delta) => {
48282
+ buf += delta;
48283
+ if (!useContext7) {
48284
+ setStreamingEnglish(buf);
48285
+ return;
48286
+ }
48287
+ if (translationStart === -1) {
48288
+ const nl = buf.indexOf(`
48289
+ `);
48290
+ if (nl === -1)
48291
+ return;
48292
+ const firstLine = buf.slice(0, nl).trim();
48293
+ if (firstLine === "OK" || firstLine.startsWith("WARN:")) {
48294
+ let i = nl + 1;
48295
+ while (i < buf.length && /\s/.test(buf[i]))
48296
+ i += 1;
48297
+ translationStart = i;
48298
+ } else {
48299
+ translationStart = 0;
48300
+ }
48301
+ }
48302
+ setStreamingEnglish(buf.slice(translationStart));
48303
+ }
48304
+ });
46546
48305
  en = result2.content;
48306
+ concern = result2.concern ?? null;
46547
48307
  markRequestSuccess("zh-to-en", result2.elapsed_ms, result2.usage?.total_tokens ?? null);
46548
48308
  } catch (err) {
46549
48309
  const te = err instanceof TranslateError ? err : null;
@@ -46551,34 +48311,20 @@ function App2({
46551
48311
  markRequestError("zh-to-en", te?.elapsed_ms ?? 0, te?.code ?? "error", msg);
46552
48312
  setError(msg);
46553
48313
  setAppState("error");
48314
+ setStreamingEnglish("");
46554
48315
  return;
46555
48316
  }
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;
48317
+ if (concern) {
48318
+ setPendingConcern(concern);
46571
48319
  setLastEnglish(en);
46572
- setStatusLine(`✓ wrote ${path} — switch to Claude Code, type /i ↵`);
46573
- setChineseInput("");
48320
+ setStreamingEnglish("");
48321
+ translatedInputRef.current = trimmed;
46574
48322
  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");
48323
+ return;
46579
48324
  }
48325
+ commitTranslation(trimmed, en);
46580
48326
  }
46581
- import_react29.useEffect(() => {
48327
+ import_react30.useEffect(() => {
46582
48328
  if (statusLine && appState === "idle") {
46583
48329
  const t = setTimeout(() => setStatusLine(undefined), 8000);
46584
48330
  return () => clearTimeout(t);
@@ -46588,35 +48334,40 @@ function App2({
46588
48334
  const inputDisabled = appState === "translating";
46589
48335
  const combinedStatus = statusLine ?? (tailError ? `tail error: ${tailError}` : undefined);
46590
48336
  if (showSettings) {
46591
- return /* @__PURE__ */ import_react29.default.createElement(SettingsModal, {
48337
+ return /* @__PURE__ */ import_react30.default.createElement(SettingsModal, {
46592
48338
  initialConfig: config,
46593
- onSave: saveSettingsFromModal
48339
+ onSave: saveSettingsFromModal,
48340
+ currentSession: session,
48341
+ projectDir,
48342
+ onSessionChange: handleSessionChange
46594
48343
  });
46595
48344
  }
46596
- return /* @__PURE__ */ import_react29.default.createElement(import_react29.default.Fragment, null, /* @__PURE__ */ import_react29.default.createElement(OutputPane, {
48345
+ return /* @__PURE__ */ import_react30.default.createElement(import_react30.default.Fragment, null, /* @__PURE__ */ import_react30.default.createElement(OutputPane, {
46597
48346
  entries: outputEntries,
46598
- pendingCount: pendingOutputCount
46599
- }), /* @__PURE__ */ import_react29.default.createElement(Box_default, {
48347
+ streaming: streamingOutputs
48348
+ }), /* @__PURE__ */ import_react30.default.createElement(Box_default, {
46600
48349
  flexDirection: "column"
46601
- }, /* @__PURE__ */ import_react29.default.createElement(StatusBar, {
48350
+ }, /* @__PURE__ */ import_react30.default.createElement(StatusBar, {
46602
48351
  session,
46603
48352
  appState,
46604
48353
  model: config.model,
46605
48354
  statusLine: combinedStatus,
46606
- contextEnabled,
48355
+ contextEnabled: config.context_enabled,
46607
48356
  contextSize: lastAssistantText.length,
46608
48357
  lastRequest,
46609
48358
  sessionMetrics
46610
- }), /* @__PURE__ */ import_react29.default.createElement(InputPane, {
48359
+ }), /* @__PURE__ */ import_react30.default.createElement(InputPane, {
46611
48360
  value: chineseInput,
46612
48361
  onChange: setChineseInput,
46613
48362
  onSubmit: () => void handleSubmit(),
46614
48363
  disabled: inputDisabled,
46615
48364
  placeholder: inputDisabled ? "" : "输入中文,回车直接翻译并发送…"
46616
- }), /* @__PURE__ */ import_react29.default.createElement(PreviewPane, {
48365
+ }), /* @__PURE__ */ import_react30.default.createElement(PreviewPane, {
46617
48366
  text: appState === "idle" ? lastEnglish : "",
46618
48367
  translating: appState === "translating",
46619
- error: appState === "error" ? error : null
48368
+ streamingText: streamingEnglish,
48369
+ error: appState === "error" ? error : null,
48370
+ concern: pendingConcern
46620
48371
  })));
46621
48372
  }
46622
48373
  function stripEmpty(map2) {
@@ -46627,128 +48378,144 @@ function stripEmpty(map2) {
46627
48378
  }
46628
48379
  return Object.keys(out).length > 0 ? out : undefined;
46629
48380
  }
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);
48381
+ function seedTranslatedPayload(raw) {
48382
+ const seedOption = (o) => {
48383
+ const out = {
48384
+ label: seedString(o.label),
48385
+ description: seedString(o.description)
48386
+ };
48387
+ if (o.preview !== undefined)
48388
+ out.preview = o.preview;
48389
+ return out;
48390
+ };
48391
+ return {
48392
+ questions: raw.questions.map((q) => ({
48393
+ question: seedString(q.question),
48394
+ header: q.header,
48395
+ multiSelect: q.multiSelect,
48396
+ options: q.options.map(seedOption)
48397
+ }))
48398
+ };
46637
48399
  }
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);
48400
+ function seedTranslatedTodos(raw) {
48401
+ return {
48402
+ todos: raw.todos.map((t) => ({
48403
+ content: seedString(t.content),
48404
+ activeForm: seedString(t.activeForm),
48405
+ status: t.status
48406
+ }))
48407
+ };
48408
+ }
48409
+ function seedTranslatedAgent(raw) {
48410
+ return {
48411
+ description: seedString(raw.description),
48412
+ prompt: seedString(raw.prompt),
48413
+ subagent_type: raw.subagent_type
48414
+ };
48415
+ }
48416
+ function patchField(payload, qi, which, oi, zh) {
48417
+ return {
48418
+ questions: payload.questions.map((q, i) => {
48419
+ if (i !== qi)
48420
+ return q;
48421
+ if (which === "question") {
48422
+ return { ...q, question: { ...q.question, zh } };
46668
48423
  }
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"))));
48424
+ return {
48425
+ ...q,
48426
+ options: q.options.map((o, j) => {
48427
+ if (j !== oi)
48428
+ return o;
48429
+ if (which === "label")
48430
+ return { ...o, label: { ...o.label, zh } };
48431
+ return { ...o, description: { ...o.description, zh } };
48432
+ })
48433
+ };
48434
+ })
48435
+ };
48436
+ }
48437
+ function patchTodoField(payload, i, which, zh) {
48438
+ return {
48439
+ todos: payload.todos.map((t, j) => {
48440
+ if (j !== i)
48441
+ return t;
48442
+ if (which === "content")
48443
+ return { ...t, content: { ...t.content, zh } };
48444
+ return { ...t, activeForm: { ...t.activeForm, zh } };
48445
+ })
48446
+ };
46748
48447
  }
48448
+ // package.json
48449
+ var package_default = {
48450
+ name: "cc-zh-watcher",
48451
+ version: "0.2.0",
48452
+ description: "Side-channel Chinese ↔ English bridge for Claude Code (single-watcher TUI). Type Chinese, Claude sees English; Claude replies in English, you see Chinese.",
48453
+ type: "module",
48454
+ bin: {
48455
+ "cc-zh-watcher": "dist/cli.js"
48456
+ },
48457
+ main: "./dist/cli.js",
48458
+ files: [
48459
+ "dist/cli.js",
48460
+ "README.md",
48461
+ "LICENSE"
48462
+ ],
48463
+ scripts: {
48464
+ dev: "bun run src/cli.ts",
48465
+ build: "bun build src/cli.ts --outfile dist/cli.js --target node",
48466
+ compile: "bun build src/cli.ts --compile --outfile dist/cc-zh-watcher",
48467
+ "compile:linux-x64": "bun build src/cli.ts --compile --target=bun-linux-x64 --outfile dist/cc-zh-watcher-linux-x64",
48468
+ "compile:linux-arm64": "bun build src/cli.ts --compile --target=bun-linux-arm64 --outfile dist/cc-zh-watcher-linux-arm64",
48469
+ "compile:darwin-x64": "bun build src/cli.ts --compile --target=bun-darwin-x64 --outfile dist/cc-zh-watcher-darwin-x64",
48470
+ "compile:darwin-arm64": "bun build src/cli.ts --compile --target=bun-darwin-arm64 --outfile dist/cc-zh-watcher-darwin-arm64",
48471
+ "compile:windows-x64": "bun build src/cli.ts --compile --target=bun-windows-x64 --outfile dist/cc-zh-watcher-windows-x64.exe",
48472
+ "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",
48473
+ test: "bun test",
48474
+ typecheck: "tsc --noEmit",
48475
+ prepublishOnly: "bun run typecheck && bun test && bun run build",
48476
+ "install:local": "bash scripts/install-local.sh",
48477
+ "uninstall:local": "npm uninstall -g cc-zh-watcher"
48478
+ },
48479
+ keywords: [
48480
+ "claude-code",
48481
+ "claude",
48482
+ "translation",
48483
+ "chinese",
48484
+ "i18n",
48485
+ "tui",
48486
+ "ink",
48487
+ "cli",
48488
+ "openai",
48489
+ "deepseek"
48490
+ ],
48491
+ author: "storm <xm4763@gmail.com>",
48492
+ license: "MIT",
48493
+ repository: {
48494
+ type: "git",
48495
+ url: "git+https://github.com/CodingForMoney/cc-zh-watcher.git"
48496
+ },
48497
+ homepage: "https://github.com/CodingForMoney/cc-zh-watcher/#readme",
48498
+ bugs: {
48499
+ url: "https://github.com/CodingForMoney/cc-zh-watcher/issues"
48500
+ },
48501
+ engines: {
48502
+ node: ">=20.0.0"
48503
+ },
48504
+ devDependencies: {
48505
+ "@types/node": "^20.14.0",
48506
+ "@types/react": "^18.3.0",
48507
+ "bun-types": "^1.3.13",
48508
+ commander: "^12.1.0",
48509
+ ink: "^5.0.1",
48510
+ "ink-text-input": "^6.0.0",
48511
+ react: "^18.3.1",
48512
+ "react-devtools-core": "^7.0.1",
48513
+ typescript: "^5.5.0"
48514
+ }
48515
+ };
46749
48516
 
46750
48517
  // src/cli.ts
46751
- var VERSION = "0.1.0";
48518
+ var VERSION = package_default.version;
46752
48519
  function fail(msg, code = 1) {
46753
48520
  process.stderr.write(`error: ${msg}
46754
48521
  `);
@@ -46817,37 +48584,42 @@ async function resolveSession(opts) {
46817
48584
  return picked;
46818
48585
  }
46819
48586
  async function runWatch(opts) {
46820
- const projectDir = process.cwd();
48587
+ const cwd2 = process.cwd();
46821
48588
  let config;
46822
48589
  try {
46823
- config = loadConfig({ projectDir, explicitPath: opts.config });
48590
+ config = loadConfig({ projectDir: cwd2, explicitPath: opts.config });
46824
48591
  } catch (err) {
46825
- fail(`${err.message}`);
48592
+ const msg = err.message;
48593
+ if (msg.includes("no api_key configured")) {
48594
+ await runInlineConfigure();
48595
+ try {
48596
+ config = loadConfig({ projectDir: cwd2, explicitPath: opts.config });
48597
+ } catch (err2) {
48598
+ fail(err2.message);
48599
+ }
48600
+ } else {
48601
+ fail(msg);
48602
+ }
46826
48603
  }
46827
48604
  const session = await resolveSession({
46828
48605
  pick: opts.pick,
46829
48606
  sessionPrefix: opts.session,
46830
- projectDir
48607
+ projectDir: cwd2
46831
48608
  });
46832
48609
  if (!session) {
46833
48610
  fail("no session selected", 0);
46834
48611
  }
48612
+ const projectDir = session.cwd ?? cwd2;
46835
48613
  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.
48614
+ const accepted = await promptInstallSlashCommand(projectDir);
48615
+ if (!accepted) {
48616
+ process.stderr.write(`note: /i not installed — you'll need to manually paste the
48617
+ ` + ` translated English into Claude Code each turn.
46837
48618
  `);
48619
+ }
46838
48620
  }
46839
48621
  render_default(import_react31.default.createElement(App2, { session, config, projectDir }));
46840
48622
  }
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
48623
  async function runUninstall(opts) {
46852
48624
  const projectDir = process.cwd();
46853
48625
  let wipe = opts.wipeCache;
@@ -46863,18 +48635,18 @@ async function runUninstall(opts) {
46863
48635
  for (const s of skipped)
46864
48636
  ok(` (not present) ${s}`);
46865
48637
  }
46866
- async function runConfigure() {
48638
+ async function runInlineConfigure() {
46867
48639
  const path = userConfigPath();
46868
48640
  let existing = {};
46869
48641
  if (existsSync8(path)) {
46870
48642
  try {
46871
48643
  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
48644
  } catch {}
46877
48645
  }
48646
+ ok("");
48647
+ ok("─ First-time setup ─────────────────────────────────────────");
48648
+ ok("No API endpoint configured yet. Set one up now (Ctrl+C to abort).");
48649
+ ok("");
46878
48650
  const rl = createInterface({ input, output });
46879
48651
  const ask = async (prompt, fallback) => {
46880
48652
  const ans = (await rl.question(prompt)).trim();
@@ -46894,10 +48666,31 @@ async function runConfigure() {
46894
48666
  model,
46895
48667
  timeout_seconds: Number(timeoutStr) || 30
46896
48668
  };
46897
- mkdirSync4(dirname4(path), { recursive: true });
46898
- writeFileSync4(path, JSON.stringify(next, null, 2) + `
46899
- `, "utf-8");
48669
+ writeAtomic(path, JSON.stringify(next, null, 2) + `
48670
+ `, { mode: 384 });
46900
48671
  ok(`✓ wrote ${path}`);
48672
+ ok("");
48673
+ }
48674
+ async function promptInstallSlashCommand(projectDir) {
48675
+ const target = slashCommandPath(projectDir);
48676
+ const rl = createInterface({ input, output });
48677
+ const ans = (await rl.question(`
48678
+ /i slash command not yet installed in this project.
48679
+ ` + ` project: ${projectDir}
48680
+ ` + ` will write: ${target}
48681
+ ` + `Install now? [Y/n] `)).trim().toLowerCase();
48682
+ rl.close();
48683
+ if (ans === "n" || ans === "no")
48684
+ return false;
48685
+ try {
48686
+ installSlashCommand(projectDir);
48687
+ ok(`✓ installed ${target}`);
48688
+ return true;
48689
+ } catch (err) {
48690
+ process.stderr.write(`✗ install failed: ${err.message}
48691
+ `);
48692
+ return false;
48693
+ }
46901
48694
  }
46902
48695
  function runPromptsShow(opts) {
46903
48696
  let config = null;
@@ -47009,17 +48802,15 @@ function runSessionsList() {
47009
48802
  state file: ${stateFilePath(projectDir)}`);
47010
48803
  }
47011
48804
  var program2 = new Command;
47012
- program2.name("cc-zh-watcher").version(VERSION);
48805
+ program2.name("cc-zh-watcher").version(VERSION, "-v, --version", "output the version number");
47013
48806
  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
48807
  await runWatch(opts);
47015
48808
  });
47016
- program2.command("install").description("install /i slash command into <project>/.claude/commands/").action(runInstall);
47017
48809
  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
48810
  await runUninstall({
47019
48811
  wipeCache: opts.wipeCache ? true : opts.keepCache ? false : undefined
47020
48812
  });
47021
48813
  });
47022
- program2.command("configure").description("interactively create / edit user-global config").action(runConfigure);
47023
48814
  program2.command("sessions").description("list all sessions in this project").action(runSessionsList);
47024
48815
  var promptsCmd = program2.command("prompts").description("inspect / introspect the translation prompts");
47025
48816
  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) => {