codex-partner 0.0.1__py3-none-any.whl

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 (244) hide show
  1. app.py +5530 -0
  2. codex_partner/__init__.py +4 -0
  3. codex_partner/__main__.py +6 -0
  4. codex_partner/app_server.py +150 -0
  5. codex_partner/cli.py +35 -0
  6. codex_partner/commands.py +77 -0
  7. codex_partner/database.py +206 -0
  8. codex_partner/discovery.py +136 -0
  9. codex_partner/schemas.py +133 -0
  10. codex_partner-0.0.1.dist-info/METADATA +101 -0
  11. codex_partner-0.0.1.dist-info/RECORD +244 -0
  12. codex_partner-0.0.1.dist-info/WHEEL +5 -0
  13. codex_partner-0.0.1.dist-info/entry_points.txt +2 -0
  14. codex_partner-0.0.1.dist-info/top_level.txt +3 -0
  15. static/__init__.py +1 -0
  16. static/app.js +505 -0
  17. static/chat-worker.js +157 -0
  18. static/conversation.js +894 -0
  19. static/core.js +880 -0
  20. static/editor.js +142 -0
  21. static/index.html +71 -0
  22. static/mascot-dance.css +100 -0
  23. static/mascot-dance.js +46 -0
  24. static/settings.js +335 -0
  25. static/styles.css +690 -0
  26. static/vendor/ace/LICENSE +24 -0
  27. static/vendor/ace/ace.js +23 -0
  28. static/vendor/ace/ext-modelist.js +8 -0
  29. static/vendor/ace/mode-abap.js +8 -0
  30. static/vendor/ace/mode-abc.js +8 -0
  31. static/vendor/ace/mode-actionscript.js +8 -0
  32. static/vendor/ace/mode-ada.js +8 -0
  33. static/vendor/ace/mode-alda.js +8 -0
  34. static/vendor/ace/mode-apache_conf.js +8 -0
  35. static/vendor/ace/mode-apex.js +8 -0
  36. static/vendor/ace/mode-applescript.js +8 -0
  37. static/vendor/ace/mode-aql.js +8 -0
  38. static/vendor/ace/mode-asciidoc.js +8 -0
  39. static/vendor/ace/mode-asl.js +8 -0
  40. static/vendor/ace/mode-assembly_arm32.js +8 -0
  41. static/vendor/ace/mode-assembly_x86.js +8 -0
  42. static/vendor/ace/mode-astro.js +8 -0
  43. static/vendor/ace/mode-autohotkey.js +8 -0
  44. static/vendor/ace/mode-basic.js +8 -0
  45. static/vendor/ace/mode-batchfile.js +8 -0
  46. static/vendor/ace/mode-bibtex.js +8 -0
  47. static/vendor/ace/mode-c9search.js +8 -0
  48. static/vendor/ace/mode-c_cpp.js +8 -0
  49. static/vendor/ace/mode-cedar.js +8 -0
  50. static/vendor/ace/mode-cedarschema.js +8 -0
  51. static/vendor/ace/mode-cirru.js +8 -0
  52. static/vendor/ace/mode-clojure.js +8 -0
  53. static/vendor/ace/mode-clue.js +8 -0
  54. static/vendor/ace/mode-cobol.js +8 -0
  55. static/vendor/ace/mode-coffee.js +8 -0
  56. static/vendor/ace/mode-coldfusion.js +8 -0
  57. static/vendor/ace/mode-crystal.js +8 -0
  58. static/vendor/ace/mode-csharp.js +8 -0
  59. static/vendor/ace/mode-csound_document.js +8 -0
  60. static/vendor/ace/mode-csound_orchestra.js +8 -0
  61. static/vendor/ace/mode-csound_score.js +8 -0
  62. static/vendor/ace/mode-csp.js +8 -0
  63. static/vendor/ace/mode-css.js +8 -0
  64. static/vendor/ace/mode-csv.js +8 -0
  65. static/vendor/ace/mode-curly.js +8 -0
  66. static/vendor/ace/mode-cuttlefish.js +8 -0
  67. static/vendor/ace/mode-d.js +8 -0
  68. static/vendor/ace/mode-dart.js +8 -0
  69. static/vendor/ace/mode-diff.js +8 -0
  70. static/vendor/ace/mode-django.js +8 -0
  71. static/vendor/ace/mode-dockerfile.js +8 -0
  72. static/vendor/ace/mode-dot.js +8 -0
  73. static/vendor/ace/mode-drools.js +8 -0
  74. static/vendor/ace/mode-edifact.js +8 -0
  75. static/vendor/ace/mode-eiffel.js +8 -0
  76. static/vendor/ace/mode-ejs.js +8 -0
  77. static/vendor/ace/mode-elixir.js +8 -0
  78. static/vendor/ace/mode-elm.js +8 -0
  79. static/vendor/ace/mode-erlang.js +8 -0
  80. static/vendor/ace/mode-flix.js +8 -0
  81. static/vendor/ace/mode-forth.js +8 -0
  82. static/vendor/ace/mode-fortran.js +8 -0
  83. static/vendor/ace/mode-fsharp.js +8 -0
  84. static/vendor/ace/mode-fsl.js +8 -0
  85. static/vendor/ace/mode-ftl.js +8 -0
  86. static/vendor/ace/mode-gcode.js +8 -0
  87. static/vendor/ace/mode-gherkin.js +8 -0
  88. static/vendor/ace/mode-gitignore.js +8 -0
  89. static/vendor/ace/mode-glsl.js +8 -0
  90. static/vendor/ace/mode-gobstones.js +8 -0
  91. static/vendor/ace/mode-golang.js +8 -0
  92. static/vendor/ace/mode-graphqlschema.js +8 -0
  93. static/vendor/ace/mode-groovy.js +8 -0
  94. static/vendor/ace/mode-groq.js +8 -0
  95. static/vendor/ace/mode-haml.js +8 -0
  96. static/vendor/ace/mode-handlebars.js +8 -0
  97. static/vendor/ace/mode-haskell.js +8 -0
  98. static/vendor/ace/mode-haskell_cabal.js +8 -0
  99. static/vendor/ace/mode-haxe.js +8 -0
  100. static/vendor/ace/mode-hjson.js +8 -0
  101. static/vendor/ace/mode-html.js +8 -0
  102. static/vendor/ace/mode-html_elixir.js +8 -0
  103. static/vendor/ace/mode-html_ruby.js +8 -0
  104. static/vendor/ace/mode-ini.js +8 -0
  105. static/vendor/ace/mode-io.js +8 -0
  106. static/vendor/ace/mode-ion.js +8 -0
  107. static/vendor/ace/mode-jack.js +8 -0
  108. static/vendor/ace/mode-jade.js +8 -0
  109. static/vendor/ace/mode-java.js +8 -0
  110. static/vendor/ace/mode-javascript.js +8 -0
  111. static/vendor/ace/mode-jexl.js +8 -0
  112. static/vendor/ace/mode-json.js +8 -0
  113. static/vendor/ace/mode-json5.js +8 -0
  114. static/vendor/ace/mode-jsoniq.js +8 -0
  115. static/vendor/ace/mode-jsp.js +8 -0
  116. static/vendor/ace/mode-jssm.js +8 -0
  117. static/vendor/ace/mode-jsx.js +8 -0
  118. static/vendor/ace/mode-julia.js +8 -0
  119. static/vendor/ace/mode-kotlin.js +8 -0
  120. static/vendor/ace/mode-latex.js +8 -0
  121. static/vendor/ace/mode-latte.js +8 -0
  122. static/vendor/ace/mode-less.js +8 -0
  123. static/vendor/ace/mode-liquid.js +8 -0
  124. static/vendor/ace/mode-lisp.js +8 -0
  125. static/vendor/ace/mode-livescript.js +8 -0
  126. static/vendor/ace/mode-logiql.js +8 -0
  127. static/vendor/ace/mode-logtalk.js +8 -0
  128. static/vendor/ace/mode-lsl.js +8 -0
  129. static/vendor/ace/mode-lua.js +8 -0
  130. static/vendor/ace/mode-luapage.js +8 -0
  131. static/vendor/ace/mode-lucene.js +8 -0
  132. static/vendor/ace/mode-makefile.js +8 -0
  133. static/vendor/ace/mode-mariadb.js +8 -0
  134. static/vendor/ace/mode-markdown.js +8 -0
  135. static/vendor/ace/mode-mask.js +8 -0
  136. static/vendor/ace/mode-matlab.js +8 -0
  137. static/vendor/ace/mode-maze.js +8 -0
  138. static/vendor/ace/mode-mediawiki.js +8 -0
  139. static/vendor/ace/mode-mel.js +8 -0
  140. static/vendor/ace/mode-mips.js +8 -0
  141. static/vendor/ace/mode-mixal.js +8 -0
  142. static/vendor/ace/mode-mushcode.js +8 -0
  143. static/vendor/ace/mode-mysql.js +8 -0
  144. static/vendor/ace/mode-nasal.js +8 -0
  145. static/vendor/ace/mode-nginx.js +8 -0
  146. static/vendor/ace/mode-nim.js +8 -0
  147. static/vendor/ace/mode-nix.js +8 -0
  148. static/vendor/ace/mode-nsis.js +8 -0
  149. static/vendor/ace/mode-nunjucks.js +8 -0
  150. static/vendor/ace/mode-objectivec.js +8 -0
  151. static/vendor/ace/mode-ocaml.js +8 -0
  152. static/vendor/ace/mode-odin.js +8 -0
  153. static/vendor/ace/mode-partiql.js +8 -0
  154. static/vendor/ace/mode-pascal.js +8 -0
  155. static/vendor/ace/mode-perl.js +8 -0
  156. static/vendor/ace/mode-pgsql.js +8 -0
  157. static/vendor/ace/mode-php.js +8 -0
  158. static/vendor/ace/mode-php_laravel_blade.js +8 -0
  159. static/vendor/ace/mode-pig.js +8 -0
  160. static/vendor/ace/mode-plain_text.js +8 -0
  161. static/vendor/ace/mode-plsql.js +8 -0
  162. static/vendor/ace/mode-powershell.js +8 -0
  163. static/vendor/ace/mode-praat.js +8 -0
  164. static/vendor/ace/mode-prisma.js +8 -0
  165. static/vendor/ace/mode-prolog.js +8 -0
  166. static/vendor/ace/mode-properties.js +8 -0
  167. static/vendor/ace/mode-protobuf.js +8 -0
  168. static/vendor/ace/mode-prql.js +8 -0
  169. static/vendor/ace/mode-puppet.js +8 -0
  170. static/vendor/ace/mode-python.js +8 -0
  171. static/vendor/ace/mode-qml.js +8 -0
  172. static/vendor/ace/mode-r.js +8 -0
  173. static/vendor/ace/mode-raku.js +8 -0
  174. static/vendor/ace/mode-razor.js +8 -0
  175. static/vendor/ace/mode-rdoc.js +8 -0
  176. static/vendor/ace/mode-red.js +8 -0
  177. static/vendor/ace/mode-redshift.js +8 -0
  178. static/vendor/ace/mode-rhtml.js +8 -0
  179. static/vendor/ace/mode-robot.js +8 -0
  180. static/vendor/ace/mode-rst.js +8 -0
  181. static/vendor/ace/mode-ruby.js +8 -0
  182. static/vendor/ace/mode-rust.js +8 -0
  183. static/vendor/ace/mode-sac.js +8 -0
  184. static/vendor/ace/mode-sass.js +8 -0
  185. static/vendor/ace/mode-scad.js +8 -0
  186. static/vendor/ace/mode-scala.js +8 -0
  187. static/vendor/ace/mode-scheme.js +8 -0
  188. static/vendor/ace/mode-scrypt.js +8 -0
  189. static/vendor/ace/mode-scss.js +8 -0
  190. static/vendor/ace/mode-sh.js +8 -0
  191. static/vendor/ace/mode-sjs.js +8 -0
  192. static/vendor/ace/mode-slim.js +8 -0
  193. static/vendor/ace/mode-smarty.js +8 -0
  194. static/vendor/ace/mode-smithy.js +8 -0
  195. static/vendor/ace/mode-snippets.js +8 -0
  196. static/vendor/ace/mode-soy_template.js +8 -0
  197. static/vendor/ace/mode-space.js +8 -0
  198. static/vendor/ace/mode-sparql.js +8 -0
  199. static/vendor/ace/mode-sql.js +8 -0
  200. static/vendor/ace/mode-sqlserver.js +8 -0
  201. static/vendor/ace/mode-stylus.js +8 -0
  202. static/vendor/ace/mode-svg.js +8 -0
  203. static/vendor/ace/mode-swift.js +8 -0
  204. static/vendor/ace/mode-tcl.js +8 -0
  205. static/vendor/ace/mode-terraform.js +8 -0
  206. static/vendor/ace/mode-tex.js +8 -0
  207. static/vendor/ace/mode-text.js +8 -0
  208. static/vendor/ace/mode-textile.js +8 -0
  209. static/vendor/ace/mode-toml.js +8 -0
  210. static/vendor/ace/mode-tsv.js +8 -0
  211. static/vendor/ace/mode-tsx.js +8 -0
  212. static/vendor/ace/mode-turtle.js +8 -0
  213. static/vendor/ace/mode-twig.js +8 -0
  214. static/vendor/ace/mode-typescript.js +8 -0
  215. static/vendor/ace/mode-vala.js +8 -0
  216. static/vendor/ace/mode-vbscript.js +8 -0
  217. static/vendor/ace/mode-velocity.js +8 -0
  218. static/vendor/ace/mode-verilog.js +8 -0
  219. static/vendor/ace/mode-vhdl.js +8 -0
  220. static/vendor/ace/mode-visualforce.js +8 -0
  221. static/vendor/ace/mode-vue.js +8 -0
  222. static/vendor/ace/mode-wollok.js +8 -0
  223. static/vendor/ace/mode-xml.js +8 -0
  224. static/vendor/ace/mode-xquery.js +8 -0
  225. static/vendor/ace/mode-yaml.js +8 -0
  226. static/vendor/ace/mode-zeek.js +8 -0
  227. static/vendor/ace/mode-zig.js +8 -0
  228. static/vendor/ace/theme-tomorrow_night_eighties.js +8 -0
  229. static/vendor/i18next/LICENSE +21 -0
  230. static/vendor/i18next/i18next.min.js +1 -0
  231. static/vendor/toastui-editor/LICENSE +21 -0
  232. static/vendor/toastui-editor/toastui-editor-dark.css +474 -0
  233. static/vendor/toastui-editor/toastui-editor.css +1597 -0
  234. static/vendor/toastui-editor/toastui-editor.js +24 -0
  235. static/vendor/toastui-editor/zh-cn.js +7 -0
  236. static/vendor/xterm/LICENSE +21 -0
  237. static/vendor/xterm/addons/fit.LICENSE +19 -0
  238. static/vendor/xterm/addons/fit.js +2 -0
  239. static/vendor/xterm/addons/unicode11.LICENSE +19 -0
  240. static/vendor/xterm/addons/unicode11.js +2 -0
  241. static/vendor/xterm/addons/web-links.LICENSE +19 -0
  242. static/vendor/xterm/addons/web-links.js +2 -0
  243. static/vendor/xterm/xterm.css +285 -0
  244. static/vendor/xterm/xterm.js +2 -0
