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/app.js ADDED
@@ -0,0 +1,505 @@
1
+ // DOM event wiring and application bootstrap. Feature logic lives in focused modules.
2
+ document.addEventListener("click", async event => {
3
+ const button = event.target.closest("button"); if (!button) return;
4
+ try {
5
+ if (button.id === "install-codex") {
6
+ button.disabled = true; button.textContent = "安装中…";
7
+ const result = await api("/codex/install", { method: "POST" });
8
+ toast(`Codex 已安装:${result.codex_bin}`);
9
+ return refresh(false);
10
+ }
11
+ if (button.id === "new-task" || button.id === "empty-new-task") return createQuickSession();
12
+ if (button.id === "open-terminal") return openTerminal();
13
+ if (button.id === "terminal-close") return closeTerminal();
14
+ if (button.id === "terminal-reconnect") { if (!state.selectedId) return toast("请先选择一个会话"); const windowNode = $("#terminal-window"); windowNode.classList.add("open"); windowNode.setAttribute("aria-hidden", "false"); const preserveHistory = terminalTaskId === state.selectedId; if (terminalSocket) { terminalSocket.onclose = null; terminalSocket.close(); terminalSocket = null; } return connectTerminal(state.selectedId, preserveHistory); }
15
+ if (button.id === "connection-status") return openPanel("server");
16
+ if (button.id === "update-codex") {
17
+ button.disabled = true; button.textContent = uiLabel("updatingCodex");
18
+ const result = await api("/codex/update", { method: "POST" });
19
+ toast(`Codex 已更新:${result.codex_bin || "latest"}`);
20
+ return openPanel("server");
21
+ }
22
+ if (button.dataset.trashRestore) { await api(`/trash/${encodeURIComponent(button.dataset.trashRestore)}/restore`, { method: "POST" }); await refresh(false); await renderTrashPanel(); return toast("会话已恢复"); }
23
+ if (button.dataset.trashDelete && await appConfirm(uiLabel("permanentDeleteConfirm"), { danger: true })) { await api(`/trash/${encodeURIComponent(button.dataset.trashDelete)}`, { method: "DELETE" }); await renderTrashPanel(); return toast("会话已永久删除"); }
24
+ if (button.id === "sync-native") { await api("/native/sync", { method: "POST" }); await refresh(false); return toast("本机 Codex 已同步"); }
25
+ if (button.dataset.composerAttachmentRemove !== undefined) {
26
+ pendingAttachments.splice(Number(button.dataset.composerAttachmentRemove), 1);
27
+ renderComposerAttachments(); renderGoalBar(); return;
28
+ }
29
+ if (button.dataset.sessionId) return selectSession(button.dataset.sessionId);
30
+ if (button.dataset.queueClear !== undefined) {
31
+ if (!state.selectedId || !await appConfirm(uiLabel("clearQueueConfirm"), { danger: true })) return;
32
+ const result = await api(`/tasks/${state.selectedId}/messages`, { method: "DELETE" });
33
+ for (const messageId of result.message_ids || []) removeTaskMessage(messageId);
34
+ if (state.editingQueuedId && (result.message_ids || []).includes(state.editingQueuedId)) {
35
+ state.editingQueuedId = null; $("#message-input").value = ""; $("#composer-context").textContent = "";
36
+ }
37
+ renderQueuedMessages(); renderGoalBar(); scheduleRenderChat(); return toast("排队消息已清空");
38
+ }
39
+ if (button.dataset.queueEdit) {
40
+ const message = state.selectedMessages.find(item => item.id === button.dataset.queueEdit);
41
+ if (!message) return;
42
+ state.editingQueuedId = message.id;
43
+ const input = $("#message-input"); input.value = message.body || ""; resizeComposerInput(input); $("#composer-context").textContent = "正在编辑排队消息"; renderQueuedMessages(); renderGoalBar(); input.focus(); return;
44
+ }
45
+ if (button.dataset.queueDispatch) {
46
+ const message = state.selectedMessages.find(item => item.id === button.dataset.queueDispatch);
47
+ if (!message) return;
48
+ button.disabled = true; button.textContent = "…";
49
+ try {
50
+ const result = await api(`/tasks/${state.selectedId}/messages/${button.dataset.queueDispatch}/dispatch`, { method: "POST" });
51
+ upsertTaskMessage(result); renderQueuedMessages(); scheduleRenderChat();
52
+ toast(result.status === "steered" ? "已插入当前 Codex turn" : "已立即执行");
53
+ } catch (error) { button.disabled = false; button.textContent = "▶"; toast(error.message); }
54
+ return;
55
+ }
56
+ if (button.dataset.queueDelete) {
57
+ await api(`/tasks/${state.selectedId}/messages/${button.dataset.queueDelete}`, { method: "DELETE" });
58
+ removeTaskMessage(button.dataset.queueDelete);
59
+ if (state.editingQueuedId === button.dataset.queueDelete) { state.editingQueuedId = null; $("#message-input").value = ""; $("#composer-context").textContent = ""; }
60
+ renderQueuedMessages(); renderGoalBar(); scheduleRenderChat(); return toast("排队消息已删除");
61
+ }
62
+ if (button.dataset.filter) { state.filter = button.dataset.filter; $$(".filter").forEach(node => node.classList.toggle("active", node === button)); return renderSessionList(); }
63
+ if (button.dataset.panel) return openPanel(button.dataset.panel);
64
+ if (button.id === "close-panel") return closePanel();
65
+ if (button.id === "close-drawer" || button.id === "cancel") return closeDrawer();
66
+ if (button.id === "mobile-sessions") return $(".session-sidebar").classList.toggle("open");
67
+ if (button.id === "close-inspector") { state.inspectorClosed = true; setInspectorOpen(false); return; }
68
+ if (button.dataset.workspaceChange !== undefined) return openWorkspacePicker();
69
+ if (button.dataset.workspaceUpload !== undefined) {
70
+ const input = $("#workspace-upload-input"); input.dataset.path = state.workspacePath; input.click(); return;
71
+ }
72
+ if (button.dataset.workspaceDownload !== undefined) return downloadWorkspaceFile(button.dataset.workspaceDownload);
73
+ if (button.dataset.workspaceEdit !== undefined) return openWorkspaceFileEditor(button.dataset.workspaceEdit);
74
+ if (button.dataset.copyThread) { await copyText(button.dataset.copyThread); return toast("Thread ID 已复制"); }
75
+ if (button.dataset.sshUse !== undefined) { state.activeSSHHost = button.dataset.sshUse; if (state.activeSSHHost) localStorage.setItem("codex-dashboard-ssh-host", state.activeSSHHost); else localStorage.removeItem("codex-dashboard-ssh-host"); drawSSHPanel(); return toast(state.activeSSHHost ? `新会话将使用 ${state.activeSSHHost}` : "新会话将使用本机"); }
76
+ if (button.dataset.sshConnect) return connectSSHHost(button.dataset.sshConnect);
77
+ if (button.dataset.sshInstall) { button.disabled = true; button.textContent = "安装中…"; const row = await api(`/ssh/install-codex?host=${encodeURIComponent(button.dataset.sshInstall)}`, { method: "POST" }); updateSSHHost(row); drawSSHPanel(); return toast("Codex 已安装"); }
78
+ if (button.dataset.sshDisconnect) { await api(`/ssh/disconnect?host=${encodeURIComponent(button.dataset.sshDisconnect)}`, { method: "POST" }); updateSSHHost({ ...state.sshHosts.find(item => item.alias === button.dataset.sshDisconnect), connected: false, status: "disconnected" }); if (state.activeSSHHost === button.dataset.sshDisconnect) { state.activeSSHHost = ""; localStorage.removeItem("codex-dashboard-ssh-host"); } drawSSHPanel(); return toast("SSH 已断开"); }
79
+ if (button.dataset.pickerBrowse !== undefined) return loadWorkspacePicker(button.dataset.pickerBrowse);
80
+ if (button.id === "workspace-picker-select") return selectWorkspaceDirectory();
81
+ if (button.dataset.browse !== undefined) return loadWorkspace(button.dataset.browse);
82
+ if (button.id === "conversation-rename") return openRename();
83
+ if (button.id === "conversation-fork") return runOperation("fork");
84
+ if (button.id === "conversation-more") {
85
+ const inspector = $("#inspector");
86
+ const shouldOpen = inspector.classList.contains("closed") || !inspector.classList.contains("open");
87
+ state.inspectorClosed = !shouldOpen; setInspectorOpen(shouldOpen); return;
88
+ }
89
+ if (button.dataset.threadAction) return runOperation(button.dataset.threadAction);
90
+ if (button.id === "new-memory") return memoryForm();
91
+ if (button.dataset.memoryView) return memoryView(button.dataset.memoryView);
92
+ if (button.dataset.memoryEdit) return memoryForm(button.dataset.memoryEdit);
93
+ if (button.dataset.generatedMemory) return generatedMemoryView(button.dataset.generatedMemory);
94
+ if (button.dataset.memoryDelete && await appConfirm(uiLabel("deleteMemoryConfirm"), { danger: true })) { await api(`/memories/${button.dataset.memoryDelete.split("/").map(encodeURIComponent).join("/")}`, { method: "DELETE" }); await refresh(false); return openPanel("memories"); }
95
+ if (button.id === "reset-generated-memories" && await appConfirm(uiLabel("rebuildMemoryConfirm"), { danger: true })) { await api("/memories/reset", { method: "POST", body: JSON.stringify({ confirm: true }) }); return toast("生成记忆已重置"); }
96
+ if (button.id === "new-provider") return providerForm();
97
+ if (button.dataset.providerUse) {
98
+ if (!state.selectedId || !state.selectedTask) return toast("请先选择一个会话");
99
+ if (button.disabled) return;
100
+ button.disabled = true;
101
+ const provider = state.providers.find(item => item.id === button.dataset.providerUse);
102
+ try {
103
+ const activeTurn = ["running", "retrying", "queued"].includes(state.selectedTask.status);
104
+ const task = await api(`/tasks/${state.selectedId}`, { method: "PATCH", body: JSON.stringify({ provider_id: button.dataset.providerUse, model: "" }) });
105
+ state.selectedTask = { ...state.selectedTask, ...task };
106
+ mergeTask(task); renderConversation(); refreshOpenPanel();
107
+ toast(activeTurn ? `已选择 ${provider?.name || "Provider"},当前 turn 完成后生效` : `当前会话已切换到 ${provider?.name || "Provider"}`);
108
+ } catch (error) { toast(`Provider 切换失败:${error.message}`); }
109
+ finally { button.disabled = false; }
110
+ return;
111
+ }
112
+ if (button.dataset.providerEdit) return providerForm(state.providers.find(item => item.id === button.dataset.providerEdit));
113
+ if (button.dataset.providerCheck) return checkOneProvider(button.dataset.providerCheck);
114
+ if (button.dataset.providerDelete && await appConfirm(uiLabel("deleteProviderConfirm"), { danger: true })) { await api(`/providers/${button.dataset.providerDelete}`, { method: "DELETE" }); await refresh(false); return openPanel("server"); }
115
+ if (button.id === "sync-providers") { await api("/providers/sync-native", { method: "POST" }); await refresh(false); return openPanel("server"); }
116
+ if (button.id === "check-providers") return checkAllProviders();
117
+ if (button.id === "new-skill") return skillForm();
118
+ if (button.dataset.skillEdit) return skillForm(state.skills.find(item => item.id === button.dataset.skillEdit));
119
+ if (button.dataset.skillDelete && await appConfirm(uiLabel("deleteSkillConfirm"), { danger: true })) { await api(`/skills/${button.dataset.skillDelete}`, { method: "DELETE" }); await refresh(false); return openPanel("skills"); }
120
+ } catch (error) { toast(error.message); }
121
+ });
122
+ $("#language-select").onchange = event => {
123
+ applyLanguage(event.target.value);
124
+ renderSessionList(); renderSidebarStats();
125
+ if (state.selectedTask) renderConversation();
126
+ renderConnectionStatus();
127
+ refreshOpenPanel();
128
+ };
129
+ $("#terminal-window").addEventListener("click", event => { if (event.target.id === "terminal-window") closeTerminal(); });
130
+ $("#panel").addEventListener("click", event => { if (event.target === event.currentTarget) closePanel(); });
131
+ $("#drawer").addEventListener("click", event => { if (event.target === event.currentTarget) closeDrawer(); });
132
+ async function openRename() { const name = await appPrompt({ title: uiLabel("renameSessionTitle"), label: uiLabel("sessionName"), value: state.selectedTask?.name || "", confirmLabel: uiLabel("save"), required: true }); if (!name?.trim()) return; await api(`/tasks/${state.selectedId}/operation`, { method: "POST", body: JSON.stringify({ operation: "rename", args: [name.trim()] }) }); await refresh(); toast("会话已重命名"); }
133
+ function switchSession(step) {
134
+ const tasks = sortTasks(state.tasks);
135
+ if (!tasks.length) return toast("还没有可切换的会话");
136
+ const current = tasks.findIndex(task => task.id === state.selectedId);
137
+ const nextIndex = current < 0 ? (step > 0 ? 0 : tasks.length - 1) : (current + step + tasks.length) % tasks.length;
138
+ const next = tasks[nextIndex];
139
+ if (next?.id && next.id !== state.selectedId) selectSession(next.id);
140
+ }
141
+ function switchToTopSession() {
142
+ const top = sortTasks(state.tasks)[0];
143
+ if (!top) return toast("还没有可切换的会话");
144
+ if (top.id === state.selectedId) { renderSessionList(); scrollSessionIntoView(top.id); return; }
145
+ selectSession(top.id);
146
+ }
147
+ function toggleTerminalShortcut() {
148
+ if (!state.selectedId) return toast("请先选择一个会话");
149
+ if ($("#terminal-window")?.classList.contains("open")) closeTerminal();
150
+ else openTerminal();
151
+ }
152
+ async function runOperation(operation) {
153
+ if (!state.selectedId) return;
154
+ if (["archive", "delete"].includes(operation)) {
155
+ const message = uiLabel(operation === "delete" ? "trashSessionConfirm" : "archiveSessionConfirm");
156
+ if (!await appConfirm(message, { danger: operation === "delete" })) return;
157
+ }
158
+ const result = await api(`/tasks/${state.selectedId}/operation`, { method: "POST", body: JSON.stringify({ operation, args: [] }) });
159
+ if (result.memory_mode && state.selectedTask) {
160
+ // Reflect the acknowledged mode immediately while the authoritative
161
+ // refresh also updates the other browser clients.
162
+ state.selectedTask = { ...state.selectedTask, memory_mode: result.memory_mode };
163
+ mergeTask(state.selectedTask);
164
+ renderConversation();
165
+ }
166
+ if (result.deleted || result.trashed) {
167
+ if (terminalTaskId === state.selectedId) destroyTerminal();
168
+ state.selectedId = null;
169
+ showEmptyConversation();
170
+ await refresh(false);
171
+ } else {
172
+ // A command can change fields that are only present in the full task
173
+ // payload (memory mode, archive state, renamed title). Refresh the
174
+ // selected conversation as well as the sidebar, otherwise the inspector
175
+ // keeps rendering the previous snapshot.
176
+ await refresh(true);
177
+ if (result.task?.id && result.task.id !== state.selectedId) await selectSession(result.task.id);
178
+ }
179
+ const message = operation === "fork" ? "会话已复制" : result.trashed ? "会话已移到回收站" : operation === "unarchive" ? "会话已取消归档" : operation === "memory-enable" ? "记忆已打开" : operation === "memory-disable" ? "记忆已关闭" : "操作已完成";
180
+ toast(message);
181
+ }
182
+
183
+ async function submitMessage(mode = "codex") {
184
+ if (!state.selectedId) return;
185
+ const taskId = state.selectedId;
186
+ const input = $("#message-input");
187
+ let message = input.value.trim();
188
+ const activeTurn = ["running", "retrying", "queued"].includes(state.selectedTask?.status);
189
+ if (!message && !pendingAttachments.length) {
190
+ if (mode === "codex" && activeTurn && !state.editingQueuedId) {
191
+ await changeSelectedRun("stop");
192
+ }
193
+ return;
194
+ }
195
+ if (state.editingQueuedId) {
196
+ const messageId = state.editingQueuedId;
197
+ try {
198
+ const updated = await api(`/tasks/${taskId}/messages/${messageId}`, { method: "PATCH", body: JSON.stringify({ message }) });
199
+ upsertTaskMessage(updated); state.editingQueuedId = null; input.value = ""; input.style.height = ""; $("#composer-context").textContent = ""; renderQueuedMessages(); renderGoalBar(); scheduleRenderChat(); input.focus(); toast("排队消息已更新");
200
+ } catch (error) { toast(error.message); }
201
+ return;
202
+ }
203
+ if (mode === "codex" && message.startsWith("/") && !pendingAttachments.length) {
204
+ input.value = ""; input.style.height = ""; $("#command-palette").hidden = true;
205
+ try { await submitSlashCommand(message); } catch (error) { input.value = message; renderCommandPalette(); toast(error.message); }
206
+ return;
207
+ }
208
+ if (pendingAttachments.length) {
209
+ message += pendingAttachments.map((file, index) => {
210
+ const prefix = message || index ? "\n\n" : "";
211
+ if (file.binary) return `${prefix}[${uiLabel("attachments")}:${file.name}]\n${uiLabel("binaryAttachment", { path: file.workspacePath || file.name })}\n[[codex-file:${encodeURIComponent(file.workspacePath || file.name)}]]`;
212
+ return `${prefix}[${uiLabel("attachments")}:${file.name}]\n\n\`\`\`\n${file.content}\n\`\`\``;
213
+ }).join("");
214
+ }
215
+ // Enter never interrupts an active turn. The server owns the durable FIFO
216
+ // and dispatches this message as a new turn once the current owner exits.
217
+ const clientMessageId = crypto.randomUUID ? crypto.randomUUID() : String(Date.now());
218
+ const delivery = mode === "queue" || activeTurn ? "queue" : "auto";
219
+ const optimisticStatus = delivery === "queue" ? "queued" : "sending";
220
+ const optimistic = { id: clientMessageId, body: message, status: optimisticStatus, created_at: new Date().toISOString() };
221
+ upsertTaskMessage(optimistic); state.historyIndex = -1; input.value = ""; resizeComposerInput(input); pendingAttachments = []; $("#composer-context").textContent = ""; $("#attach-file").value = ""; $("#command-palette").hidden = true; renderGoalBar(); scheduleRenderChat(); input.focus();
222
+ try {
223
+ const delivered = await api(`/tasks/${taskId}/messages`, { method: "POST", body: JSON.stringify({ message, client_message_id: clientMessageId, delivery }) });
224
+ if (state.selectedId === taskId) { upsertTaskMessage(delivered); renderQueuedMessages(); refreshComposerHistory(); renderGoalBar(); scheduleRenderChat(); }
225
+ if (activeTurn && delivered.status === "queued") toast(uiLabel("queuedAfterTurn"));
226
+ } catch (error) {
227
+ if (state.selectedId === taskId) { upsertTaskMessage({ id: clientMessageId, status: "failed", error: error.message }); renderGoalBar(); scheduleRenderChat(); }
228
+ toast(error.message);
229
+ }
230
+ }
231
+ $("#message-form").onsubmit = async event => { event.preventDefault(); await submitMessage("codex"); };
232
+
233
+ $("#goal-edit").onclick = () => {
234
+ const task = state.selectedTask; if (!task) return;
235
+ $("#goal-input").value = task.goal || "";
236
+ $("#goal-clear").hidden = !task.goal;
237
+ $("#goal-text").hidden = true; $("#goal-edit-form").hidden = false;
238
+ $("#goal-input").focus();
239
+ };
240
+ $("#goal-edit-cancel").onclick = () => { $("#goal-edit-form").hidden = true; $("#goal-text").hidden = false; };
241
+ $("#goal-clear").onclick = async () => {
242
+ if (!state.selectedId || !state.selectedTask?.goal || !await appConfirm(uiLabel("clearGoalConfirm"), { danger: true })) return;
243
+ try {
244
+ const task = await api(`/tasks/${state.selectedId}/goal`, { method: "PUT", body: JSON.stringify({ objective: "" }) });
245
+ state.selectedTask = { ...state.selectedTask, ...task }; mergeTask(task); $("#goal-edit-form").hidden = true; $("#goal-text").hidden = false; renderConversation(); toast("Goal 已清空");
246
+ } catch (error) { toast(error.message); }
247
+ };
248
+ $("#goal-edit-form").onsubmit = async event => {
249
+ event.preventDefault(); if (!state.selectedId) return;
250
+ try {
251
+ const task = await api(`/tasks/${state.selectedId}/goal`, { method: "PUT", body: JSON.stringify({ objective: $("#goal-input").value.trim() }) });
252
+ state.selectedTask = { ...state.selectedTask, ...task }; mergeTask(task); $("#goal-edit-form").hidden = true; $("#goal-text").hidden = false; renderConversation(); toast(task.goal ? "Goal 已更新" : "Goal 已清除");
253
+ } catch (error) { toast(error.message); }
254
+ };
255
+ async function changeSelectedRun(action, announce = true) {
256
+ if (!state.selectedId || !state.selectedTask) return null;
257
+ try {
258
+ const task = await api(`/tasks/${state.selectedId}/${action}`, { method: "POST" });
259
+ state.selectedTask = { ...state.selectedTask, ...task }; mergeTask(task); renderConversation();
260
+ if (announce) toast(action === "stop" ? "Codex 已停止,输入内容仍保留" : "Codex 已继续运行");
261
+ return task;
262
+ } catch (error) {
263
+ if (announce) toast(error.message);
264
+ return null;
265
+ }
266
+ }
267
+ async function toggleGoalRun() {
268
+ const task = state.selectedTask;
269
+ const goal = String(task?.goal || "").trim();
270
+ if (!task || !state.selectedId || !goal) return toast("请先设置 Goal");
271
+ const goalRunning = ["running", "retrying", "queued"].includes(task.status) && task.goal_status !== "paused";
272
+ if (goalRunning) {
273
+ const stopped = await changeSelectedRun("stop", false);
274
+ if (!stopped || ["running", "retrying", "queued"].includes(stopped.status)) return toast("当前 Codex 未能暂停");
275
+ }
276
+ try {
277
+ const updated = await api(`/tasks/${state.selectedId}/goal`, { method: "PUT", body: JSON.stringify({ status: goalRunning ? "paused" : "active" }) });
278
+ state.selectedTask = { ...state.selectedTask, ...updated }; mergeTask(updated); renderConversation();
279
+ if (!goalRunning) {
280
+ const resumed = await changeSelectedRun("resume", false);
281
+ if (!resumed) return toast("Goal 已继续,但恢复 Codex 失败");
282
+ toast("Goal 已继续,正在 resume");
283
+ } else {
284
+ toast("Goal 已暂停");
285
+ }
286
+ } catch (error) { toast(error.message); }
287
+ }
288
+ $("#goal-run-toggle").onclick = toggleGoalRun;
289
+ $("#goal-retry-toggle").onclick = async () => {
290
+ if (!state.selectedId || !state.selectedTask) return;
291
+ if (!String(state.selectedTask.goal || "").trim()) return toast("请先设置 Goal");
292
+ try {
293
+ const task = await api(`/tasks/${state.selectedId}`, { method: "PATCH", body: JSON.stringify({ retry_forever: !state.selectedTask.retry_forever }) });
294
+ state.selectedTask = { ...state.selectedTask, ...task }; mergeTask(task); renderConversation(); toast(task.retry_forever ? "Goal 自动续跑已开启" : "Goal 自动续跑已关闭");
295
+ } catch (error) { toast(error.message); }
296
+ };
297
+ function resizeComposerInput(input = $("#message-input")) {
298
+ if (!input) return;
299
+ input.style.height = "30px";
300
+ const height = Math.min(Math.max(input.scrollHeight, 30), 120);
301
+ input.style.height = `${height}px`;
302
+ input.style.overflowY = input.scrollHeight > 120 ? "auto" : "hidden";
303
+ }
304
+ function renderComposerAttachments() {
305
+ const context = $("#composer-context");
306
+ if (!context) return;
307
+ context.classList.toggle("attachments", Boolean(pendingAttachments.length));
308
+ if (!pendingAttachments.length) { context.textContent = ""; return; }
309
+ context.innerHTML = pendingAttachments.map((file, index) => `<span class="composer-attachment" title="${esc(file.name)}"><span aria-hidden="true">▤</span><strong>${esc(file.name)}</strong><small>${formatBytes(file.size)}</small><button type="button" data-composer-attachment-remove="${index}" title="${uiLabel("delete")}">×</button></span>`).join("");
310
+ }
311
+ function attachmentUploadName(name) {
312
+ const value = String(name || "attachment").replace(/[^\w.()\-\u4e00-\u9fff ]/g, "_").trim() || "attachment";
313
+ const dot = value.lastIndexOf(".");
314
+ const stem = dot > 0 ? value.slice(0, dot) : value;
315
+ const ext = dot > 0 ? value.slice(dot) : "";
316
+ return `${stem}-${Date.now().toString(36)}${ext}`;
317
+ }
318
+ async function attachComposerFiles(fileList) {
319
+ const files = [...(fileList || [])].filter(Boolean);
320
+ if (!files.length) return;
321
+ const added = [];
322
+ let inlineChars = pendingAttachments.reduce((total, item) => total + (item.binary ? 0 : String(item.content || "").length), 0);
323
+ for (const file of files) {
324
+ const name = file.name || "attachment";
325
+ try {
326
+ const sample = new Uint8Array(await file.slice(0, Math.min(file.size || 0, 8192)).arrayBuffer());
327
+ const declaredText = String(file.type || "").startsWith("text/") || /\.(txt|md|markdown|json|ya?ml|xml|csv|log|py|js|ts|tsx?|jsx|css|html?|sh|toml|ini|cfg|conf)$/i.test(name);
328
+ const magic = String.fromCharCode(...sample.slice(0, 8));
329
+ const binarySignature = magic.startsWith("\x89PNG\r\n\x1a\n") || magic.startsWith("\xff\xd8\xff") || magic.startsWith("GIF8") || magic.startsWith("%PDF") || magic.startsWith("PK\x03\x04") || magic.startsWith("\x7fELF");
330
+ let text = "";
331
+ let textFile = declaredText && !binarySignature;
332
+ if (textFile) {
333
+ try { text = await file.text(); new TextDecoder("utf-8", { fatal: true }).decode(sample); }
334
+ catch (_) { textFile = false; }
335
+ } else {
336
+ textFile = !binarySignature && !sample.includes(0) && (() => { try { new TextDecoder("utf-8", { fatal: true }).decode(sample); return true; } catch (_) { return false; } })();
337
+ if (textFile) text = await file.text();
338
+ }
339
+ if (textFile && inlineChars + text.length <= 12000) {
340
+ added.push({ name, size: file.size || 0, content: text, binary: false });
341
+ inlineChars += text.length;
342
+ continue;
343
+ }
344
+ if (!state.selectedId) throw new Error("请先选择一个会话");
345
+ // Pasted/copied files get a unique name, so an attachment never opens
346
+ // the workspace overwrite confirmation dialog unexpectedly.
347
+ const uploadName = attachmentUploadName(name);
348
+ const uploadFile = new File([file], uploadName, { type: file.type || "application/octet-stream", lastModified: file.lastModified });
349
+ const result = await uploadWorkspaceFile(state.selectedId, state.workspacePath || "", uploadFile);
350
+ if (result?.skipped) continue;
351
+ const workspacePath = result?.entry?.path || `${state.workspacePath ? `${state.workspacePath}/` : ""}${name}`;
352
+ added.push({ name, size: file.size || 0, content: "", binary: true, workspacePath });
353
+ } catch (error) {
354
+ toast(error?.message ? `${uiLabel("fileUploadFailed", { name })}:${error.message}` : uiLabel("fileReadFailed", { name }));
355
+ }
356
+ }
357
+ if (!added.length) return;
358
+ pendingAttachments.push(...added);
359
+ renderComposerAttachments();
360
+ renderGoalBar();
361
+ toast(uiLabel("filesReady", { count: added.length }));
362
+ $("#message-input").focus();
363
+ }
364
+ $("#message-input").addEventListener("input", event => { resizeComposerInput(event.target); state.commandIndex = 0; state.historyIndex = -1; renderCommandPalette(); renderGoalBar(); });
365
+ $("#message-input").addEventListener("keydown", async event => {
366
+ if (event.isComposing || event.keyCode === 229) return;
367
+ const palette = $("#command-palette");
368
+ if (!palette.hidden) {
369
+ const matches = commandMatches(event.target.value);
370
+ if (event.key === "ArrowDown") { event.preventDefault(); state.commandIndex = (state.commandIndex + 1) % matches.length; renderCommandPalette(); return; }
371
+ if (event.key === "ArrowUp") { event.preventDefault(); state.commandIndex = (state.commandIndex - 1 + matches.length) % matches.length; renderCommandPalette(); return; }
372
+ if (event.key === "Tab") { event.preventDefault(); completeCommand(); return; }
373
+ if (event.key === "Escape") { event.preventDefault(); palette.hidden = true; return; }
374
+ if (event.key === "Enter" && !event.shiftKey && matches.length && !event.target.value.trim().includes(" ")) {
375
+ const command = selectedCommand(); const value = event.target.value.trim().slice(1).toLowerCase();
376
+ const exact = command && (value === command.name || (command.aliases || []).includes(value));
377
+ if (!exact) { event.preventDefault(); completeCommand(command); return; }
378
+ }
379
+ }
380
+ if (event.key === "Escape" && state.editingQueuedId) {
381
+ event.preventDefault(); event.stopPropagation(); state.editingQueuedId = null; event.target.value = ""; resizeComposerInput(event.target); $("#composer-context").textContent = ""; renderQueuedMessages(); renderGoalBar(); return;
382
+ }
383
+ if (event.key === "Enter" && event.shiftKey) { event.preventDefault(); return; }
384
+ if (event.key === "Escape" && ["running", "retrying", "queued"].includes(state.selectedTask?.status)) {
385
+ event.preventDefault(); event.stopPropagation(); await changeSelectedRun("stop"); return;
386
+ }
387
+ if (event.key === "ArrowUp" && !event.shiftKey && !event.altKey && !event.metaKey && !event.ctrlKey && (!event.target.value || state.historyIndex >= 0)) {
388
+ if (!state.composerHistory.length) return;
389
+ event.preventDefault(); state.historyIndex = state.historyIndex < 0 ? state.composerHistory.length - 1 : Math.max(0, state.historyIndex - 1); event.target.value = state.composerHistory[state.historyIndex]; resizeComposerInput(event.target); return;
390
+ }
391
+ if (event.key === "ArrowDown" && !event.shiftKey && !event.altKey && !event.metaKey && !event.ctrlKey && state.historyIndex >= 0) {
392
+ event.preventDefault(); state.historyIndex += 1; if (state.historyIndex >= state.composerHistory.length) { state.historyIndex = -1; event.target.value = ""; } else event.target.value = state.composerHistory[state.historyIndex]; resizeComposerInput(event.target); return;
393
+ }
394
+ if (event.key === "Enter" && event.altKey && !event.ctrlKey) { event.preventDefault(); await submitMessage("queue"); return; }
395
+ if (event.key === "Enter" && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey) { event.preventDefault(); $("#message-form").requestSubmit(); }
396
+ });
397
+ $("#session-search").addEventListener("input", event => { state.query = event.target.value.trim(); renderSessionList(); });
398
+ $("#attach-button").onclick = () => $("#attach-file").click();
399
+ $("#theme-toggle").onclick = toggleTheme;
400
+ $("#attach-file").multiple = true;
401
+ $("#attach-file").onchange = async event => { await attachComposerFiles(event.target.files); event.target.value = ""; };
402
+ document.addEventListener("click", event => {
403
+ if (event.target.closest(".message.user .message-avatar")) $("#user-avatar-file")?.click();
404
+ });
405
+ $("#user-avatar-file").onchange = event => {
406
+ const file = event.target.files?.[0];
407
+ if (!file) return;
408
+ if (!String(file.type || "").startsWith("image/")) { toast("请选择图片文件"); event.target.value = ""; return; }
409
+ const reader = new FileReader();
410
+ reader.onload = () => {
411
+ state.userAvatar = String(reader.result || "");
412
+ try { localStorage.setItem("codex-dashboard-user-avatar", state.userAvatar); } catch (_) { toast("图片过大,无法保存头像"); return; }
413
+ renderChat();
414
+ toast("头像已更新");
415
+ };
416
+ reader.onerror = () => toast("头像读取失败");
417
+ reader.readAsDataURL(file);
418
+ event.target.value = "";
419
+ };
420
+ $("#message-input").addEventListener("paste", event => {
421
+ const files = [...(event.clipboardData?.files || [])];
422
+ if (!files.length) return;
423
+ event.preventDefault();
424
+ attachComposerFiles(files);
425
+ });
426
+ const composer = $("#message-form");
427
+ for (const eventName of ["dragenter", "dragover"]) {
428
+ composer.addEventListener(eventName, event => {
429
+ if (!event.dataTransfer?.types.includes("Files")) return;
430
+ event.preventDefault(); event.stopPropagation(); composer.classList.add("dragging");
431
+ });
432
+ }
433
+ composer.addEventListener("dragleave", event => {
434
+ if (event.relatedTarget && composer.contains(event.relatedTarget)) return;
435
+ composer.classList.remove("dragging");
436
+ });
437
+ composer.addEventListener("drop", event => {
438
+ if (!event.dataTransfer?.files.length) return;
439
+ event.preventDefault(); event.stopPropagation(); composer.classList.remove("dragging");
440
+ attachComposerFiles(event.dataTransfer.files);
441
+ });
442
+ $("#workspace-upload-input").onchange = event => uploadWorkspaceFiles(event.target.files || [], event.target.dataset.path || "");
443
+ document.addEventListener("dragover", event => {
444
+ const section = event.target.closest("#workspace-section[data-entry-kind=\"directory\"]");
445
+ if (!section || !event.dataTransfer?.types.includes("Files")) return;
446
+ event.preventDefault(); section.classList.add("dragging");
447
+ });
448
+ document.addEventListener("dragleave", event => { event.target.closest("#workspace-section")?.classList.remove("dragging"); });
449
+ document.addEventListener("drop", event => {
450
+ const section = event.target.closest("#workspace-section[data-entry-kind=\"directory\"]");
451
+ if (!section || !event.dataTransfer?.files.length) return;
452
+ event.preventDefault(); section.classList.remove("dragging");
453
+ uploadWorkspaceFiles(event.dataTransfer.files, section.dataset.path || "");
454
+ });
455
+ $("#permission-toggle").onclick = async () => { if (!state.selectedTask) return; try { const task = await api(`/tasks/${state.selectedId}`, { method: "PATCH", body: JSON.stringify({ yolo: !state.selectedTask.yolo }) }); state.selectedTask = { ...state.selectedTask, ...task }; renderConversation(); toast(task.yolo ? "YOLO 已开启" : "已切换为受控模式"); } catch (error) { toast(error.message); } };
456
+ $("#command-button").onclick = () => { const input = $("#message-input"); if (!input.value.startsWith("/")) input.value = "/"; input.focus(); state.commandIndex = 0; renderCommandPalette(); };
457
+ document.addEventListener("click", event => { const option = event.target.closest("[data-command-index]"); if (!option) return; const command = commandMatches($("#message-input").value)[Number(option.dataset.commandIndex)]; completeCommand(command); });
458
+ // Capture the physical backquote key before xterm can turn it into terminal input.
459
+ document.addEventListener("keydown", event => {
460
+ if (event.code !== "Backquote" || event.shiftKey || event.ctrlKey || event.altKey || event.metaKey || event.repeat) return;
461
+ event.preventDefault();
462
+ event.stopImmediatePropagation();
463
+ toggleTerminalShortcut();
464
+ }, true);
465
+ document.addEventListener("keydown", event => {
466
+ if ((event.ctrlKey || event.metaKey) && !event.altKey && event.key.toLowerCase() === "k") { event.preventDefault(); $("#message-input").focus(); return; }
467
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "n" && !event.shiftKey) { event.preventDefault(); createQuickSession(); }
468
+ const target = event.target;
469
+ const editing = target instanceof HTMLElement && target.closest("input, textarea, select, [contenteditable=\"true\"]");
470
+ if (event.shiftKey && !event.ctrlKey && !event.altKey && !event.metaKey && !editing) {
471
+ if (event.code === "KeyN") { event.preventDefault(); switchSession(1); return; }
472
+ if (event.code === "KeyP") { event.preventDefault(); switchSession(-1); return; }
473
+ if (event.code === "KeyC") { event.preventDefault(); switchToTopSession(); return; }
474
+ }
475
+ if (event.key === "Escape" && ["running", "retrying", "queued"].includes(state.selectedTask?.status) && !$("#panel").classList.contains("open") && !$("#drawer").classList.contains("open") && !$("#terminal-window").classList.contains("open")) { event.preventDefault(); changeSelectedRun("stop"); }
476
+ });
477
+ const mascotDanceSequence = ["ArrowUp", "ArrowUp", "ArrowDown", "ArrowDown", "ArrowLeft", "ArrowRight", "ArrowLeft", "ArrowRight"];
478
+ let mascotDanceIndex = 0;
479
+ document.addEventListener("keydown", event => {
480
+ const target = event.target;
481
+ const editing = target instanceof HTMLElement && target.closest("input, textarea, select, [contenteditable=\"true\"], .ace_editor");
482
+ const terminalOpen = $("#terminal-window")?.classList.contains("open");
483
+ if (editing || terminalOpen || event.repeat || event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) { mascotDanceIndex = 0; return; }
484
+ if (!mascotDanceSequence.includes(event.key)) { mascotDanceIndex = 0; return; }
485
+ mascotDanceIndex = event.key === mascotDanceSequence[mascotDanceIndex] ? mascotDanceIndex + 1 : (event.key === mascotDanceSequence[0] ? 1 : 0);
486
+ if (mascotDanceIndex !== mascotDanceSequence.length) return;
487
+ mascotDanceIndex = 0;
488
+ triggerMascotDance();
489
+ });
490
+ document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") { connectOverviewSocket(); if (state.selectedId && !socketPaused && (!socket || socket.readyState === WebSocket.CLOSED)) connectSocket(state.selectedId, true); if (terminalTaskId && terminalShouldReconnect && !terminalSocket) connectTerminal(terminalTaskId, true); } });
491
+ window.addEventListener("offline", renderConnectionStatus);
492
+ window.addEventListener("online", () => {
493
+ setRealtimeChannel("overview", "reconnecting");
494
+ if (state.selectedId && !socketPaused) setRealtimeChannel("task", "reconnecting");
495
+ if (terminalTaskId && terminalShouldReconnect) setRealtimeChannel("terminal", "reconnecting");
496
+ if (!overviewSocket || overviewSocket.readyState === WebSocket.CLOSED) connectOverviewSocket();
497
+ if (state.selectedId && !socketPaused && (!socket || socket.readyState === WebSocket.CLOSED)) connectSocket(state.selectedId, true);
498
+ if (terminalTaskId && terminalShouldReconnect && !terminalSocket) connectTerminal(terminalTaskId, true);
499
+ heartbeatRealtime();
500
+ });
501
+ setTheme(localStorage.getItem("codex-dashboard-theme") || "dark");
502
+ setInterval(updateTurnElapsed, 1000);
503
+ setInterval(heartbeatRealtime, 10_000);
504
+ renderConnectionStatus();
505
+ showEmptyConversation(); refresh(false);