static/core.js ADDED
@@ -0,0 +1,880 @@
1
+ // Shared state, transport, formatting, and realtime synchronization primitives.
2
+ const state = {
3
+ tasks: [], sessions: [], skills: [], providers: [], memories: [], generatedMemories: [], commands: [], sshHosts: [], sshConfig: "", activeSSHHost: localStorage.getItem("codex-dashboard-ssh-host") || "",
4
+ selectedId: null, selectedTask: null, selectedEvents: [], selectedMessages: [], sessionRequestId: 0, workspacePath: "", workspaceFile: null, workspaceRequestId: 0, workspacePickerPath: "", workspaceUploading: false, inspectorClosed: false, filter: "all", query: "",
5
+ historyCursor: "", historyHasMore: false, historyLoading: false, chatBlocks: [], chatVirtualStart: null, chatAverageHeight: 112,
6
+ commandIndex: 0, rawActivity: true, composerHistory: [], historyIndex: -1, editingQueuedId: null, defaultWorkspace: "", codexAvailable: true, codexInstall: null, codexInfo: null,
7
+ runtimeMetrics: { taskId: "", ttftMs: null, tpotMs: null, estimated: true, outputTokens: 0 }, serverInfo: { user: "", hostname: "" }, chatSnapToBottom: false, userAvatar: localStorage.getItem("codex-dashboard-user-avatar") || "",
8
+ };
9
+ state.selectedId = localStorage.getItem("codex-dashboard-session") || null;
10
+ state.language = localStorage.getItem("codex-dashboard-language") || "zh";
11
+ state.titleExpanded = false;
12
+ const LANGUAGE_PACKS = {
13
+ zh: { newSession: "新会话", terminal: "终端", nextSession: "下一个会话", previousSession: "上一个会话", firstSession: "首个会话", searchSessions: "搜索会话", workspace: "工作区", sessions: "会话", memories: "记忆", skills: "技能", all: "全部", running: "进行中", available: "待处理", trash: "回收站", sync: "同步本机 Codex", emptyTitle: "开始一个 Codex 会话", emptyCopy: "从左侧选择已有线程,或新建一个会话开始工作。", create: "新建会话", rename: "重命名", fork: "复制会话", more: "更多操作", send: "发送", start: "开始", pause: "暂停", expand: "展开", collapse: "收起", statusRunning: "运行中", statusRetrying: "重试中", statusQueued: "排队中", statusAvailable: "待处理", statusSucceeded: "已完成", statusFailed: "失败", statusStopped: "已停止", statusArchived: "已归档", statusTrashed: "回收站", statusImported: "已导入", unknown: "未知", composerPlaceholder: "给 Codex 发送消息…" },
14
+ en: { newSession: "New session", terminal: "Terminal", nextSession: "Next session", previousSession: "Previous session", firstSession: "First session", searchSessions: "Search sessions", workspace: "Workspace", sessions: "Sessions", memories: "Memory", skills: "Skills", all: "All", running: "Running", available: "Pending", trash: "Trash", sync: "Sync Codex", emptyTitle: "Start a Codex session", emptyCopy: "Choose a thread on the left, or create a new session.", create: "New session", rename: "Rename", fork: "Duplicate", more: "More actions", send: "Send", start: "Start", pause: "Pause", expand: "Expand", collapse: "Collapse", statusRunning: "Running", statusRetrying: "Retrying", statusQueued: "Queued", statusAvailable: "Pending", statusSucceeded: "Completed", statusFailed: "Failed", statusStopped: "Stopped", statusArchived: "Archived", statusTrashed: "Trash", statusImported: "Imported", unknown: "Unknown", composerPlaceholder: "Message Codex…" },
15
+ fr: { newSession: "Nouvelle session", terminal: "Terminal", workspace: "Espace de travail", sessions: "Sessions", memories: "Mémoire", skills: "Skills", all: "Toutes", running: "En cours", available: "En attente", trash: "Corbeille", sync: "Synchroniser Codex", emptyTitle: "Démarrer une session Codex", emptyCopy: "Choisissez un fil à gauche ou créez une session.", create: "Nouvelle session", rename: "Renommer", fork: "Dupliquer", more: "Plus d'actions", send: "Envoyer", expand: "Développer", collapse: "Réduire", statusRunning: "En cours", statusRetrying: "Nouvel essai", statusQueued: "En file", statusAvailable: "En attente", statusSucceeded: "Terminé", statusFailed: "Échec", statusStopped: "Arrêté", statusArchived: "Archivé", statusTrashed: "Corbeille", statusImported: "Importé", unknown: "Inconnu", composerPlaceholder: "Écrire à Codex…" },
16
+ ja: { newSession: "新しいセッション", terminal: "ターミナル", workspace: "ワークスペース", sessions: "セッション", memories: "メモリ", skills: "Skills", all: "すべて", running: "実行中", available: "保留中", trash: "ゴミ箱", sync: "Codexを同期", emptyTitle: "Codexセッションを開始", emptyCopy: "左からスレッドを選ぶか、新しいセッションを作成してください。", create: "新しいセッション", rename: "名前を変更", fork: "複製", more: "その他", send: "送信", expand: "展開", collapse: "折りたたむ", statusRunning: "実行中", statusRetrying: "再試行中", statusQueued: "待機中", statusAvailable: "保留中", statusSucceeded: "完了", statusFailed: "失敗", statusStopped: "停止", statusArchived: "アーカイブ済み", statusTrashed: "ゴミ箱", statusImported: "インポート済み", unknown: "不明", composerPlaceholder: "Codexにメッセージ…" },
17
+ ko: { newSession: "새 세션", terminal: "터미널", workspace: "작업 공간", sessions: "세션", memories: "메모리", skills: "Skills", all: "전체", running: "실행 중", available: "대기 중", trash: "휴지통", sync: "Codex 동기화", emptyTitle: "Codex 세션 시작", emptyCopy: "왼쪽에서 스레드를 선택하거나 새 세션을 만드세요.", create: "새 세션", rename: "이름 변경", fork: "복제", more: "더 보기", send: "보내기", expand: "펼치기", collapse: "접기", statusRunning: "실행 중", statusRetrying: "재시도 중", statusQueued: "대기열", statusAvailable: "대기 중", statusSucceeded: "완료", statusFailed: "실패", statusStopped: "중지됨", statusArchived: "보관됨", statusTrashed: "휴지통", statusImported: "가져옴", unknown: "알 수 없음", composerPlaceholder: "Codex에 메시지…" },
18
+ };
19
+ const LANGUAGE_EXTRAS = {
20
+ fr: { nextSession: "Session suivante", previousSession: "Session précédente", firstSession: "Première session", searchSessions: "Rechercher des sessions", skills: "Compétences" },
21
+ ja: { nextSession: "次のセッション", previousSession: "前のセッション", firstSession: "最初のセッション", searchSessions: "セッションを検索", skills: "スキル" },
22
+ ko: { nextSession: "다음 세션", previousSession: "이전 세션", firstSession: "첫 세션", searchSessions: "세션 검색", skills: "스킬" },
23
+ };
24
+ function t(key, fallback = key, options = {}) {
25
+ if (window.i18next?.isInitialized) return window.i18next.t(key, { defaultValue: fallback, ...options });
26
+ return LANGUAGE_EXTRAS[state.language]?.[key] || LANGUAGE_PACKS[state.language]?.[key] || LANGUAGE_PACKS.zh[key] || fallback;
27
+ }
28
+ const UI_LABELS = {
29
+ local: { zh: "本机", en: "Local", fr: "Local", ja: "ローカル", ko: "로컬" },
30
+ emptySession: { zh: "空会话", en: "Empty session", fr: "Session vide", ja: "空のセッション", ko: "빈 세션" },
31
+ controlled: { zh: "受控", en: "Controlled", fr: "Contrôlé", ja: "制御", ko: "제어됨" },
32
+ activity: { zh: "工具与文件活动", en: "Tools and files", fr: "Outils et fichiers", ja: "ツールとファイル", ko: "도구 및 파일" },
33
+ sending: { zh: "发送中", en: "Sending", fr: "Envoi", ja: "送信中", ko: "전송 중" },
34
+ steering: { zh: "当前 turn", en: "Current turn", fr: "Tour actuel", ja: "現在のターン", ko: "현재 턴" },
35
+ queued: { zh: "排队中", en: "Queued", fr: "En file", ja: "待機中", ko: "대기 중" },
36
+ failedSend: { zh: "发送失败", en: "Send failed", fr: "Échec de l'envoi", ja: "送信失敗", ko: "전송 실패" },
37
+ firstMessage: { zh: "发送第一条消息,开始这个 Codex 会话。", en: "Send the first message to start this Codex session.", fr: "Envoyez un premier message pour démarrer cette session Codex.", ja: "最初のメッセージを送ってCodexセッションを開始します。", ko: "첫 메시지를 보내 Codex 세션을 시작하세요." },
38
+ wastelandTheme: { zh: "切换废土终端主题", en: "Switch to wasteland terminal theme", fr: "Activer le thème terminal rétro", ja: "ウェイストランド端末テーマに切替", ko: "황무지 터미널 테마로 전환" },
39
+ darkTheme: { zh: "切换默认深色主题", en: "Switch to default dark theme", fr: "Revenir au thème sombre", ja: "標準ダークテーマに戻す", ko: "기본 다크 테마로 전환" },
40
+ wastelandEnabled: { zh: "已进入废土终端模式", en: "Wasteland terminal enabled", fr: "Terminal rétro activé", ja: "ウェイストランド端末を有効化しました", ko: "황무지 터미널 모드 활성화" },
41
+ darkEnabled: { zh: "已恢复默认深色主题", en: "Default dark theme restored", fr: "Thème sombre restauré", ja: "標準ダークテーマに戻しました", ko: "기본 다크 테마 복원" },
42
+ mascotDance: { zh: "Codex Partner 与废土侦察机正在表演未来双人舞", en: "Codex Partner and the wasteland scout are performing a future duet", fr: "Codex Partner et l’éclaireur présentent un duo futuriste", ja: "Codex Partner と偵察ユニットが未来的なデュエットをパフォーマンス中", ko: "Codex Partner와 황무지 정찰 유닛이 미래적인 듀엣 공연 중" },
43
+ loadOlderMessages: { zh: "查看更早消息({{count}})", en: "Load earlier messages ({{count}})", fr: "Charger les messages précédents ({{count}})", ja: "以前のメッセージを表示({{count}})", ko: "이전 메시지 보기 ({{count}})" },
44
+ loadEarlierMessages: { zh: "查看更早消息", en: "Load earlier messages", fr: "Charger les messages précédents", ja: "以前のメッセージを表示", ko: "이전 메시지 보기" },
45
+ localThread: { zh: "本机 Codex 线程", en: "Local Codex thread", fr: "Fil Codex local", ja: "ローカルCodexスレッド", ko: "로컬 Codex 스레드" },
46
+ threadId: { zh: "线程", en: "Thread", fr: "Fil", ja: "スレッド", ko: "스레드" },
47
+ sshSession: { zh: "SSH Codex 会话", en: "SSH Codex session", fr: "Session Codex SSH", ja: "SSH Codexセッション", ko: "SSH Codex 세션" },
48
+ webSession: { zh: "网页会话", en: "Web session", fr: "Session web", ja: "ウェブセッション", ko: "웹 세션" },
49
+ terminalLabel: { zh: "终端", en: "Terminal", fr: "Terminal", ja: "ターミナル", ko: "터미널" },
50
+ commandLabel: { zh: "命令", en: "Command", fr: "Commande", ja: "コマンド", ko: "명령" },
51
+ commandResult: { zh: "命令结果", en: "Command result", fr: "Résultat", ja: "コマンド結果", ko: "명령 결과" },
52
+ model: { zh: "模型", en: "Model", fr: "Modèle", ja: "モデル", ko: "모델" },
53
+ effort: { zh: "思考", en: "Reasoning", fr: "Raisonnement", ja: "思考", ko: "추론" },
54
+ providerDefault: { zh: "Provider 默认", en: "Provider default", fr: "Par défaut", ja: "Provider既定", ko: "Provider 기본" },
55
+ providerDefaultModel: { zh: "Provider 默认模型", en: "Provider default model", fr: "Modèle par défaut", ja: "Provider既定モデル", ko: "Provider 기본 모델" },
56
+ minimal: { zh: "最小", en: "Minimal", fr: "Minimal", ja: "最小", ko: "최소" },
57
+ low: { zh: "低", en: "Low", fr: "Faible", ja: "低", ko: "낮음" },
58
+ medium: { zh: "中", en: "Medium", fr: "Moyen", ja: "中", ko: "중간" },
59
+ high: { zh: "高", en: "High", fr: "Élevé", ja: "高", ko: "높음" },
60
+ xhigh: { zh: "很高", en: "Very high", fr: "Très élevé", ja: "非常に高い", ko: "매우 높음" },
61
+ ultra: { zh: "极高", en: "Ultra", fr: "Très élevé+", ja: "極高", ko: "최고" },
62
+ you: { zh: "用户", en: "User", fr: "Utilisateur", ja: "ユーザー", ko: "사용자" },
63
+ start: { zh: "开始", en: "Start", fr: "Démarrer", ja: "開始", ko: "시작" },
64
+ pause: { zh: "暂停", en: "Pause", fr: "Pause", ja: "一時停止", ko: "일시정지" },
65
+ goalStart: { zh: "开始目标续跑", en: "Start goal resume", fr: "Reprendre l’objectif", ja: "目標の再開", ko: "목표 재개" },
66
+ goalPause: { zh: "暂停目标续跑", en: "Pause goal resume", fr: "Suspendre l’objectif", ja: "目標を一時停止", ko: "목표 일시정지" },
67
+ setGoal: { zh: "先设置目标", en: "Set a goal first", fr: "Définissez un objectif", ja: "目標を設定してください", ko: "목표를 먼저 설정하세요" },
68
+ retryOn: { zh: "开启目标自动续跑", en: "Enable goal auto-resume", fr: "Activer la reprise automatique", ja: "目標の自動再開を有効化", ko: "목표 자동 재개 켜기" },
69
+ retryOff: { zh: "关闭目标自动续跑", en: "Disable goal auto-resume", fr: "Désactiver la reprise automatique", ja: "目標の自動再開を無効化", ko: "목표 자동 재개 끄기" },
70
+ on: { zh: "开", en: "On", fr: "Oui", ja: "オン", ko: "켬" },
71
+ off: { zh: "关", en: "Off", fr: "Non", ja: "オフ", ko: "끔" },
72
+ save: { zh: "保存", en: "Save", fr: "Enregistrer", ja: "保存", ko: "저장" },
73
+ saveQueued: { zh: "保存排队消息(Enter)", en: "Save queued message (Enter)", fr: "Enregistrer le message en attente (Entrée)", ja: "待機メッセージを保存 (Enter)", ko: "대기 메시지 저장 (Enter)" },
74
+ pauseCodex: { zh: "暂停当前 Codex turn(Enter)", en: "Pause the current Codex turn (Enter)", fr: "Mettre en pause le turn Codex (Entrée)", ja: "現在のCodex turnを一時停止 (Enter)", ko: "현재 Codex turn 일시정지 (Enter)" },
75
+ startCodex: { zh: "开始 Codex(Enter)", en: "Start Codex (Enter)", fr: "Démarrer Codex (Entrée)", ja: "Codexを開始 (Enter)", ko: "Codex 시작 (Enter)" },
76
+ realtime: { zh: "实时连接", en: "Live", fr: "Temps réel", ja: "リアルタイム", ko: "실시간" },
77
+ disconnected: { zh: "连接中断", en: "Disconnected", fr: "Déconnecté", ja: "切断", ko: "연결 끊김" },
78
+ reconnecting: { zh: "重连中", en: "Reconnecting", fr: "Reconnexion", ja: "再接続中", ko: "재연결 중" },
79
+ connecting: { zh: "连接中", en: "Connecting", fr: "Connexion", ja: "接続中", ko: "연결 중" },
80
+ currentApi: { zh: "当前 API", en: "Current API", fr: "API actuelle", ja: "現在の API", ko: "현재 API" },
81
+ automatic: { zh: "自动选择", en: "Auto", fr: "Auto", ja: "自動選択", ko: "자동 선택" },
82
+ estimated: { zh: "估算", en: "estimated", fr: "estimé", ja: "推定", ko: "추정" },
83
+ session: { zh: "会话", en: "Session", fr: "Session", ja: "セッション", ko: "세션" },
84
+ state: { zh: "状态", en: "Status", fr: "État", ja: "状態", ko: "상태" },
85
+ thread: { zh: "线程", en: "Thread", fr: "Thread", ja: "スレッド", ko: "스레드" },
86
+ sshHost: { zh: "SSH 主机", en: "SSH host", fr: "Hôte SSH", ja: "SSHホスト", ko: "SSH 호스트" },
87
+ workspacePath: { zh: "工作目录", en: "Workspace", fr: "Espace de travail", ja: "ワークスペース", ko: "작업 공간" },
88
+ goal: { zh: "目标", en: "Goal", fr: "Objectif", ja: "目標", ko: "목표" },
89
+ noGoal: { zh: "没有设置目标", en: "No goal set", fr: "Aucun objectif défini", ja: "目標未設定", ko: "설정된 목표 없음" },
90
+ progress: { zh: "进度", en: "Progress", fr: "Progression", ja: "進捗", ko: "진행률" },
91
+ runHistory: { zh: "运行记录", en: "Run history", fr: "Historique", ja: "実行履歴", ko: "실행 기록" },
92
+ attempt: { zh: "尝试", en: "Attempt", fr: "Tentative", ja: "試行", ko: "시도" },
93
+ autoProvider: { zh: "自动 Provider", en: "Auto Provider", fr: "Provider auto", ja: "自動 Provider", ko: "자동 Provider" },
94
+ noRunHistory: { zh: "暂无运行记录", en: "No runs yet", fr: "Aucune exécution", ja: "実行履歴なし", ko: "실행 기록 없음" },
95
+ threadControls: { zh: "线程控制", en: "Thread controls", fr: "Contrôles du thread", ja: "スレッド操作", ko: "스레드 제어" },
96
+ copySession: { zh: "复制会话", en: "Duplicate session", fr: "Dupliquer", ja: "セッションを複製", ko: "세션 복제" },
97
+ compactContext: { zh: "压缩上下文", en: "Compact context", fr: "Compacter le contexte", ja: "コンテキストを圧縮", ko: "컨텍스트 압축" },
98
+ archive: { zh: "归档", en: "Archive", fr: "Archiver", ja: "アーカイブ", ko: "보관" },
99
+ unarchive: { zh: "取消归档", en: "Unarchive", fr: "Désarchiver", ja: "アーカイブ解除", ko: "보관 해제" },
100
+ moveTrash: { zh: "移到回收站", en: "Move to trash", fr: "Mettre à la corbeille", ja: "ゴミ箱へ移動", ko: "휴지통으로 이동" },
101
+ enableMemory: { zh: "打开记忆", en: "Enable memory", fr: "Activer la mémoire", ja: "メモリを有効化", ko: "메모리 켜기" },
102
+ disableMemory: { zh: "关闭记忆", en: "Disable memory", fr: "Désactiver la mémoire", ja: "メモリを無効化", ko: "메모리 끄기" },
103
+ workspaceFiles: { zh: "工作区文件", en: "Workspace files", fr: "Fichiers de l’espace", ja: "ワークスペースのファイル", ko: "작업 공간 파일" },
104
+ workspaceRoot: { zh: "工作区根目录", en: "Workspace root", fr: "Racine de l'espace", ja: "ワークスペースのルート", ko: "작업 공간 루트" },
105
+ chooseWorkspace: { zh: "选择工作目录", en: "Choose workspace", fr: "Choisir l'espace", ja: "ワークスペースを選択", ko: "작업 공간 선택" },
106
+ back: { zh: "返回上级", en: "Back", fr: "Retour", ja: "上へ戻る", ko: "상위로" },
107
+ changeDirectory: { zh: "更改目录", en: "Change directory", fr: "Changer de dossier", ja: "ディレクトリを変更", ko: "디렉터리 변경" },
108
+ upload: { zh: "上传", en: "Upload", fr: "Téléverser", ja: "アップロード", ko: "업로드" },
109
+ edit: { zh: "编辑", en: "Edit", fr: "Modifier", ja: "編集", ko: "편집" },
110
+ download: { zh: "下载", en: "Download", fr: "Télécharger", ja: "ダウンロード", ko: "다운로드" },
111
+ emptyDirectory: { zh: "目录为空,可将文件拖到这里上传。", en: "Directory is empty. Drop files here to upload.", fr: "Dossier vide. Déposez des fichiers pour téléverser.", ja: "空のディレクトリです。ファイルをドロップしてアップロードできます。", ko: "디렉터리가 비어 있습니다. 파일을 놓아 업로드하세요." },
112
+ language: { zh: "界面语言", en: "Interface language", fr: "Langue", ja: "表示言語", ko: "인터페이스 언어" },
113
+ clear: { zh: "清空", en: "Clear", fr: "Effacer", ja: "クリア", ko: "지우기" },
114
+ cancel: { zh: "取消", en: "Cancel", fr: "Annuler", ja: "キャンセル", ko: "취소" },
115
+ save: { zh: "保存", en: "Save", fr: "Enregistrer", ja: "保存", ko: "저장" },
116
+ editGoal: { zh: "修改目标", en: "Edit goal", fr: "Modifier l’objectif", ja: "目標を編集", ko: "목표 수정" },
117
+ goalInput: { zh: "输入要持续完成的目标", en: "Enter the goal to pursue", fr: "Saisissez l’objectif", ja: "継続する目標を入力", ko: "추적할 목표 입력" },
118
+ ready: { zh: "待命", en: "Ready", fr: "Prêt", ja: "待機中", ko: "대기" },
119
+ shortcutTerminal: { zh: "单按反引号键 (`) 显示/隐藏终端", en: "Press the backquote key (`) to toggle terminal", fr: "Touche accent grave (`) : afficher/masquer le terminal", ja: "バッククォート (`) でターミナル表示切替", ko: "백쿼트 (`) 키로 터미널 표시 전환" },
120
+ shortcutNext: { zh: "下个会话", en: "Next", fr: "Suivante", ja: "次へ", ko: "다음" },
121
+ shortcutPrevious: { zh: "上个会话", en: "Previous", fr: "Précédente", ja: "前へ", ko: "이전" },
122
+ shortcutFirst: { zh: "首个会话", en: "First", fr: "Première", ja: "先頭", ko: "처음" },
123
+ stopAction: { zh: "停止", en: "Stop", fr: "Arrêter", ja: "停止", ko: "중지" },
124
+ editQueued: { zh: "修改排队中的消息…", en: "Edit queued message…", fr: "Modifier le message en attente…", ja: "待機メッセージを編集…", ko: "대기 메시지 편집…" },
125
+ readingDirectory: { zh: "正在读取目录…", en: "Reading directory…", fr: "Lecture du dossier…", ja: "ディレクトリを読み込み中…", ko: "디렉터리 읽는 중…" },
126
+ open: { zh: "打开", en: "Open", fr: "Ouvrir", ja: "開く", ko: "열기" },
127
+ noSubdirectories: { zh: "没有子目录", en: "No subdirectories", fr: "Aucun sous-dossier", ja: "サブディレクトリなし", ko: "하위 디렉터리 없음" },
128
+ go: { zh: "前往", en: "Go", fr: "Aller", ja: "移動", ko: "이동" },
129
+ selectCurrentDirectory: { zh: "选择当前目录", en: "Select current directory", fr: "Sélectionner ce dossier", ja: "現在のディレクトリを選択", ko: "현재 디렉터리 선택" },
130
+ serverDirectory: { zh: "服务器目录", en: "Server directory", fr: "Dossier serveur", ja: "サーバーディレクトリ", ko: "서버 디렉터리" },
131
+ attachments: { zh: "附件", en: "Attachments", fr: "Pièces jointes", ja: "添付ファイル", ko: "첨부 파일" },
132
+ filesReady: { zh: "{{count}} 个文件已加入下一条消息", en: "{{count}} files added to the next message", fr: "{{count}} fichiers ajoutés au prochain message", ja: "{{count}}個のファイルを次のメッセージに追加", ko: "{{count}}개 파일을 다음 메시지에 추가" },
133
+ fileReadFailed: { zh: "文件读取失败:{{name}}", en: "Could not read file: {{name}}", fr: "Lecture impossible : {{name}}", ja: "ファイルを読み込めません:{{name}}", ko: "파일을 읽을 수 없음: {{name}}" },
134
+ fileUploadFailed: { zh: "文件上传失败:{{name}}", en: "Could not upload file: {{name}}", fr: "Envoi impossible : {{name}}", ja: "ファイルをアップロードできません:{{name}}", ko: "파일을 업로드할 수 없음: {{name}}" },
135
+ binaryAttachment: { zh: "已上传到工作区:{{path}}", en: "Uploaded to workspace: {{path}}", fr: "Téléversé dans l’espace : {{path}}", ja: "ワークスペースにアップロード済み:{{path}}", ko: "작업 공간에 업로드됨: {{path}}" },
136
+ delete: { zh: "删除", en: "Delete", fr: "Supprimer", ja: "削除", ko: "삭제" },
137
+ codexMemory: { zh: "Codex 记忆", en: "Codex memory", fr: "Mémoire Codex", ja: "Codex メモリ", ko: "Codex 메모리" },
138
+ memorySummary: { zh: "{{markdown}} 个 Markdown 记忆 · {{generated}} 个生成记忆", en: "{{markdown}} Markdown memories · {{generated}} generated memories", fr: "{{markdown}} mémoires Markdown · {{generated}} mémoires générées", ja: "Markdown メモリ {{markdown}} 件 · 生成メモリ {{generated}} 件", ko: "Markdown 메모리 {{markdown}}개 · 생성된 메모리 {{generated}}개" },
139
+ searchMemory: { zh: "搜索记忆", en: "Search memory", fr: "Rechercher dans la mémoire", ja: "メモリを検索", ko: "메모리 검색" },
140
+ newItem: { zh: "新建", en: "New", fr: "Nouveau", ja: "新規", ko: "새로 만들기" },
141
+ editable: { zh: "可编辑", en: "Editable", fr: "Modifiable", ja: "編集可能", ko: "편집 가능" },
142
+ readOnly: { zh: "只读", en: "Read only", fr: "Lecture seule", ja: "読み取り専用", ko: "읽기 전용" },
143
+ view: { zh: "查看", en: "View", fr: "Afficher", ja: "表示", ko: "보기" },
144
+ generated: { zh: "生成记忆", en: "Generated", fr: "Générée", ja: "生成メモリ", ko: "생성된 메모리" },
145
+ memoryUsage: { zh: "使用 {{count}} 次", en: "Used {{count}} times", fr: "Utilisée {{count}} fois", ja: "{{count}} 回使用", ko: "{{count}}회 사용" },
146
+ noMarkdownMemories: { zh: "没有匹配的 Markdown 记忆", en: "No matching Markdown memories", fr: "Aucune mémoire Markdown correspondante", ja: "一致する Markdown メモリはありません", ko: "일치하는 Markdown 메모리가 없습니다" },
147
+ noGeneratedMemories: { zh: "暂无生成记忆", en: "No generated memories", fr: "Aucune mémoire générée", ja: "生成メモリはありません", ko: "생성된 메모리가 없습니다" },
148
+ rebuildGeneratedMemories: { zh: "重建生成记忆", en: "Rebuild generated memory", fr: "Reconstruire la mémoire générée", ja: "生成メモリを再構築", ko: "생성된 메모리 다시 만들기" },
149
+ installedSkillsSummary: { zh: "已检测 {{count}} 个已安装 Skill,Codex 与 Agent Skill 均可直接编辑。", en: "{{count}} installed skills detected. Codex and Agent skills can be edited directly.", fr: "{{count}} skills installés détectés. Les skills Codex et Agent sont modifiables directement.", ja: "インストール済み Skill を {{count}} 件検出しました。Codex と Agent の Skill を直接編集できます。", ko: "설치된 Skill {{count}}개를 찾았습니다. Codex와 Agent Skill을 직접 편집할 수 있습니다." },
150
+ newPersonalSkill: { zh: "新建个人 Skill", en: "New personal skill", fr: "Nouveau skill personnel", ja: "個人 Skill を作成", ko: "개인 Skill 만들기" },
151
+ enabled: { zh: "已启用", en: "Enabled", fr: "Activé", ja: "有効", ko: "활성화됨" },
152
+ disabled: { zh: "已停用", en: "Disabled", fr: "Désactivé", ja: "無効", ko: "비활성화됨" },
153
+ enable: { zh: "启用", en: "Enable", fr: "Activer", ja: "有効にする", ko: "활성화" },
154
+ noSkills: { zh: "未检测到 Skill", en: "No skills detected", fr: "Aucun skill détecté", ja: "Skill が見つかりません", ko: "Skill을 찾지 못했습니다" },
155
+ editMemory: { zh: "编辑记忆", en: "Edit memory", fr: "Modifier la mémoire", ja: "メモリを編集", ko: "메모리 편집" },
156
+ newMemory: { zh: "新建记忆", en: "New memory", fr: "Nouvelle mémoire", ja: "新しいメモリ", ko: "새 메모리" },
157
+ createMarkdownMemory: { zh: "创建 Codex Markdown 记忆", en: "Create a Codex Markdown memory", fr: "Créer une mémoire Codex Markdown", ja: "Codex Markdown メモリを作成", ko: "Codex Markdown 메모리 만들기" },
158
+ reading: { zh: "正在读取…", en: "Loading…", fr: "Chargement…", ja: "読み込み中…", ko: "불러오는 중…" },
159
+ fileName: { zh: "文件名", en: "File name", fr: "Nom du fichier", ja: "ファイル名", ko: "파일 이름" },
160
+ content: { zh: "内容", en: "Content", fr: "Contenu", ja: "内容", ko: "내용" },
161
+ saving: { zh: "保存中…", en: "Saving…", fr: "Enregistrement…", ja: "保存中…", ko: "저장 중…" },
162
+ memorySaved: { zh: "记忆已保存", en: "Memory saved", fr: "Mémoire enregistrée", ja: "メモリを保存しました", ko: "메모리가 저장되었습니다" },
163
+ saveFailed: { zh: "保存失败:{{message}}", en: "Save failed: {{message}}", fr: "Échec de l’enregistrement : {{message}}", ja: "保存に失敗しました:{{message}}", ko: "저장 실패: {{message}}" },
164
+ close: { zh: "关闭", en: "Close", fr: "Fermer", ja: "閉じる", ko: "닫기" },
165
+ viewSkill: { zh: "查看 Skill", en: "View skill", fr: "Afficher le skill", ja: "Skill を表示", ko: "Skill 보기" },
166
+ editSkill: { zh: "编辑 Skill", en: "Edit skill", fr: "Modifier le skill", ja: "Skill を編集", ko: "Skill 편집" },
167
+ name: { zh: "名称", en: "Name", fr: "Nom", ja: "名前", ko: "이름" },
168
+ description: { zh: "描述", en: "Description", fr: "Description", ja: "説明", ko: "설명" },
169
+ skillSaved: { zh: "Skill 已保存", en: "Skill saved", fr: "Skill enregistré", ja: "Skill を保存しました", ko: "Skill이 저장되었습니다" },
170
+ editFile: { zh: "编辑文件", en: "Edit file", fr: "Modifier le fichier", ja: "ファイルを編集", ko: "파일 편집" },
171
+ fileSaved: { zh: "文件已保存", en: "File saved", fr: "Fichier enregistré", ja: "ファイルを保存しました", ko: "파일이 저장되었습니다" },
172
+ unsupportedFileEdit: { zh: "该文件不支持网页编辑", en: "This file cannot be edited in the browser", fr: "Ce fichier ne peut pas être modifié dans le navigateur", ja: "このファイルはブラウザーで編集できません", ko: "이 파일은 브라우저에서 편집할 수 없습니다" },
173
+ editorUnavailable: { zh: "文本编辑器未加载,请刷新页面重试", en: "The text editor did not load. Refresh and try again.", fr: "L’éditeur de texte n’est pas chargé. Actualisez la page.", ja: "テキストエディターを読み込めませんでした。ページを更新してください。", ko: "텍스트 편집기를 불러오지 못했습니다. 페이지를 새로 고치세요." },
174
+ connectionAndProviders: { zh: "连接与 Provider", en: "Connection and providers", fr: "Connexion et providers", ja: "接続と Provider", ko: "연결 및 Provider" },
175
+ connectionDescription: { zh: "当前网页服务、实时指标和 Provider 路由集中管理。", en: "Manage this web service, live metrics, and provider routing in one place.", fr: "Gérez le service web, les mesures en direct et le routage des providers.", ja: "Web サービス、リアルタイム指標、Provider ルーティングを一元管理します。", ko: "웹 서비스, 실시간 지표 및 Provider 라우팅을 한곳에서 관리합니다." },
176
+ webService: { zh: "网页服务", en: "Web service", fr: "Service web", ja: "Web サービス", ko: "웹 서비스" },
177
+ serviceUser: { zh: "服务用户", en: "Service user", fr: "Utilisateur du service", ja: "サービスユーザー", ko: "서비스 사용자" },
178
+ liveConnected: { zh: "实时已连接", en: "Live connected", fr: "Temps réel connecté", ja: "リアルタイム接続済み", ko: "실시간 연결됨" },
179
+ currentProvider: { zh: "当前 Provider", en: "Current provider", fr: "Provider actuel", ja: "現在の Provider", ko: "현재 Provider" },
180
+ usableRoutes: { zh: "可用路由", en: "Available routes", fr: "Routes disponibles", ja: "利用可能なルート", ko: "사용 가능한 경로" },
181
+ codexReady: { zh: "Codex 已就绪", en: "Codex ready", fr: "Codex prêt", ja: "Codex 準備完了", ko: "Codex 준비됨" },
182
+ codexMissing: { zh: "Codex 未安装", en: "Codex not installed", fr: "Codex non installé", ja: "Codex 未インストール", ko: "Codex 설치되지 않음" },
183
+ codexManagement: { zh: "Codex 管理", en: "Codex management", fr: "Gestion de Codex", ja: "Codex 管理", ko: "Codex 관리" },
184
+ codexVersion: { zh: "版本", en: "Version", fr: "Version", ja: "バージョン", ko: "버전" },
185
+ codexPath: { zh: "Codex 路径", en: "Codex path", fr: "Chemin Codex", ja: "Codex パス", ko: "Codex 경로" },
186
+ codexWorkingDirectory: { zh: "当前路径", en: "Current path", fr: "Chemin actuel", ja: "現在のパス", ko: "현재 경로" },
187
+ codexSource: { zh: "来源", en: "Source", fr: "Source", ja: "ソース", ko: "소스" },
188
+ updateCodex: { zh: "更新 Codex", en: "Update Codex", fr: "Mettre Codex à jour", ja: "Codex を更新", ko: "Codex 업데이트" },
189
+ updatingCodex: { zh: "更新中…", en: "Updating…", fr: "Mise à jour…", ja: "更新中…", ko: "업데이트 중…" },
190
+ codexVersionUnknown: { zh: "版本未知", en: "Version unavailable", fr: "Version indisponible", ja: "バージョン不明", ko: "버전 확인 불가" },
191
+ runningTasks: { zh: "{{count}} 个任务运行中", en: "{{count}} tasks running", fr: "{{count}} tâches en cours", ja: "{{count}} 件のタスクを実行中", ko: "{{count}}개 작업 실행 중" },
192
+ workspaceUnset: { zh: "未设置工作目录", en: "Workspace not set", fr: "Espace de travail non défini", ja: "ワークスペース未設定", ko: "작업 공간이 설정되지 않음" },
193
+ providerRoutingDescription: { zh: "按优先级自动切换,API Key 仅显示保存状态。", en: "Automatically fail over by priority. API keys only show their saved state.", fr: "Basculement automatique par priorité. Seul l’état enregistré des clés API est affiché.", ja: "優先度順に自動切り替えします。API Key は保存状態のみ表示します。", ko: "우선순위에 따라 자동 전환합니다. API Key는 저장 상태만 표시합니다." },
194
+ sync: { zh: "同步", en: "Sync", fr: "Synchroniser", ja: "同期", ko: "동기화" },
195
+ checkAll: { zh: "全部检查", en: "Check all", fr: "Tout vérifier", ja: "すべて確認", ko: "모두 확인" },
196
+ add: { zh: "新增", en: "Add", fr: "Ajouter", ja: "追加", ko: "추가" },
197
+ noProviders: { zh: "还没有 Provider,点击“新增”配置第一个路由", en: "No providers yet. Select Add to configure the first route.", fr: "Aucun provider. Sélectionnez Ajouter pour configurer la première route.", ja: "Provider がありません。「追加」から最初のルートを設定してください。", ko: "Provider가 없습니다. 추가를 눌러 첫 경로를 설정하세요." },
198
+ current: { zh: "当前", en: "Current", fr: "Actuel", ja: "現在", ko: "현재" },
199
+ unavailable: { zh: "不可用", en: "Unavailable", fr: "Indisponible", ja: "利用不可", ko: "사용 불가" },
200
+ use: { zh: "使用", en: "Use", fr: "Utiliser", ja: "使用", ko: "사용" },
201
+ default: { zh: "默认", en: "Default", fr: "Par défaut", ja: "既定", ko: "기본" },
202
+ priority: { zh: "优先级 {{value}}", en: "Priority {{value}}", fr: "Priorité {{value}}", ja: "優先度 {{value}}", ko: "우선순위 {{value}}" },
203
+ excludedFromFailover: { zh: "不参与切换", en: "Excluded from failover", fr: "Exclu du basculement", ja: "切り替え対象外", ko: "전환에서 제외" },
204
+ apiKeyUnset: { zh: "未设置 API Key", en: "API key not set", fr: "Clé API non définie", ja: "API Key 未設定", ko: "API Key 미설정" },
205
+ latency: { zh: "延迟 {{value}}", en: "Latency {{value}}", fr: "Latence {{value}}", ja: "遅延 {{value}}", ko: "지연 {{value}}" },
206
+ successFailure: { zh: "成功 {{success}} / 失败 {{failure}}", en: "{{success}} succeeded / {{failure}} failed", fr: "{{success}} réussites / {{failure}} échecs", ja: "成功 {{success}} / 失敗 {{failure}}", ko: "성공 {{success}} / 실패 {{failure}}" },
207
+ check: { zh: "检查", en: "Check", fr: "Vérifier", ja: "確認", ko: "확인" },
208
+ codexDefaultAddress: { zh: "Codex 默认地址", en: "Codex default URL", fr: "URL Codex par défaut", ja: "Codex 既定 URL", ko: "Codex 기본 URL" },
209
+ recycleBinDescription: { zh: "删除会话会先移动到这里;只有在此处永久删除才会销毁 Codex 线程。", en: "Deleted sessions move here first. Only permanent deletion destroys a Codex thread.", fr: "Les sessions supprimées arrivent d’abord ici. Seule la suppression définitive détruit un fil Codex.", ja: "削除したセッションはまずここへ移動します。完全削除した場合のみ Codex スレッドが破棄されます。", ko: "삭제된 세션은 먼저 여기로 이동합니다. 영구 삭제할 때만 Codex 스레드가 제거됩니다." },
210
+ readingTrash: { zh: "正在读取回收站…", en: "Loading trash…", fr: "Chargement de la corbeille…", ja: "ゴミ箱を読み込み中…", ko: "휴지통 불러오는 중…" },
211
+ deleted: { zh: "已删除", en: "Deleted", fr: "Supprimé", ja: "削除済み", ko: "삭제됨" },
212
+ restore: { zh: "恢复", en: "Restore", fr: "Restaurer", ja: "復元", ko: "복원" },
213
+ permanentDelete: { zh: "永久删除", en: "Delete permanently", fr: "Supprimer définitivement", ja: "完全に削除", ko: "영구 삭제" },
214
+ emptyTrash: { zh: "回收站为空", en: "Trash is empty", fr: "La corbeille est vide", ja: "ゴミ箱は空です", ko: "휴지통이 비어 있습니다" },
215
+ serverConnection: { zh: "服务器连接", en: "Server connection", fr: "Connexion serveur", ja: "サーバー接続", ko: "서버 연결" },
216
+ sshConnectionDescription: { zh: "打开网址后自动尝试免密 SSH;只有认证失败时才填写一次 SSH 用户名和密码。", en: "Passwordless SSH is attempted automatically. Credentials are requested only if authentication fails.", fr: "SSH sans mot de passe est tenté automatiquement. Les identifiants ne sont demandés qu’en cas d’échec.", ja: "パスワードなし SSH を自動で試行し、認証に失敗した場合のみ資格情報を求めます。", ko: "비밀번호 없는 SSH를 자동으로 시도하며 인증 실패 시에만 자격 증명을 요청합니다." },
217
+ healthHealthy: { zh: "正常", en: "Healthy", fr: "Opérationnel", ja: "正常", ko: "정상" },
218
+ healthReachable: { zh: "可达", en: "Reachable", fr: "Accessible", ja: "到達可能", ko: "연결 가능" },
219
+ healthConfigured: { zh: "已配置", en: "Configured", fr: "Configuré", ja: "設定済み", ko: "설정됨" },
220
+ healthNeedsKey: { zh: "缺少密钥", en: "Key required", fr: "Clé requise", ja: "キーが必要", ko: "키 필요" },
221
+ healthAuthError: { zh: "鉴权失败", en: "Authentication failed", fr: "Échec d’authentification", ja: "認証失敗", ko: "인증 실패" },
222
+ healthDegraded: { zh: "服务异常", en: "Degraded", fr: "Dégradé", ja: "サービス異常", ko: "서비스 저하" },
223
+ healthUnreachable: { zh: "不可达", en: "Unreachable", fr: "Inaccessible", ja: "到達不可", ko: "연결 불가" },
224
+ healthInvalid: { zh: "配置错误", en: "Invalid configuration", fr: "Configuration incorrecte", ja: "設定エラー", ko: "설정 오류" },
225
+ healthError: { zh: "运行失败", en: "Runtime failure", fr: "Échec d’exécution", ja: "実行失敗", ko: "실행 실패" },
226
+ healthUnchecked: { zh: "未检查", en: "Not checked", fr: "Non vérifié", ja: "未確認", ko: "확인되지 않음" },
227
+ readyStatus: { zh: "已就绪", en: "Ready", fr: "Prêt", ja: "準備完了", ko: "준비됨" },
228
+ useLocal: { zh: "使用本机", en: "Use local", fr: "Utiliser en local", ja: "ローカルを使用", ko: "로컬 사용" },
229
+ noFixedServer: { zh: "还没有固定服务器,请输入地址连接", en: "No fixed server yet. Enter an address to connect.", fr: "Aucun serveur fixe. Saisissez une adresse pour vous connecter.", ja: "固定サーバーがありません。アドレスを入力して接続してください。", ko: "고정 서버가 없습니다. 연결할 주소를 입력하세요." },
230
+ serverAddress: { zh: "服务器地址", en: "Server address", fr: "Adresse du serveur", ja: "サーバーアドレス", ko: "서버 주소" },
231
+ sshUsername: { zh: "SSH 用户名", en: "SSH username", fr: "Nom d’utilisateur SSH", ja: "SSH ユーザー名", ko: "SSH 사용자 이름" },
232
+ port: { zh: "端口", en: "Port", fr: "Port", ja: "ポート", ko: "포트" },
233
+ passwordOrPassphrase: { zh: "密码或私钥口令", en: "Password or key passphrase", fr: "Mot de passe ou phrase secrète", ja: "パスワードまたは秘密鍵パスフレーズ", ko: "비밀번호 또는 개인 키 암호" },
234
+ connectServer: { zh: "连接服务器", en: "Connect server", fr: "Connecter le serveur", ja: "サーバーに接続", ko: "서버 연결" },
235
+ defaultUser: { zh: "默认用户", en: "Default user", fr: "Utilisateur par défaut", ja: "既定ユーザー", ko: "기본 사용자" },
236
+ installCodex: { zh: "安装 Codex", en: "Install Codex", fr: "Installer Codex", ja: "Codex をインストール", ko: "Codex 설치" },
237
+ disconnect: { zh: "断开", en: "Disconnect", fr: "Déconnecter", ja: "切断", ko: "연결 해제" },
238
+ login: { zh: "登录", en: "Log in", fr: "Se connecter", ja: "ログイン", ko: "로그인" },
239
+ retry: { zh: "重试", en: "Retry", fr: "Réessayer", ja: "再試行", ko: "다시 시도" },
240
+ connected: { zh: "已连接", en: "Connected", fr: "Connecté", ja: "接続済み", ko: "연결됨" },
241
+ codexUnavailable: { zh: "缺少 Codex", en: "Codex unavailable", fr: "Codex indisponible", ja: "Codex なし", ko: "Codex 없음" },
242
+ needsPassword: { zh: "需要密码", en: "Password required", fr: "Mot de passe requis", ja: "パスワードが必要", ko: "비밀번호 필요" },
243
+ connectionFailed: { zh: "连接失败", en: "Connection failed", fr: "Échec de connexion", ja: "接続失敗", ko: "연결 실패" },
244
+ notConnected: { zh: "未连接", en: "Not connected", fr: "Non connecté", ja: "未接続", ko: "연결되지 않음" },
245
+ summary: { zh: "摘要", en: "Summary", fr: "Résumé", ja: "要約", ko: "요약" },
246
+ rawMemory: { zh: "原始记忆", en: "Raw memory", fr: "Mémoire brute", ja: "元のメモリ", ko: "원본 메모리" },
247
+ undo: { zh: "撤销", en: "Undo", fr: "Annuler", ja: "元に戻す", ko: "실행 취소" },
248
+ redo: { zh: "重做", en: "Redo", fr: "Rétablir", ja: "やり直す", ko: "다시 실행" },
249
+ bold: { zh: "粗体", en: "Bold", fr: "Gras", ja: "太字", ko: "굵게" },
250
+ italic: { zh: "斜体", en: "Italic", fr: "Italique", ja: "斜体", ko: "기울임꼴" },
251
+ heading: { zh: "标题", en: "Heading", fr: "Titre", ja: "見出し", ko: "제목" },
252
+ link: { zh: "链接", en: "Link", fr: "Lien", ja: "リンク", ko: "링크" },
253
+ inlineCode: { zh: "行内代码", en: "Inline code", fr: "Code en ligne", ja: "インラインコード", ko: "인라인 코드" },
254
+ bulletList: { zh: "项目列表", en: "Bullet list", fr: "Liste à puces", ja: "箇条書き", ko: "글머리 기호 목록" },
255
+ wrapLines: { zh: "自动换行", en: "Wrap lines", fr: "Retour à la ligne", ja: "行を折り返す", ko: "자동 줄 바꿈" },
256
+ editView: { zh: "编辑", en: "Edit", fr: "Modifier", ja: "編集", ko: "편집" },
257
+ splitView: { zh: "分栏", en: "Split", fr: "Partagé", ja: "分割", ko: "분할" },
258
+ preview: { zh: "预览", en: "Preview", fr: "Aperçu", ja: "プレビュー", ko: "미리 보기" },
259
+ lineColumn: { zh: "第 {{line}} 行,第 {{column}} 列", en: "Ln {{line}}, Col {{column}}", fr: "Lig {{line}}, Col {{column}}", ja: "{{line}} 行 {{column}} 列", ko: "{{line}}행 {{column}}열" },
260
+ memoryEditorPlaceholder: { zh: "用 Markdown 记录项目约定、重要结论或长期上下文…", en: "Record project conventions, important conclusions, or long-term context in Markdown…", fr: "Notez les conventions, conclusions importantes ou le contexte durable en Markdown…", ja: "プロジェクトの規約、重要な結論、長期的な文脈を Markdown で記録…", ko: "프로젝트 규칙, 중요한 결론 또는 장기 컨텍스트를 Markdown으로 기록하세요…" },
261
+ skillTemplate: { zh: "# 使用说明\n\n说明 Codex 应在什么情况下使用这个 Skill,以及必须遵循的步骤。\n", en: "# Instructions\n\nDescribe when Codex should use this skill and the steps it must follow.\n", fr: "# Instructions\n\nDécrivez quand Codex doit utiliser ce skill et les étapes à suivre.\n", ja: "# 使用方法\n\nCodex がこの Skill を使用する条件と従うべき手順を記述します。\n", ko: "# 사용 방법\n\nCodex가 이 Skill을 사용해야 하는 상황과 따라야 할 단계를 설명하세요.\n" },
262
+ codexPartner: { zh: "Codex Partner", en: "Codex Partner", fr: "Codex Partner", ja: "Codex Partner", ko: "Codex Partner" },
263
+ messageFromTerminal: { zh: "来自终端", en: "From terminal", fr: "Depuis le terminal", ja: "ターミナルから", ko: "터미널에서" },
264
+ phaseGenerating: { zh: "正在组织回复", en: "Composing the response", fr: "Rédaction de la réponse", ja: "回答を作成中", ko: "답변 작성 중" },
265
+ phasePlanning: { zh: "正在分析并规划下一步", en: "Analyzing and planning the next step", fr: "Analyse et planification de la suite", ja: "分析して次の手順を計画中", ko: "분석하고 다음 단계 계획 중" },
266
+ phaseCommand: { zh: "正在运行命令并读取结果", en: "Running a command and reading the result", fr: "Exécution d’une commande et lecture du résultat", ja: "コマンドを実行して結果を確認中", ko: "명령을 실행하고 결과 확인 중" },
267
+ phaseFiles: { zh: "正在整理工作区文件", en: "Working through workspace files", fr: "Traitement des fichiers de l’espace", ja: "ワークスペースのファイルを処理中", ko: "작업 공간 파일 처리 중" },
268
+ phaseTools: { zh: "正在调用工具完成这一步", en: "Using a tool for this step", fr: "Utilisation d’un outil pour cette étape", ja: "この手順でツールを使用中", ko: "이 단계에 도구 사용 중" },
269
+ phaseSearch: { zh: "正在检索需要的资料", en: "Looking up the needed information", fr: "Recherche des informations nécessaires", ja: "必要な情報を検索中", ko: "필요한 정보 검색 중" },
270
+ phaseCompact: { zh: "正在压缩上下文,保留关键线索", en: "Compacting context while keeping key details", fr: "Compression du contexte en conservant l’essentiel", ja: "重要な情報を残してコンテキストを圧縮中", ko: "핵심 정보를 유지하며 컨텍스트 압축 중" },
271
+ phaseProvider: { zh: "正在切换可用的 Provider", en: "Switching to an available provider", fr: "Basculement vers un provider disponible", ja: "利用可能な Provider に切り替え中", ko: "사용 가능한 Provider로 전환 중" },
272
+ phaseTerminalSynced: { zh: "已接入同一任务的终端进度", en: "Following the same task from the terminal", fr: "Suivi de la même tâche depuis le terminal", ja: "同じタスクのターミナル進捗に接続済み", ko: "같은 작업의 터미널 진행 상황 연결됨" },
273
+ phaseInstruction: { zh: "已收到新的指令", en: "New instruction received", fr: "Nouvelle instruction reçue", ja: "新しい指示を受信", ko: "새 지시 수신됨" },
274
+ phaseInteraction: { zh: "正在处理 Codex 交互请求", en: "Handling a Codex interaction request", fr: "Traitement d’une requête interactive Codex", ja: "Codex の対話リクエストを処理中", ko: "Codex 상호작용 요청 처리 중" },
275
+ phaseQueued: { zh: "已排队,等待服务端接管", en: "Queued and waiting for the service", fr: "En file d’attente du service", ja: "キューでサービスの開始待ち", ko: "대기열에서 서비스 시작 대기 중" },
276
+ phaseAnalyzing: { zh: "正在读懂任务和上下文", en: "Reading the task and its context", fr: "Lecture de la tâche et de son contexte", ja: "タスクとコンテキストを確認中", ko: "작업과 컨텍스트 파악 중" },
277
+ phaseRetry: { zh: "正在重新连接并继续当前 Goal", en: "Reconnecting and continuing the current goal", fr: "Reconnexion et poursuite de l’objectif actuel", ja: "再接続して現在の Goal を継続中", ko: "다시 연결하여 현재 Goal 계속 진행 중" },
278
+ workingTitle: { zh: "Codex 正在工作", en: "Codex is working", fr: "Codex travaille", ja: "Codex が作業中", ko: "Codex 작업 중" },
279
+ recoveringTitle: { zh: "Codex 正在恢复", en: "Codex is recovering", fr: "Codex reprend", ja: "Codex が復旧中", ko: "Codex 복구 중" },
280
+ queuedTitle: { zh: "任务等待运行", en: "Task waiting to run", fr: "Tâche en attente", ja: "タスクは実行待ち", ko: "작업 실행 대기 중" },
281
+ standbyTip: { zh: "准备好了,下一条消息会从这里开始", en: "Ready when you are; the next message starts here", fr: "Prêt : le prochain message démarre ici", ja: "準備完了。次のメッセージから開始します", ko: "준비 완료. 다음 메시지부터 시작합니다" },
282
+ wastelandStandbyTip: { zh: "侦察机已待命 · 等待新指令", en: "Scout online · awaiting a new directive", fr: "Éclaireur en ligne · en attente d’instructions", ja: "偵察ユニット待機中 · 新しい指示を待っています", ko: "정찰 유닛 대기 중 · 새 지시를 기다리는 중" },
283
+ terminalWorkingTitle: { zh: "Codex 正在工作 · 终端已同步", en: "Codex is working · terminal synced", fr: "Codex travaille · terminal synchronisé", ja: "Codex が作業中 · ターミナル同期済み", ko: "Codex 작업 중 · 터미널 동기화됨" },
284
+ activeTurns: { zh: "{{count}} 个 turn", en: "{{count}} turns", fr: "{{count}} tours", ja: "{{count}} ターン", ko: "{{count}}개 turn" },
285
+ tipSteps: { zh: "正在把线索拼成答案", en: "Turning clues into an answer", fr: "Les indices deviennent une réponse", ja: "手がかりを答えにまとめています", ko: "단서를 답으로 엮는 중" },
286
+ tipContext: { zh: "正在检查有没有漏掉一步", en: "Checking for a missed step", fr: "Recherche d’une étape oubliée", ja: "見落とした手順がないか確認中", ko: "놓친 단계가 없는지 확인 중" },
287
+ tipEvidence: { zh: "正在给答案做最后抛光", en: "Giving the answer a final polish", fr: "Dernière touche à la réponse", ja: "答えを最後に磨いています", ko: "답변을 마지막으로 다듬는 중" },
288
+ tipSteer: { zh: "新消息会在下一回合接上", en: "New messages join the next turn", fr: "Les nouveaux messages passent au prochain tour", ja: "新しいメッセージは次のターンへ", ko: "새 메시지는 다음 turn에 이어집니다" },
289
+ tipGoal: { zh: "Goal 正沿着同一条线继续", en: "The goal keeps moving on this thread", fr: "L’objectif avance dans ce fil", ja: "Goal はこのスレッドで続いています", ko: "Goal은 이 스레드에서 계속됩니다" },
290
+ tipContinue: { zh: "可以先喝口水,这里还在推进", en: "Take a short break; work continues here", fr: "Petite pause possible, le travail continue ici", ja: "ひと息ついても、作業は続いています", ko: "잠시 쉬어도 여기서 계속 진행됩니다" },
291
+ queue: { zh: "队列", en: "Queue", fr: "File", ja: "キュー", ko: "대기열" },
292
+ queueEmpty: { zh: "队列为空,连续消息会在这里等待", en: "Queue is empty; messages sent in sequence wait here", fr: "File vide : les messages successifs attendront ici", ja: "キューは空です。続けて送るメッセージはここで待機します", ko: "대기열이 비어 있습니다. 연속 메시지는 여기서 기다립니다" },
293
+ queuedAfterTurn: { zh: "已加入队列,当前 turn 结束后自动发射", en: "Queued and will run after the current turn", fr: "Ajouté à la file et exécuté après le tour actuel", ja: "キューに追加し、現在のターン完了後に自動実行します", ko: "대기열에 추가되었으며 현재 turn 종료 후 자동 실행됩니다" },
294
+ clearQueue: { zh: "清空所有排队消息", en: "Clear all queued messages", fr: "Vider tous les messages en attente", ja: "待機メッセージをすべて消去", ko: "대기 메시지 모두 지우기" },
295
+ dispatchNow: { zh: "立即执行这条消息", en: "Run this message now", fr: "Exécuter ce message maintenant", ja: "このメッセージを今すぐ実行", ko: "이 메시지 지금 실행" },
296
+ editQueuedAction: { zh: "编辑排队消息", en: "Edit queued message", fr: "Modifier le message en attente", ja: "待機メッセージを編集", ko: "대기 메시지 편집" },
297
+ deleteQueuedAction: { zh: "删除排队消息", en: "Delete queued message", fr: "Supprimer le message en attente", ja: "待機メッセージを削除", ko: "대기 메시지 삭제" },
298
+ queueMore: { zh: "还有 {{count}} 条", en: "{{count}} more", fr: "{{count}} de plus", ja: "あと {{count}} 件", ko: "{{count}}개 더 있음" },
299
+ fileChanged: { zh: "文件已更新", en: "File updated", fr: "Fichier mis à jour", ja: "ファイルを更新", ko: "파일 업데이트됨" },
300
+ contextCompressed: { zh: "上下文已压缩", en: "Context compacted", fr: "Contexte compacté", ja: "コンテキストを圧縮しました", ko: "컨텍스트 압축됨" },
301
+ contextRemaining: { zh: "剩余", en: "left", fr: "restant", ja: "残り", ko: "남음" },
302
+ contextUsage: { zh: "上下文容量", en: "Context capacity", fr: "Capacité du contexte", ja: "コンテキスト容量", ko: "컨텍스트 용량" },
303
+ terminalTurnStarted: { zh: "同一任务的终端 turn 已开始", en: "A terminal turn started on this task", fr: "Un tour terminal a démarré sur cette tâche", ja: "同じタスクでターミナル turn が開始しました", ko: "같은 작업에서 터미널 turn 시작됨" },
304
+ terminalTurnCompleted: { zh: "终端 turn 已完成", en: "Terminal turn completed", fr: "Tour terminal terminé", ja: "ターミナル turn が完了しました", ko: "터미널 turn 완료됨" },
305
+ terminalTurnAborted: { zh: "终端 turn 已中断", en: "Terminal turn interrupted", fr: "Tour terminal interrompu", ja: "ターミナル turn が中断しました", ko: "터미널 turn 중단됨" },
306
+ terminalRemoteShell: { zh: "远程 Shell", en: "Remote shell", fr: "Shell distant", ja: "リモートシェル", ko: "원격 셸" },
307
+ terminalTitle: { zh: "远程终端", en: "Remote terminal", fr: "Terminal distant", ja: "リモートターミナル", ko: "원격 터미널" },
308
+ terminalNotConnected: { zh: "未连接", en: "Not connected", fr: "Non connecté", ja: "未接続", ko: "연결되지 않음" },
309
+ terminalConnecting: { zh: "连接中", en: "Connecting", fr: "Connexion", ja: "接続中", ko: "연결 중" },
310
+ terminalReconnecting: { zh: "重连中", en: "Reconnecting", fr: "Reconnexion", ja: "再接続中", ko: "재연결 중" },
311
+ terminalConnected: { zh: "已连接", en: "Connected", fr: "Connecté", ja: "接続済み", ko: "연결됨" },
312
+ terminalModuleError: { zh: "终端模块加载失败", en: "Terminal module failed to load", fr: "Échec du chargement du terminal", ja: "ターミナルモジュールの読み込みに失敗", ko: "터미널 모듈 로드 실패" },
313
+ terminalModuleToast: { zh: "终端组件未加载,请刷新页面重试", en: "Terminal components are unavailable; refresh and try again", fr: "Composants indisponibles ; actualisez puis réessayez", ja: "ターミナル部品を読み込めません。更新して再試行してください", ko: "터미널 구성 요소를 사용할 수 없습니다. 새로 고친 후 다시 시도하세요" },
314
+ terminalOutput: { zh: "远程终端输出", en: "Remote terminal output", fr: "Sortie du terminal distant", ja: "リモートターミナル出力", ko: "원격 터미널 출력" },
315
+ terminalHint: { zh: "点击终端后输入 · Enter 执行 · Ctrl+C 中断", en: "Click the terminal to type · Enter runs · Ctrl+C interrupts", fr: "Cliquez dans le terminal · Entrée exécute · Ctrl+C interrompt", ja: "ターミナルをクリックして入力 · Enterで実行 · Ctrl+Cで中断", ko: "터미널을 클릭해 입력 · Enter 실행 · Ctrl+C 중단" },
316
+ terminalReconnect: { zh: "重连", en: "Reconnect", fr: "Reconnecter", ja: "再接続", ko: "재연결" },
317
+ terminalClose: { zh: "关闭终端", en: "Close terminal", fr: "Fermer le terminal", ja: "ターミナルを閉じる", ko: "터미널 닫기" },
318
+ selectSessionFirst: { zh: "请先选择一个会话", en: "Select a session first", fr: "Sélectionnez d’abord une session", ja: "先にセッションを選択してください", ko: "먼저 세션을 선택하세요" },
319
+ terminalReconnectNotice: { zh: "连接中断,正在重连", en: "Connection interrupted; reconnecting", fr: "Connexion interrompue ; reconnexion", ja: "接続が中断されました。再接続中", ko: "연결이 끊겨 재연결 중" },
320
+ providerSwitched: { zh: "Provider 已切换:{{from}} → {{to}}", en: "Provider switched: {{from}} → {{to}}", fr: "Provider changé : {{from}} → {{to}}", ja: "Provider を切り替えました:{{from}} → {{to}}", ko: "Provider 전환됨: {{from}} → {{to}}" },
321
+ nextProvider: { zh: "下一个", en: "next", fr: "suivant", ja: "次", ko: "다음" },
322
+ codexActivity: { zh: "Codex 活动", en: "Codex activity", fr: "Activité Codex", ja: "Codex アクティビティ", ko: "Codex 활동" },
323
+ confirmTitle: { zh: "确认操作", en: "Confirm action", fr: "Confirmer l’action", ja: "操作の確認", ko: "작업 확인" },
324
+ confirmAction: { zh: "确认", en: "Confirm", fr: "Confirmer", ja: "確認", ko: "확인" },
325
+ authTitle: { zh: "连接 Codex Partner", en: "Connect to Codex Partner", fr: "Connexion à Codex Partner", ja: "Codex Partner に接続", ko: "Codex Partner 연결" },
326
+ authMessage: { zh: "输入服务器访问令牌后继续。令牌只保存在当前浏览器中。", en: "Enter the server access token to continue. It stays in this browser.", fr: "Saisissez le jeton d’accès au serveur. Il reste dans ce navigateur.", ja: "サーバーのアクセストークンを入力してください。現在のブラウザーだけに保存されます。", ko: "서버 액세스 토큰을 입력하세요. 현재 브라우저에만 저장됩니다." },
327
+ accessToken: { zh: "访问令牌", en: "Access token", fr: "Jeton d’accès", ja: "アクセストークン", ko: "액세스 토큰" },
328
+ connect: { zh: "连接", en: "Connect", fr: "Se connecter", ja: "接続", ko: "연결" },
329
+ renameSessionTitle: { zh: "重命名会话", en: "Rename session", fr: "Renommer la session", ja: "セッション名を変更", ko: "세션 이름 변경" },
330
+ sessionName: { zh: "会话名称", en: "Session name", fr: "Nom de la session", ja: "セッション名", ko: "세션 이름" },
331
+ permanentDeleteConfirm: { zh: "永久删除后无法恢复,Codex 线程记录也会被销毁。", en: "Permanent deletion cannot be undone and destroys the Codex thread record.", fr: "La suppression définitive est irréversible et détruit le fil Codex.", ja: "完全削除は取り消せず、Codex スレッドの記録も破棄されます。", ko: "영구 삭제는 취소할 수 없으며 Codex 스레드 기록도 제거됩니다." },
332
+ clearQueueConfirm: { zh: "清空所有排队消息?当前正在运行的 Codex turn 不会受到影响。", en: "Clear all queued messages? The current Codex turn will keep running.", fr: "Vider tous les messages en attente ? Le tour Codex actuel continuera.", ja: "待機メッセージをすべて消去しますか?現在の Codex turn は継続します。", ko: "대기 메시지를 모두 지울까요? 현재 Codex turn은 계속 실행됩니다." },
333
+ deleteMemoryConfirm: { zh: "删除这个 Codex 记忆文件?此操作无法撤销。", en: "Delete this Codex memory file? This cannot be undone.", fr: "Supprimer ce fichier mémoire Codex ? Cette action est irréversible.", ja: "この Codex メモリファイルを削除しますか?元に戻せません。", ko: "이 Codex 메모리 파일을 삭제할까요? 취소할 수 없습니다." },
334
+ rebuildMemoryConfirm: { zh: "重建线程生成记忆?现有生成内容将被重新计算。", en: "Rebuild generated thread memory? Existing generated content will be recalculated.", fr: "Reconstruire la mémoire générée ? Le contenu existant sera recalculé.", ja: "生成スレッドメモリを再構築しますか?既存の内容は再計算されます。", ko: "생성된 스레드 메모리를 다시 만들까요? 기존 내용이 다시 계산됩니다." },
335
+ deleteProviderConfirm: { zh: "删除这个 Provider?使用它的会话将回到自动选择。", en: "Delete this provider? Sessions using it will return to automatic selection.", fr: "Supprimer ce provider ? Les sessions associées repasseront en sélection automatique.", ja: "この Provider を削除しますか?使用中のセッションは自動選択に戻ります。", ko: "이 Provider를 삭제할까요? 사용 중인 세션은 자동 선택으로 돌아갑니다." },
336
+ deleteSkillConfirm: { zh: "删除这个 Skill?对应的 Skill 文件将被移除。", en: "Delete this skill? Its skill file will be removed.", fr: "Supprimer ce skill ? Son fichier sera supprimé.", ja: "この Skill を削除しますか?対応するファイルも削除されます。", ko: "이 Skill을 삭제할까요? 해당 Skill 파일도 제거됩니다." },
337
+ archiveSessionConfirm: { zh: "归档这个会话?之后仍可从归档中恢复。", en: "Archive this session? It can be restored later.", fr: "Archiver cette session ? Elle pourra être restaurée plus tard.", ja: "このセッションをアーカイブしますか?後で復元できます。", ko: "이 세션을 보관할까요? 나중에 복원할 수 있습니다." },
338
+ trashSessionConfirm: { zh: "把这个会话移到回收站?在永久删除前仍可恢复。", en: "Move this session to trash? It can be restored before permanent deletion.", fr: "Mettre cette session à la corbeille ? Elle reste récupérable avant suppression définitive.", ja: "このセッションをゴミ箱へ移動しますか?完全削除前なら復元できます。", ko: "이 세션을 휴지통으로 옮길까요? 영구 삭제 전에는 복원할 수 있습니다." },
339
+ clearGoalConfirm: { zh: "清空当前 Goal?Goal 自动续跑也会同时关闭。", en: "Clear the current goal? Goal auto-resume will also turn off.", fr: "Effacer l’objectif actuel ? La reprise automatique sera aussi désactivée.", ja: "現在の Goal を消去しますか?自動再開も無効になります。", ko: "현재 Goal을 지울까요? Goal 자동 재개도 함께 꺼집니다." },
340
+ overwriteFileConfirm: { zh: "{{name}} 已存在。要用新文件覆盖它吗?", en: "{{name}} already exists. Replace it with the new file?", fr: "{{name}} existe déjà. Le remplacer par le nouveau fichier ?", ja: "{{name}} は既に存在します。新しいファイルで上書きしますか?", ko: "{{name}} 파일이 이미 있습니다. 새 파일로 덮어쓸까요?" },
341
+ destructiveCommandConfirm: { zh: "{{command}} 会修改或删除持久内容。确认继续执行?", en: "{{command}} changes or deletes persistent content. Continue?", fr: "{{command}} modifie ou supprime du contenu persistant. Continuer ?", ja: "{{command}} は永続データを変更または削除します。続行しますか?", ko: "{{command}} 명령은 영구 콘텐츠를 변경하거나 삭제합니다. 계속할까요?" },
342
+ };
343
+ const I18N_RESOURCES = Object.fromEntries(Object.keys(LANGUAGE_PACKS).map(language => {
344
+ const translation = { ...LANGUAGE_PACKS[language], ...(LANGUAGE_EXTRAS[language] || {}) };
345
+ for (const [key, values] of Object.entries(UI_LABELS)) translation[key] = values[language] || values.zh || key;
346
+ return [language, { translation }];
347
+ }));
348
+ if (!window.i18next) throw new Error("i18next failed to load");
349
+ window.i18next.init({
350
+ lng: state.language,
351
+ fallbackLng: "zh",
352
+ supportedLngs: Object.keys(LANGUAGE_PACKS),
353
+ resources: I18N_RESOURCES,
354
+ initImmediate: false,
355
+ interpolation: { escapeValue: false },
356
+ });
357
+ function uiLabel(key, options = {}) { return t(key, key, options); }
358
+ function mascotMarkup(className = "") { return `<span class="mascot mascot-inline ${className}" aria-hidden="true"><i></i><b></b><em></em></span>`; }
359
+ function humanMarkup(className = "") {
360
+ if (state.userAvatar) return `<span class="human-avatar image-avatar ${className}" aria-hidden="true"><img class="human-avatar-image" src="${esc(state.userAvatar)}" alt="" /></span>`;
361
+ return `<span class="human-avatar ${className}" aria-hidden="true"><i></i><b></b></span>`;
362
+ }
363
+ function updateThemeToggle() {
364
+ const toggle = document.querySelector("#theme-toggle");
365
+ if (!toggle) return;
366
+ const wasteland = document.documentElement.dataset.theme === "wasteland";
367
+ const label = uiLabel(wasteland ? "darkTheme" : "wastelandTheme");
368
+ toggle.title = label; toggle.setAttribute("aria-label", label); toggle.setAttribute("aria-pressed", wasteland ? "true" : "false");
369
+ }
370
+ function standbyTipLabel() { return uiLabel(document.documentElement.dataset.theme === "wasteland" ? "wastelandStandbyTip" : "standbyTip"); }
371
+ function setTheme(theme, announce = false) {
372
+ const next = theme === "wasteland" ? "wasteland" : "dark";
373
+ document.documentElement.dataset.theme = next;
374
+ localStorage.setItem("codex-dashboard-theme", next);
375
+ updateThemeToggle();
376
+ const progress = document.querySelector("#turn-progress");
377
+ if (progress?.classList.contains("idle")) document.querySelector("#turn-progress-tip").textContent = standbyTipLabel();
378
+ if (announce) toast(uiLabel(next === "wasteland" ? "wastelandEnabled" : "darkEnabled"));
379
+ }
380
+ function toggleTheme() { setTheme(document.documentElement.dataset.theme === "wasteland" ? "dark" : "wasteland", true); }
381
+ function applyLanguage(language = state.language) {
382
+ state.language = LANGUAGE_PACKS[language] ? language : "zh";
383
+ window.i18next.changeLanguage(state.language);
384
+ localStorage.setItem("codex-dashboard-language", state.language);
385
+ document.documentElement.lang = state.language === "zh" ? "zh-CN" : state.language;
386
+ const text = { "#new-task span": "newSession", ".shortcut-hint:nth-child(1) .shortcut-label": "terminal", ".shortcut-hint:nth-child(2) .shortcut-label": "shortcutNext", ".shortcut-hint:nth-child(3) .shortcut-label": "shortcutPrevious", ".shortcut-hint:nth-child(4) .shortcut-label": "shortcutFirst", ".sidebar-top h1": "sessions", ".filter[data-filter=all]": "all", ".filter[data-filter=running]": "running", ".filter[data-filter=available]": "available", ".trash-filter": "trash", ".sidebar-link[data-panel=memories] span": "memories", ".sidebar-link[data-panel=skills] span": "skills", "#empty-conversation h2": "emptyTitle", "#empty-conversation p": "emptyCopy", "#empty-new-task": "create", "#send-codex-label": "send", "#message-input": "composerPlaceholder", "#session-search": "searchSessions", ".composer-model-control > span": "model", ".composer-effort-control > span": "effort", "#inspector .inspector-head h2": "workspace", "#goal-bar .eyebrow": "goal", "#goal-clear": "clear", "#goal-edit-cancel": "cancel", "#goal-edit-form .primary-small": "save", "#goal-input": "goalInput", "#composer-live-state > span": "ready", "#terminal-eyebrow": "terminalRemoteShell", "#terminal-title": "terminalTitle", "#terminal-cwd": "terminalNotConnected", "#terminal-hint": "terminalHint", "#terminal-reconnect": "terminalReconnect", "#terminal-screen": "terminalOutput" };
387
+ for (const [selector, key] of Object.entries(text)) { const node = $(selector); if (!node) continue; if (["#message-input", "#session-search"].includes(selector)) node.placeholder = t(key); else if (selector === "#terminal-screen") node.setAttribute("aria-label", t(key)); else node.textContent = t(key); }
388
+ const titles = { "#open-terminal": "terminal", "#sync-native": "sync", "#conversation-rename": "rename", "#conversation-fork": "fork", "#conversation-more": "more", "#language-select": "language", "#goal-edit": "editGoal", "#inspector": "workspace", "#composer-meta": "session", "#terminal-close": "terminalClose", ".shortcut-hint:nth-child(1)": "shortcutTerminal", ".shortcut-hint:nth-child(2)": "shortcutNext", ".shortcut-hint:nth-child(3)": "shortcutPrevious", ".shortcut-hint:nth-child(4)": "shortcutFirst" };
389
+ for (const [selector, key] of Object.entries(titles)) { const node = $(selector); if (node) { node.title = t(key); node.setAttribute("aria-label", t(key)); } }
390
+ const select = $("#language-select"); if (select) select.value = state.language;
391
+ updateThemeToggle();
392
+ window.dispatchEvent(new CustomEvent("languagechange"));
393
+ }
394
+ const $ = (selector, root = document) => root.querySelector(selector);
395
+ const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
396
+ const esc = (value = "") => String(value).replace(/[&<>"']/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;", "'":"&#39;"}[c]));
397
+ applyLanguage();
398
+ function setInspectorOpen(open) {
399
+ const inspector = $("#inspector");
400
+ if (!inspector) return;
401
+ inspector.classList.toggle("open", open);
402
+ inspector.classList.toggle("closed", !open);
403
+ inspector.setAttribute("aria-hidden", open ? "false" : "true");
404
+ }
405
+ let authToken = localStorage.getItem("codex-dashboard-token") || new URLSearchParams(location.search).get("token") || "";
406
+ if (authToken) localStorage.setItem("codex-dashboard-token", authToken);
407
+ let socket = null;
408
+ let socketTaskId = null;
409
+ let socketReconnectTimer = null;
410
+ let socketPaused = false;
411
+ let overviewSocket = null;
412
+ let overviewReconnectTimer = null;
413
+ let chatRenderFrame = null;
414
+ let pendingAttachments = [];
415
+ let terminalSocket = null;
416
+ let terminalTaskId = null;
417
+ let terminalEmulator = null;
418
+ let terminalFitAddon = null;
419
+ let terminalResizeObserver = null;
420
+ let terminalReconnectTimer = null;
421
+ let terminalReconnectAttempt = 0;
422
+ let terminalShouldReconnect = false;
423
+ let queueSyncTimer = null;
424
+ let queueSyncInFlight = false;
425
+ let markdownComponents = [];
426
+ const livePhases = new Map();
427
+ const liveStartedAt = new Map();
428
+ const realtimeChannels = { overview: "connecting", task: "idle", terminal: "idle" };
429
+ const realtimeLastSeen = { overview: 0, task: 0, terminal: 0 };
430
+ let authPromptPromise = null;
431
+
432
+ function showAppDialog(options = {}) {
433
+ const dialog = $("#app-dialog");
434
+ const form = $("#app-dialog-form");
435
+ const input = $("#app-dialog-input");
436
+ const field = $("#app-dialog-field");
437
+ $("#app-dialog-title").textContent = options.title || uiLabel("confirmTitle");
438
+ $("#app-dialog-message").textContent = options.message || "";
439
+ $("#app-dialog-label").textContent = options.label || "";
440
+ $("#app-dialog-cancel").textContent = options.cancelLabel || uiLabel("cancel");
441
+ $("#app-dialog-confirm").textContent = options.confirmLabel || uiLabel("confirmAction");
442
+ $("#app-dialog-confirm").className = options.danger ? "danger" : "primary";
443
+ field.hidden = !options.input;
444
+ input.type = options.inputType || "text";
445
+ input.value = options.value || "";
446
+ input.placeholder = options.placeholder || "";
447
+ input.required = Boolean(options.required);
448
+ return new Promise(resolve => {
449
+ let settled = false;
450
+ const finish = value => {
451
+ if (settled) return;
452
+ settled = true;
453
+ dialog.close();
454
+ resolve(value);
455
+ };
456
+ form.onsubmit = event => { event.preventDefault(); if (!form.reportValidity()) return; finish(options.input ? input.value : true); };
457
+ $("#app-dialog-cancel").onclick = () => finish(options.input ? null : false);
458
+ dialog.oncancel = event => { event.preventDefault(); finish(options.input ? null : false); };
459
+ dialog.onclick = event => {
460
+ if (event.target !== dialog) return;
461
+ const bounds = form.getBoundingClientRect();
462
+ if (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom) finish(options.input ? null : false);
463
+ };
464
+ if (dialog.open) dialog.close();
465
+ dialog.showModal();
466
+ requestAnimationFrame(() => (options.input ? input : $("#app-dialog-confirm")).focus());
467
+ });
468
+ }
469
+
470
+ function appConfirm(message, options = {}) {
471
+ return showAppDialog({ ...options, message, input: false });
472
+ }
473
+
474
+ function appPrompt(options = {}) {
475
+ return showAppDialog({ ...options, input: true });
476
+ }
477
+
478
+ async function requestAccessToken() {
479
+ if (!authPromptPromise) {
480
+ authPromptPromise = appPrompt({ title: uiLabel("authTitle"), message: uiLabel("authMessage"), label: uiLabel("accessToken"), inputType: "password", confirmLabel: uiLabel("connect"), required: true }).finally(() => { authPromptPromise = null; });
481
+ }
482
+ return authPromptPromise;
483
+ }
484
+
485
+ async function api(path, options = {}, canPrompt = true) {
486
+ const headers = { "Content-Type": "application/json", ...(options.headers || {}) };
487
+ if (authToken) headers.Authorization = `Bearer ${authToken}`;
488
+ const response = await fetch(`/api${path}`, { ...options, headers });
489
+ if (response.status === 401 && canPrompt) {
490
+ const token = await requestAccessToken();
491
+ if (token) { authToken = token.trim(); localStorage.setItem("codex-dashboard-token", authToken); return api(path, options, false); }
492
+ }
493
+ if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText })); throw Error(error.detail || response.statusText); }
494
+ return response.json();
495
+ }
496
+ function toast(message) { const node = $("#toast"); node.textContent = message; node.classList.add("show"); setTimeout(() => node.classList.remove("show"), 2600); }
497
+ async function copyText(value) {
498
+ try { await navigator.clipboard.writeText(value); }
499
+ catch (_) {
500
+ const input = document.createElement("textarea"); input.value = value; input.style.position = "fixed"; input.style.opacity = "0"; document.body.append(input); input.select(); document.execCommand("copy"); input.remove();
501
+ }
502
+ }
503
+ function formatBytes(value) {
504
+ let size = Math.max(0, Number(value) || 0); const units = ["B", "KB", "MB", "GB"]; let index = 0;
505
+ while (size >= 1024 && index < units.length - 1) { size /= 1024; index += 1; }
506
+ const digits = index === 0 || size >= 100 ? 0 : size >= 10 ? 1 : 2;
507
+ return `${size.toFixed(digits)} ${units[index]}`;
508
+ }
509
+ function renderCodexAvailability(health) {
510
+ state.codexAvailable = health.codex_available !== false;
511
+ state.codexInstall = health.codex_install || null;
512
+ state.serverInfo = { ...state.serverInfo, ...health, user: health.server_user || "", hostname: health.server_hostname || "" };
513
+ document.body.classList.toggle("codex-missing", !state.codexAvailable);
514
+ const notice = $("#codex-install-notice");
515
+ notice.hidden = state.codexAvailable;
516
+ if (state.codexAvailable) return;
517
+ const plan = state.codexInstall || {};
518
+ $("#codex-install-command").textContent = plan.command || plan.reason || "请在服务器安装 Codex CLI";
519
+ $("#codex-install-command").title = plan.command || plan.reason || "";
520
+ const button = $("#install-codex");
521
+ button.disabled = !plan.supported;
522
+ button.textContent = plan.supported ? "一键安装" : "缺少 npm";
523
+ }
524
+ function providerUsable(provider) {
525
+ if (!provider || provider.enabled === false) return false;
526
+ if (["auth_error", "unavailable", "invalid", "degraded", "error"].includes(provider.health_status)) return false;
527
+ return Boolean(provider.has_key || provider.native || ["healthy", "reachable", "configured"].includes(provider.health_status));
528
+ }
529
+ function currentProvider() {
530
+ const task = state.selectedTask;
531
+ const session = task?.active_session_id ? state.sessions.find(item => item.id === task.active_session_id) : null;
532
+ const providerId = session?.provider_id || task?.provider_id;
533
+ if (providerId) return state.providers.find(item => item.id === providerId) || { id: providerId, name: providerId };
534
+ return state.providers.find(item => Number(item.in_use_count || 0) > 0)
535
+ || state.providers.find(item => item.is_default && providerUsable(item))
536
+ || null;
537
+ }
538
+ function formatMetricMs(value) {
539
+ if (!Number.isFinite(Number(value))) return "--";
540
+ const numeric = Math.max(0, Number(value));
541
+ return numeric < 1000 ? `${Math.round(numeric)}ms` : `${(numeric / 1000).toFixed(1)}s`;
542
+ }
543
+ function eventTimestampMs(event) {
544
+ const raw = event?.ts ?? event?.created_at;
545
+ if (raw === null || raw === undefined || raw === "") return 0;
546
+ const numeric = Number(raw);
547
+ if (Number.isFinite(numeric)) return numeric < 1e12 ? numeric * 1000 : numeric;
548
+ const parsed = Date.parse(String(raw));
549
+ return Number.isFinite(parsed) ? parsed : 0;
550
+ }
551
+ function estimateOutputTokens(text) {
552
+ const value = String(text || "");
553
+ const nonAscii = (value.match(/[^\x00-\x7F]/g) || []).length;
554
+ const asciiWords = (value.match(/[A-Za-z0-9_]+/g) || []).reduce((total, word) => total + Math.max(1, Math.ceil(word.length / 4)), 0);
555
+ const punctuation = (value.match(/[.,!?;:{}()[\]`~@#$%^&*+=/\\<>|_-]/g) || []).length;
556
+ return nonAscii + asciiWords + punctuation;
557
+ }
558
+ function calculateRuntimeMetrics(task = state.selectedTask, events = state.selectedEvents) {
559
+ if (!task) return { taskId: "", ttftMs: null, tpotMs: null, estimated: true, outputTokens: 0 };
560
+ const rows = (events || [])
561
+ .slice(-2000)
562
+ .map(event => ({ event, payload: eventValue(event.payload) || {}, timestamp: eventTimestampMs(event) }))
563
+ .filter(row => row.timestamp)
564
+ .slice()
565
+ .sort((a, b) => a.timestamp - b.timestamp);
566
+ const turnId = row => String(row.payload.turn_id || row.payload.turnId || row.payload?.params?.turnId || "");
567
+ const turnOrder = [];
568
+ for (const row of rows) { const id = turnId(row); if (id && !turnOrder.includes(id)) turnOrder.push(id); }
569
+ let selectedRows = [];
570
+ for (const id of turnOrder.slice().reverse()) {
571
+ const candidate = rows.filter(row => turnId(row) === id);
572
+ if (candidate.some(row => ["agent_delta", "agentmessage"].includes(String(row.payload.type || "").toLowerCase()))) { selectedRows = candidate; break; }
573
+ }
574
+ if (!selectedRows.length) return { taskId: task.id, ttftMs: null, tpotMs: null, estimated: true, outputTokens: 0 };
575
+ const startRow = selectedRows.find(row => ["usermessage", "browsermessage", "externalturnstarted"].includes(String(row.payload.type || "").toLowerCase()));
576
+ const allDeltas = selectedRows.filter(row => String(row.payload.type || "").toLowerCase() === "agent_delta" && String(row.payload.delta || ""));
577
+ const messages = selectedRows.filter(row => String(row.payload.type || "").toLowerCase() === "agentmessage" && String(row.payload.text || row.payload.message || ""));
578
+ const firstOutput = allDeltas[0] || messages[0];
579
+ if (!startRow || !firstOutput) return { taskId: task.id, ttftMs: null, tpotMs: null, estimated: true, outputTokens: 0 };
580
+ const lastItemId = allDeltas[allDeltas.length - 1]?.payload.item_id || "";
581
+ const deltas = lastItemId ? allDeltas.filter(row => row.payload.item_id === lastItemId) : allDeltas;
582
+ const finalMessage = messages[messages.length - 1];
583
+ const outputText = finalMessage
584
+ ? String(finalMessage.payload.text || finalMessage.payload.message || "")
585
+ : deltas.map(row => String(row.payload.delta || "")).join("");
586
+ const outputTokens = estimateOutputTokens(outputText);
587
+ const lastOutput = deltas[deltas.length - 1] || firstOutput;
588
+ const outputElapsed = deltas.length > 1 ? Math.max(0, lastOutput.timestamp - firstOutput.timestamp) : 0;
589
+ return {
590
+ taskId: task.id,
591
+ ttftMs: Math.max(0, firstOutput.timestamp - startRow.timestamp),
592
+ tpotMs: deltas.length > 1 && outputTokens > 1 ? outputElapsed / (outputTokens - 1) : null,
593
+ estimated: true,
594
+ outputTokens,
595
+ };
596
+ }
597
+ function calculateContextUsage(events = state.selectedEvents) {
598
+ for (const event of (events || []).slice().reverse()) {
599
+ const payload = eventValue(event.payload) || {};
600
+ if (payload.type !== "codex" || payload.method !== "thread/tokenUsage/updated") continue;
601
+ const usage = payload.params?.tokenUsage || {};
602
+ const used = Number(usage.last?.inputTokens);
603
+ const capacity = Number(usage.modelContextWindow);
604
+ if (!Number.isFinite(used) || !Number.isFinite(capacity) || capacity <= 0) continue;
605
+ const boundedUsed = Math.min(Math.max(0, used), capacity);
606
+ return { used: boundedUsed, capacity, remaining: Math.max(0, capacity - boundedUsed), usedPercent: boundedUsed / capacity * 100 };
607
+ }
608
+ return null;
609
+ }
610
+ function formatCompactTokens(value) {
611
+ const numeric = Math.max(0, Number(value) || 0);
612
+ if (numeric >= 1_000_000) return `${(numeric / 1_000_000).toFixed(numeric >= 10_000_000 ? 0 : 1)}M`;
613
+ if (numeric >= 1000) return `${Math.round(numeric / 1000)}K`;
614
+ return String(Math.round(numeric));
615
+ }
616
+ function renderContextUsage() {
617
+ const node = $("#composer-context-usage");
618
+ if (!node) return;
619
+ const usage = calculateContextUsage();
620
+ const copy = node.querySelector(".context-usage-copy");
621
+ const fill = node.querySelector(".context-usage-track i");
622
+ node.classList.remove("warning", "critical");
623
+ if (!usage) {
624
+ copy.textContent = "Context --";
625
+ fill.style.width = "0%";
626
+ node.title = uiLabel("contextUsage");
627
+ return;
628
+ }
629
+ const remainingPercent = Math.max(0, Math.round(100 - usage.usedPercent));
630
+ copy.textContent = `${uiLabel("contextRemaining")} ${formatCompactTokens(usage.remaining)} · ${remainingPercent}%`;
631
+ fill.style.width = `${Math.min(100, usage.usedPercent).toFixed(1)}%`;
632
+ node.classList.toggle("warning", remainingPercent <= 35);
633
+ node.classList.toggle("critical", remainingPercent <= 15);
634
+ node.title = `${uiLabel("contextUsage")} · ${formatCompactTokens(usage.used)} / ${formatCompactTokens(usage.capacity)}`;
635
+ }
636
+ function renderConnectionStatus() {
637
+ const node = $("#connection-status");
638
+ if (!node) return;
639
+ const terminalOpen = $("#terminal-window")?.classList.contains("open");
640
+ const channels = [realtimeChannels.overview];
641
+ if (state.selectedId && realtimeChannels.task !== "paused") channels.push(realtimeChannels.task);
642
+ if (terminalOpen && !["idle", "closed"].includes(realtimeChannels.terminal)) channels.push(realtimeChannels.terminal);
643
+ let kind = "live"; let label = uiLabel("realtime");
644
+ if (!navigator.onLine) { kind = "offline"; label = uiLabel("disconnected"); }
645
+ else if (channels.some(value => ["reconnecting", "offline"].includes(value))) { kind = "reconnecting"; label = uiLabel("reconnecting"); }
646
+ else if (channels.some(value => ["connecting", "idle"].includes(value))) { kind = "connecting"; label = uiLabel("connecting"); }
647
+ const overlayOpen = $("#panel")?.classList.contains("open") || $("#drawer")?.classList.contains("open");
648
+ node.className = `connection-popover ${kind}${overlayOpen ? " panel-open" : ""}`;
649
+ $("#connection-status-label").textContent = label;
650
+ const serverLabel = $("#connection-server-label");
651
+ const webHost = location.hostname || state.serverInfo.hostname || "网页服务器";
652
+ const webPort = location.port || "8787";
653
+ const serverName = `${state.serverInfo.user || "服务用户"}@${webHost}`;
654
+ if (serverLabel) { serverLabel.textContent = webHost; serverLabel.title = `${serverName}:${webPort}`; }
655
+ const provider = currentProvider();
656
+ const metrics = state.runtimeMetrics || {};
657
+ const providerLabel = $("#connection-provider-label");
658
+ const metricsLabel = $("#connection-metrics-label");
659
+ const currentApiLabel = state.language === "zh" ? "当前 API ·" : `${uiLabel("currentApi")} ·`;
660
+ if (providerLabel) providerLabel.textContent = `${currentApiLabel} ${provider?.name || uiLabel("automatic")}`;
661
+ if (metricsLabel) metricsLabel.textContent = `TTFT ${formatMetricMs(metrics.ttftMs)} · TPOT ${formatMetricMs(metrics.tpotMs)}${metrics.estimated && metrics.outputTokens ? ` · ${uiLabel("estimated")}` : ""}`;
662
+ const names = { overview: "总览", task: "会话", terminal: "终端" };
663
+ const values = { connecting: "连接中", live: "实时", reconnecting: "重连中", offline: "中断", idle: "未启用", closed: "已退出", paused: "已暂停" };
664
+ const serverDetail = `${state.serverInfo.user || "服务用户"}@${webHost}:${webPort} · 网页服务已连接`;
665
+ node.title = `${Object.entries(realtimeChannels).map(([key, value]) => `${names[key]}:${values[value] || value}`).join(" · ")} · 服务:${serverDetail} · 当前 API:${provider?.name || "自动选择"} · TTFT ${formatMetricMs(metrics.ttftMs)} · TPOT ${formatMetricMs(metrics.tpotMs)} · 点击查看网页服务`;
666
+ }
667
+ function setRealtimeChannel(channel, value) {
668
+ if (realtimeChannels[channel] === value) return;
669
+ realtimeChannels[channel] = value;
670
+ renderConnectionStatus();
671
+ }
672
+ function noteRealtime(channel) { realtimeLastSeen[channel] = Date.now(); }
673
+ function heartbeatRealtime() {
674
+ const entries = [["overview", overviewSocket], ["task", socket], ["terminal", terminalSocket]];
675
+ for (const [channel, current] of entries) {
676
+ if (!current || current.readyState !== WebSocket.OPEN) continue;
677
+ if (realtimeLastSeen[channel] && Date.now() - realtimeLastSeen[channel] > 32_000) {
678
+ setRealtimeChannel(channel, "reconnecting");
679
+ current.close(4000, "heartbeat timeout");
680
+ continue;
681
+ }
682
+ try { current.send(JSON.stringify({ type: "ping" })); } catch (_) { current.close(); }
683
+ }
684
+ }
685
+ function statusLabel(value) { const keys = { running: "statusRunning", retrying: "statusRetrying", queued: "statusQueued", available: "statusAvailable", succeeded: "statusSucceeded", failed: "statusFailed", stopped: "statusStopped", archived: "statusArchived", trashed: "statusTrashed", imported: "statusImported" }; return keys[value] ? t(keys[value]) : value || t("unknown"); }
686
+ function status(value) { return `<span class="status status-${esc(value)}"><i></i>${esc(statusLabel(value))}</span>`; }
687
+ function shortDate(value) {
688
+ if (!value) return "";
689
+ const date = new Date(value); if (Number.isNaN(date.valueOf())) return value;
690
+ const delta = Date.now() - date.valueOf();
691
+ const locale = state.language === "zh" ? "zh-CN" : state.language;
692
+ const relative = new Intl.RelativeTimeFormat(locale, { numeric: "auto", style: "short" });
693
+ if (delta < 60_000) return relative.format(0, "second");
694
+ if (delta < 3_600_000) return relative.format(-Math.floor(delta / 60_000), "minute");
695
+ if (delta < 86_400_000) return relative.format(-Math.floor(delta / 3_600_000), "hour");
696
+ return date.toLocaleDateString(locale, { month: "short", day: "numeric" });
697
+ }
698
+ function eventValue(value) { try { return typeof value === "string" ? JSON.parse(value) : value; } catch (_) { return value; } }
699
+ function eventText(value) {
700
+ const payload = eventValue(value);
701
+ if (typeof payload === "string") return payload;
702
+ if (!payload || typeof payload !== "object") return String(payload ?? "");
703
+ if (payload.type === "userMessage" || payload.type === "browserMessage") return payload.text || (payload.content || []).map(x => x.text || "").join("\n");
704
+ if (payload.type === "agentMessage") return payload.text || "";
705
+ if (payload.type === "agentMessageStarted") return "";
706
+ if (payload.type === "agent_delta") return payload.delta || "";
707
+ if (payload.type === "reasoning") return payload.text || payload.summary || "";
708
+ if (payload.type === "fileChange") return payload.text || uiLabel("fileChanged");
709
+ if (payload.type === "contextCompaction") return uiLabel("contextCompressed");
710
+ if (payload.type === "externalTurnStarted") return uiLabel("terminalTurnStarted");
711
+ if (payload.type === "turn_completed") return uiLabel("terminalTurnCompleted");
712
+ if (payload.type === "turn_aborted") return uiLabel("terminalTurnAborted");
713
+ if (payload.type === "provider_failover") return uiLabel("providerSwitched", { from: payload.from || uiLabel("default"), to: payload.to || uiLabel("nextProvider") });
714
+ if (payload.type === "slashCommand" || payload.type === "commandResult") return payload.text || "";
715
+ if (payload.type === "codex") return payload.method ? `Codex · ${payload.method}` : uiLabel("codexActivity");
716
+ if (payload.delta) return payload.delta;
717
+ return payload.text || JSON.stringify(payload);
718
+ }
719
+ function activityPhase(value) {
720
+ const payload = eventValue(value);
721
+ if (!payload || typeof payload !== "object") return "";
722
+ const type = String(payload.type || payload.item?.type || "").toLowerCase();
723
+ const method = String(payload.method || "").toLowerCase();
724
+ if (type === "agent_delta" || type.includes("agentmessage")) return "phaseGenerating";
725
+ if (type.includes("reason") || type === "plan") return "phasePlanning";
726
+ if (type.includes("command") || method.includes("command")) return "phaseCommand";
727
+ if (type.includes("file") || method.includes("file")) return "phaseFiles";
728
+ if (type.includes("tool") || type.includes("mcp") || method.includes("tool")) return "phaseTools";
729
+ if (type.includes("search") || method.includes("search")) return "phaseSearch";
730
+ if (type === "contextcompaction" || method.includes("compact")) return "phaseCompact";
731
+ if (type === "provider_failover") return "phaseProvider";
732
+ if (type === "externalturnstarted") return "phaseTerminalSynced";
733
+ if (type === "usermessage") return "phaseInstruction";
734
+ return "";
735
+ }
736
+ function activityPhaseLabel(value) {
737
+ if (UI_LABELS[value]) return uiLabel(value);
738
+ for (const key of Object.keys(UI_LABELS).filter(item => item.startsWith("phase"))) {
739
+ if (Object.values(UI_LABELS[key]).includes(value)) return uiLabel(key);
740
+ }
741
+ return value || uiLabel("phaseAnalyzing");
742
+ }
743
+ function latestActivityPhase(task) {
744
+ for (let index = state.selectedEvents.length - 1; index >= 0; index -= 1) {
745
+ const event = state.selectedEvents[index];
746
+ if (task?.active_session_id && event.session_id !== task.active_session_id) continue;
747
+ const phase = activityPhase(event.payload);
748
+ if (phase) return phase;
749
+ }
750
+ return "";
751
+ }
752
+ function activeStartedAt(task) {
753
+ if (task.external_started_at) {
754
+ const external = new Date(task.external_started_at).valueOf();
755
+ const started = Number.isFinite(external) ? external : Date.now();
756
+ liveStartedAt.set(task.id, started);
757
+ return started;
758
+ }
759
+ if (liveStartedAt.has(task.id)) return liveStartedAt.get(task.id);
760
+ const sessions = task.sessions || state.sessions.filter(session => session.task_id === task.id);
761
+ const active = sessions.find(session => session.id === task.active_session_id) || sessions.find(session => ["running", "retrying"].includes(session.status));
762
+ const candidate = new Date(active?.started_at || task.updated_at || Date.now()).valueOf();
763
+ const started = Number.isFinite(candidate) ? candidate : Date.now();
764
+ liveStartedAt.set(task.id, started);
765
+ return started;
766
+ }
767
+ function elapsedLabel(started) {
768
+ const total = Math.max(0, Math.floor((Date.now() - started) / 1000));
769
+ const minutes = Math.floor(total / 60);
770
+ return `${minutes}:${String(total % 60).padStart(2, "0")}`;
771
+ }
772
+ function updateTurnElapsed() {
773
+ const task = state.selectedTask;
774
+ const tip = $("#turn-progress-tip");
775
+ if (!tip || !task || !["running", "retrying", "queued"].includes(task.status)) return;
776
+ const keys = ["tipSteps", "tipContext", "tipEvidence", "tipSteer", "tipGoal", "tipContinue"];
777
+ const elapsed = Math.max(0, Math.floor((Date.now() - activeStartedAt(task)) / 1000));
778
+ const seed = [...String(task.id || "")].reduce((sum, char) => sum + char.charCodeAt(0), 0);
779
+ const next = uiLabel(keys[(Math.floor(elapsed / 9) + seed) % keys.length]);
780
+ if (tip.textContent !== next) tip.textContent = next;
781
+ }
782
+ function renderTurnProgress() {
783
+ const task = state.selectedTask;
784
+ const node = $("#turn-progress");
785
+ if (!node || !task) return;
786
+ const active = ["running", "retrying", "queued"].includes(task.status);
787
+ node.className = `turn-progress ${active ? "active" : "idle"} ${esc(task.status || "available")}`;
788
+ node.setAttribute("aria-hidden", "false");
789
+ if (!active) {
790
+ livePhases.delete(task.id);
791
+ liveStartedAt.delete(task.id);
792
+ $("#turn-progress-tip").textContent = standbyTipLabel();
793
+ node.setAttribute("aria-label", uiLabel("ready"));
794
+ return;
795
+ }
796
+ if (!livePhases.has(task.id)) livePhases.set(task.id, task.external_phase || latestActivityPhase(task) || (task.status === "queued" ? "phaseQueued" : "phaseAnalyzing"));
797
+ node.setAttribute("aria-label", task.status === "retrying" ? uiLabel("recoveringTitle") : uiLabel("workingTitle"));
798
+ updateTurnElapsed();
799
+ }
800
+ function isUserEvent(event) { const payload = eventValue(event.payload); return payload?.type === "userMessage" || payload?.type === "browserMessage" || payload?.type === "slashCommand" || event.stream === "user"; }
801
+ function isAssistantEvent(event) { const payload = eventValue(event.payload); return payload?.type === "agentMessage" || payload?.type === "agent_delta" || payload?.type === "commandResult" || event.stream === "assistant"; }
802
+ function taskMatches(task) { if (state.filter === "running" && !["running", "retrying", "queued"].includes(task.status)) return false; if (state.filter !== "all" && state.filter !== "running" && task.status !== state.filter) return false; if (!state.query) return true; const haystack = `${task.name} ${task.prompt} ${task.goal} ${task.workspace}`.toLowerCase(); return haystack.includes(state.query.toLowerCase()); }
803
+ function sortTasks(tasks) { return [...tasks].sort((a, b) => String(b.updated_at || b.created_at || "").localeCompare(String(a.updated_at || a.created_at || "")) || String(b.id || "").localeCompare(String(a.id || ""))); }
804
+ function mergeTask(task) {
805
+ const index = state.tasks.findIndex(item => item.id === task.id);
806
+ if (index < 0) state.tasks = [task, ...state.tasks];
807
+ else state.tasks[index] = { ...state.tasks[index], ...task };
808
+ }
809
+ function mergeOverviewSnapshot(tasks) {
810
+ const incoming = new Map(tasks.map(task => [task.id, task]));
811
+ state.tasks = state.tasks.map(task => incoming.get(task.id) ? { ...task, ...incoming.get(task.id) } : task);
812
+ for (const task of tasks) if (!state.tasks.some(item => item.id === task.id)) state.tasks.push(task);
813
+ }
814
+ function normalizeTaskMessage(message, current = null) {
815
+ const id = message.id || message.message_id;
816
+ if (!id) return null;
817
+ const ranks = { sending: 0, queued: 1, dispatching: 2, running: 3, steering: 3, sent: 4, steered: 4, failed: 4 };
818
+ const incomingRank = ranks[message.status];
819
+ const currentRank = ranks[current?.status];
820
+ // A fast dispatch can finish over WebSocket before the original enqueue
821
+ // request returns. Never let that older HTTP response resurrect the row.
822
+ if (current && !message.error && incomingRank !== undefined && currentRank !== undefined && incomingRank < currentRank) {
823
+ message = { ...message, status: current.status, started_at: current.started_at, finished_at: current.finished_at, session_id: current.session_id, error: current.error || "" };
824
+ }
825
+ return { id, ...(current || {}), ...message, body: message.body ?? current?.body ?? "" };
826
+ }
827
+ function upsertTaskMessage(message) {
828
+ const id = message.id || message.message_id;
829
+ if (!id) return;
830
+ const index = state.selectedMessages.findIndex(item => item.id === id);
831
+ const normalized = normalizeTaskMessage(message, state.selectedMessages[index]);
832
+ if (index < 0) state.selectedMessages.push(normalized);
833
+ else state.selectedMessages[index] = normalized;
834
+ }
835
+ function replaceTaskMessages(messages) {
836
+ const current = new Map(state.selectedMessages.map(message => [message.id, message]));
837
+ state.selectedMessages = (messages || []).map(message => normalizeTaskMessage(message, current.get(message.id || message.message_id))).filter(Boolean);
838
+ }
839
+ function removeTaskMessage(messageId) {
840
+ state.selectedMessages = state.selectedMessages.filter(message => message.id !== messageId);
841
+ }
842
+ function refreshComposerHistory() {
843
+ const seen = new Set();
844
+ state.composerHistory = [...(state.selectedMessages || [])]
845
+ .filter(message => message.body && ["sent", "steered", "failed", "running", "steering", "sending"].includes(message.status))
846
+ .sort((a, b) => String(a.created_at || "").localeCompare(String(b.created_at || "")))
847
+ .map(message => message.body)
848
+ .filter(body => !seen.has(body) && seen.add(body))
849
+ .slice(-40);
850
+ state.historyIndex = -1;
851
+ }
852
+ function scheduleRenderChat() {
853
+ if (chatRenderFrame) return;
854
+ chatRenderFrame = requestAnimationFrame(() => { chatRenderFrame = null; renderChat(); });
855
+ }
856
+
857
+ async function refresh(keepSelection = true) {
858
+ try {
859
+ const [tasks, sessions, providers, skills, memories, health, commandData] = await Promise.all([api("/tasks"), api("/sessions"), api("/providers"), api("/skills"), api("/memories"), api("/health"), api("/commands")]);
860
+ state.tasks = tasks; state.sessions = sessions; state.providers = providers; state.skills = skills; state.memories = memories.files || []; state.generatedMemories = memories.generated || []; state.commands = commandData.commands || []; state.defaultWorkspace = health.default_workspace || state.defaultWorkspace; renderCodexAvailability(health); renderConnectionStatus();
861
+ renderSessionList(); renderSidebarStats(); refreshOpenPanel();
862
+ if (state.selectedId && !state.tasks.some(task => task.id === state.selectedId)) state.selectedId = null;
863
+ if (!overviewSocket || overviewSocket.readyState === WebSocket.CLOSED) connectOverviewSocket();
864
+ if (!state.selectedId && state.tasks.length) await selectSession(sortTasks(state.tasks)[0].id);
865
+ else if (state.selectedId && (keepSelection || !state.selectedTask)) await selectSession(state.selectedId, !state.selectedTask);
866
+ } catch (error) { setRealtimeChannel("overview", "reconnecting"); connectOverviewSocket(); toast(error.message); }
867
+ }
868
+ async function refreshSelectedConversation(id) {
869
+ try {
870
+ const [task, timeline, messages] = await Promise.all([api(`/tasks/${id}`), api(`/tasks/${id}/timeline?limit=160`), api(`/tasks/${id}/messages`)]);
871
+ if (state.selectedId !== id) return;
872
+ mergeTask(task);
873
+ state.selectedTask = task;
874
+ state.selectedEvents = [...(timeline.items || []), ...(timeline.metrics || [])];
875
+ state.historyCursor = timeline.next_cursor || ""; state.historyHasMore = Boolean(timeline.has_more);
876
+ state.chatVirtualStart = null;
877
+ replaceTaskMessages(messages);
878
+ renderConversation(); renderSessionList(); renderSidebarStats();
879
+ } catch (_) { /* Reconnect or the overview channel will retry authoritative state. */ }
880
+ }