codex-autorunner 1.0.0__py3-none-any.whl → 1.2.0__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 (227) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/agents/codex/harness.py +1 -1
  3. codex_autorunner/agents/opencode/client.py +113 -4
  4. codex_autorunner/agents/opencode/constants.py +3 -0
  5. codex_autorunner/agents/opencode/harness.py +6 -1
  6. codex_autorunner/agents/opencode/runtime.py +59 -18
  7. codex_autorunner/agents/opencode/supervisor.py +4 -0
  8. codex_autorunner/agents/registry.py +36 -7
  9. codex_autorunner/bootstrap.py +226 -4
  10. codex_autorunner/cli.py +5 -1174
  11. codex_autorunner/codex_cli.py +20 -84
  12. codex_autorunner/core/__init__.py +20 -0
  13. codex_autorunner/core/about_car.py +119 -1
  14. codex_autorunner/core/app_server_ids.py +59 -0
  15. codex_autorunner/core/app_server_threads.py +17 -2
  16. codex_autorunner/core/app_server_utils.py +165 -0
  17. codex_autorunner/core/archive.py +349 -0
  18. codex_autorunner/core/codex_runner.py +6 -2
  19. codex_autorunner/core/config.py +433 -4
  20. codex_autorunner/core/context_awareness.py +38 -0
  21. codex_autorunner/core/docs.py +0 -122
  22. codex_autorunner/core/drafts.py +58 -4
  23. codex_autorunner/core/exceptions.py +4 -0
  24. codex_autorunner/core/filebox.py +265 -0
  25. codex_autorunner/core/flows/controller.py +96 -2
  26. codex_autorunner/core/flows/models.py +13 -0
  27. codex_autorunner/core/flows/reasons.py +52 -0
  28. codex_autorunner/core/flows/reconciler.py +134 -0
  29. codex_autorunner/core/flows/runtime.py +57 -4
  30. codex_autorunner/core/flows/store.py +142 -7
  31. codex_autorunner/core/flows/transition.py +27 -15
  32. codex_autorunner/core/flows/ux_helpers.py +272 -0
  33. codex_autorunner/core/flows/worker_process.py +32 -6
  34. codex_autorunner/core/git_utils.py +62 -0
  35. codex_autorunner/core/hub.py +291 -20
  36. codex_autorunner/core/lifecycle_events.py +253 -0
  37. codex_autorunner/core/notifications.py +14 -2
  38. codex_autorunner/core/path_utils.py +2 -1
  39. codex_autorunner/core/pma_audit.py +224 -0
  40. codex_autorunner/core/pma_context.py +496 -0
  41. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  42. codex_autorunner/core/pma_lifecycle.py +527 -0
  43. codex_autorunner/core/pma_queue.py +367 -0
  44. codex_autorunner/core/pma_safety.py +221 -0
  45. codex_autorunner/core/pma_state.py +115 -0
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
  50. codex_autorunner/core/prompt.py +0 -80
  51. codex_autorunner/core/prompts.py +56 -172
  52. codex_autorunner/core/redaction.py +0 -4
  53. codex_autorunner/core/review_context.py +11 -9
  54. codex_autorunner/core/runner_controller.py +35 -33
  55. codex_autorunner/core/runner_state.py +147 -0
  56. codex_autorunner/core/runtime.py +829 -0
  57. codex_autorunner/core/sqlite_utils.py +13 -4
  58. codex_autorunner/core/state.py +7 -10
  59. codex_autorunner/core/state_roots.py +62 -0
  60. codex_autorunner/core/supervisor_protocol.py +15 -0
  61. codex_autorunner/core/templates/__init__.py +39 -0
  62. codex_autorunner/core/templates/git_mirror.py +234 -0
  63. codex_autorunner/core/templates/provenance.py +56 -0
  64. codex_autorunner/core/templates/scan_cache.py +120 -0
  65. codex_autorunner/core/text_delta_coalescer.py +54 -0
  66. codex_autorunner/core/ticket_linter_cli.py +218 -0
  67. codex_autorunner/core/ticket_manager_cli.py +494 -0
  68. codex_autorunner/core/time_utils.py +11 -0
  69. codex_autorunner/core/types.py +18 -0
  70. codex_autorunner/core/update.py +4 -5
  71. codex_autorunner/core/update_paths.py +28 -0
  72. codex_autorunner/core/usage.py +164 -12
  73. codex_autorunner/core/utils.py +125 -15
  74. codex_autorunner/flows/review/__init__.py +17 -0
  75. codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
  76. codex_autorunner/flows/ticket_flow/definition.py +52 -3
  77. codex_autorunner/integrations/agents/__init__.py +11 -19
  78. codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
  79. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  80. codex_autorunner/integrations/agents/codex_backend.py +177 -25
  81. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  82. codex_autorunner/integrations/agents/opencode_backend.py +305 -32
  83. codex_autorunner/integrations/agents/runner.py +86 -0
  84. codex_autorunner/integrations/agents/wiring.py +279 -0
  85. codex_autorunner/integrations/app_server/client.py +7 -60
  86. codex_autorunner/integrations/app_server/env.py +2 -107
  87. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  88. codex_autorunner/integrations/telegram/adapter.py +65 -0
  89. codex_autorunner/integrations/telegram/config.py +46 -0
  90. codex_autorunner/integrations/telegram/constants.py +1 -1
  91. codex_autorunner/integrations/telegram/doctor.py +228 -6
  92. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
  93. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  94. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  95. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
  96. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  97. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
  98. codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
  99. codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
  100. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  101. codex_autorunner/integrations/telegram/helpers.py +22 -1
  102. codex_autorunner/integrations/telegram/runtime.py +9 -4
  103. codex_autorunner/integrations/telegram/service.py +45 -10
  104. codex_autorunner/integrations/telegram/state.py +38 -0
  105. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
  106. codex_autorunner/integrations/telegram/transport.py +13 -4
  107. codex_autorunner/integrations/templates/__init__.py +27 -0
  108. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  109. codex_autorunner/routes/__init__.py +37 -76
  110. codex_autorunner/routes/agents.py +2 -137
  111. codex_autorunner/routes/analytics.py +2 -238
  112. codex_autorunner/routes/app_server.py +2 -131
  113. codex_autorunner/routes/base.py +2 -596
  114. codex_autorunner/routes/file_chat.py +4 -833
  115. codex_autorunner/routes/flows.py +4 -977
  116. codex_autorunner/routes/messages.py +4 -456
  117. codex_autorunner/routes/repos.py +2 -196
  118. codex_autorunner/routes/review.py +2 -147
  119. codex_autorunner/routes/sessions.py +2 -175
  120. codex_autorunner/routes/settings.py +2 -168
  121. codex_autorunner/routes/shared.py +2 -275
  122. codex_autorunner/routes/system.py +4 -193
  123. codex_autorunner/routes/usage.py +2 -86
  124. codex_autorunner/routes/voice.py +2 -119
  125. codex_autorunner/routes/workspace.py +2 -270
  126. codex_autorunner/server.py +4 -4
  127. codex_autorunner/static/agentControls.js +61 -16
  128. codex_autorunner/static/app.js +126 -14
  129. codex_autorunner/static/archive.js +826 -0
  130. codex_autorunner/static/archiveApi.js +37 -0
  131. codex_autorunner/static/autoRefresh.js +7 -7
  132. codex_autorunner/static/chatUploads.js +137 -0
  133. codex_autorunner/static/dashboard.js +224 -171
  134. codex_autorunner/static/docChatCore.js +185 -13
  135. codex_autorunner/static/fileChat.js +68 -40
  136. codex_autorunner/static/fileboxUi.js +159 -0
  137. codex_autorunner/static/hub.js +114 -131
  138. codex_autorunner/static/index.html +375 -49
  139. codex_autorunner/static/messages.js +568 -87
  140. codex_autorunner/static/notifications.js +255 -0
  141. codex_autorunner/static/pma.js +1167 -0
  142. codex_autorunner/static/preserve.js +17 -0
  143. codex_autorunner/static/settings.js +128 -6
  144. codex_autorunner/static/smartRefresh.js +52 -0
  145. codex_autorunner/static/streamUtils.js +57 -0
  146. codex_autorunner/static/styles.css +9798 -6143
  147. codex_autorunner/static/tabs.js +152 -11
  148. codex_autorunner/static/templateReposSettings.js +225 -0
  149. codex_autorunner/static/terminal.js +18 -0
  150. codex_autorunner/static/ticketChatActions.js +165 -3
  151. codex_autorunner/static/ticketChatStream.js +17 -119
  152. codex_autorunner/static/ticketEditor.js +137 -15
  153. codex_autorunner/static/ticketTemplates.js +798 -0
  154. codex_autorunner/static/tickets.js +821 -98
  155. codex_autorunner/static/turnEvents.js +27 -0
  156. codex_autorunner/static/turnResume.js +33 -0
  157. codex_autorunner/static/utils.js +39 -0
  158. codex_autorunner/static/workspace.js +389 -82
  159. codex_autorunner/static/workspaceFileBrowser.js +15 -13
  160. codex_autorunner/surfaces/__init__.py +5 -0
  161. codex_autorunner/surfaces/cli/__init__.py +6 -0
  162. codex_autorunner/surfaces/cli/cli.py +2534 -0
  163. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  164. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  165. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  166. codex_autorunner/surfaces/web/__init__.py +1 -0
  167. codex_autorunner/surfaces/web/app.py +2223 -0
  168. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  169. codex_autorunner/surfaces/web/middleware.py +587 -0
  170. codex_autorunner/surfaces/web/pty_session.py +370 -0
  171. codex_autorunner/surfaces/web/review.py +6 -0
  172. codex_autorunner/surfaces/web/routes/__init__.py +82 -0
  173. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  174. codex_autorunner/surfaces/web/routes/analytics.py +284 -0
  175. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  176. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  177. codex_autorunner/surfaces/web/routes/base.py +615 -0
  178. codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
  179. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  180. codex_autorunner/surfaces/web/routes/flows.py +1354 -0
  181. codex_autorunner/surfaces/web/routes/messages.py +490 -0
  182. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  183. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  184. codex_autorunner/surfaces/web/routes/review.py +148 -0
  185. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  186. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  187. codex_autorunner/surfaces/web/routes/shared.py +277 -0
  188. codex_autorunner/surfaces/web/routes/system.py +196 -0
  189. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  190. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  191. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  192. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  193. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  194. codex_autorunner/surfaces/web/schemas.py +469 -0
  195. codex_autorunner/surfaces/web/static_assets.py +490 -0
  196. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  197. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  198. codex_autorunner/tickets/__init__.py +8 -1
  199. codex_autorunner/tickets/agent_pool.py +53 -4
  200. codex_autorunner/tickets/files.py +37 -16
  201. codex_autorunner/tickets/lint.py +50 -0
  202. codex_autorunner/tickets/models.py +6 -1
  203. codex_autorunner/tickets/outbox.py +50 -2
  204. codex_autorunner/tickets/runner.py +396 -57
  205. codex_autorunner/web/__init__.py +5 -1
  206. codex_autorunner/web/app.py +2 -1949
  207. codex_autorunner/web/hub_jobs.py +2 -191
  208. codex_autorunner/web/middleware.py +2 -586
  209. codex_autorunner/web/pty_session.py +2 -369
  210. codex_autorunner/web/runner_manager.py +2 -24
  211. codex_autorunner/web/schemas.py +2 -376
  212. codex_autorunner/web/static_assets.py +4 -441
  213. codex_autorunner/web/static_refresh.py +2 -85
  214. codex_autorunner/web/terminal_sessions.py +2 -77
  215. codex_autorunner/workspace/paths.py +49 -33
  216. codex_autorunner-1.2.0.dist-info/METADATA +150 -0
  217. codex_autorunner-1.2.0.dist-info/RECORD +339 -0
  218. codex_autorunner/core/adapter_utils.py +0 -21
  219. codex_autorunner/core/engine.py +0 -2653
  220. codex_autorunner/core/static_assets.py +0 -55
  221. codex_autorunner-1.0.0.dist-info/METADATA +0 -246
  222. codex_autorunner-1.0.0.dist-info/RECORD +0 -251
  223. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  224. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
  225. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
  226. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
  227. {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,19 @@
1
1
  // GENERATED FILE - do not edit directly. Source: static_src/
2
- import { api, flash } from "./utils.js";
2
+ import { api, confirmModal, flash, setButtonLoading } from "./utils.js";
3
3
  import { initAgentControls, getSelectedAgent, getSelectedModel, getSelectedReasoning } from "./agentControls.js";
4
4
  import { fetchWorkspace, ingestSpecToTickets, listTickets, fetchWorkspaceTree, uploadWorkspaceFiles, downloadWorkspaceZip, createWorkspaceFolder, writeWorkspace, } from "./workspaceApi.js";
5
- import { applyDraft, discardDraft, fetchPendingDraft, sendFileChat, interruptFileChat, } from "./fileChat.js";
5
+ import { applyDraft, discardDraft, fetchPendingDraft, sendFileChat, interruptFileChat, newClientTurnId, streamTurnEvents, } from "./fileChat.js";
6
6
  import { DocEditor } from "./docEditor.js";
7
7
  import { WorkspaceFileBrowser } from "./workspaceFileBrowser.js";
8
8
  import { createDocChat } from "./docChatCore.js";
9
+ import { initChatPasteUpload } from "./chatUploads.js";
9
10
  import { initDocChatVoice } from "./docChatVoice.js";
10
11
  import { renderDiff } from "./diffRenderer.js";
12
+ import { createSmartRefresh } from "./smartRefresh.js";
13
+ import { subscribe } from "./bus.js";
14
+ import { isRepoHealthy } from "./health.js";
15
+ import { loadPendingTurn, savePendingTurn, clearPendingTurn } from "./turnResume.js";
16
+ import { resumeFileChatTurn } from "./turnEvents.js";
11
17
  const state = {
12
18
  target: null,
13
19
  content: "",
@@ -20,6 +26,7 @@ const state = {
20
26
  };
21
27
  const WORKSPACE_CHAT_EVENT_LIMIT = 8;
22
28
  const WORKSPACE_CHAT_EVENT_MAX = 50;
29
+ const WORKSPACE_PENDING_KEY = "car.workspace.pendingTurn";
23
30
  const workspaceChat = createDocChat({
24
31
  idPrefix: "workspace-chat",
25
32
  storage: { keyPrefix: "car-workspace-chat-", maxMessages: 50, version: 1 },
@@ -43,6 +50,34 @@ const workspaceChat = createDocChat({
43
50
  },
44
51
  });
45
52
  const WORKSPACE_DOC_KINDS = new Set(["active_context", "decisions", "spec"]);
53
+ const WORKSPACE_REFRESH_REASONS = ["initial", "background", "manual"];
54
+ let workspaceRefreshCount = 0;
55
+ let currentTurnEventsController = null;
56
+ function hashString(value) {
57
+ let hash = 5381;
58
+ for (let i = 0; i < value.length; i += 1) {
59
+ hash = (hash * 33) ^ value.charCodeAt(i);
60
+ }
61
+ return (hash >>> 0).toString(36);
62
+ }
63
+ function workspaceTreeSignature(nodes) {
64
+ const parts = [];
65
+ const walk = (list) => {
66
+ list.forEach((node) => {
67
+ parts.push([
68
+ node.path || "",
69
+ node.type || "",
70
+ node.is_pinned ? "1" : "0",
71
+ node.modified_at || "",
72
+ node.size ?? "",
73
+ ].join("|"));
74
+ if (node.children?.length)
75
+ walk(node.children);
76
+ });
77
+ };
78
+ walk(nodes || []);
79
+ return parts.join("::");
80
+ }
46
81
  function els() {
47
82
  return {
48
83
  fileList: document.getElementById("workspace-file-list"),
@@ -52,6 +87,13 @@ function els() {
52
87
  statusMobile: document.getElementById("workspace-status-mobile"),
53
88
  uploadBtn: document.getElementById("workspace-upload"),
54
89
  uploadInput: document.getElementById("workspace-upload-input"),
90
+ mobileMenuToggle: document.getElementById("workspace-mobile-menu-toggle"),
91
+ mobileDropdown: document.getElementById("workspace-mobile-dropdown"),
92
+ mobileUpload: document.getElementById("workspace-mobile-upload"),
93
+ mobileNewFolder: document.getElementById("workspace-mobile-new-folder"),
94
+ mobileNewFile: document.getElementById("workspace-mobile-new-file"),
95
+ mobileDownload: document.getElementById("workspace-mobile-download"),
96
+ mobileGenerate: document.getElementById("workspace-mobile-generate"),
55
97
  newFolderBtn: document.getElementById("workspace-new-folder"),
56
98
  newFileBtn: document.getElementById("workspace-new-file"),
57
99
  downloadAllBtn: document.getElementById("workspace-download-all"),
@@ -143,6 +185,13 @@ function setStatus(text) {
143
185
  if (statusMobile)
144
186
  statusMobile.textContent = text;
145
187
  }
188
+ function setWorkspaceRefreshing(active) {
189
+ const { reloadBtn, reloadBtnMobile } = els();
190
+ workspaceRefreshCount = Math.max(0, workspaceRefreshCount + (active ? 1 : -1));
191
+ const isRefreshing = workspaceRefreshCount > 0;
192
+ setButtonLoading(reloadBtn, isRefreshing);
193
+ setButtonLoading(reloadBtnMobile, isRefreshing);
194
+ }
146
195
  function renderPatch() {
147
196
  const { patchMain, patchBody, patchSummary, patchMeta, textarea, saveBtn, reloadBtn } = els();
148
197
  if (!patchMain || !patchBody)
@@ -187,15 +236,33 @@ function renderPatch() {
187
236
  function renderChat() {
188
237
  workspaceChat.render();
189
238
  }
239
+ function closeMobileMenu() {
240
+ const dropdown = els().mobileDropdown;
241
+ if (dropdown)
242
+ dropdown.classList.add("hidden");
243
+ }
244
+ function toggleMobileMenu() {
245
+ const dropdown = els().mobileDropdown;
246
+ if (dropdown)
247
+ dropdown.classList.toggle("hidden");
248
+ }
190
249
  function updateDownloadButton() {
191
- const { downloadAllBtn } = els();
192
- if (!downloadAllBtn)
193
- return;
250
+ const { downloadAllBtn, mobileDownload } = els();
194
251
  const currentPath = state.browser?.getCurrentPath() || "";
195
252
  const isRoot = !currentPath;
196
253
  const folderName = currentPath.split("/").pop() || "";
197
- downloadAllBtn.title = isRoot ? "Download all as ZIP" : `Download ${folderName}/ as ZIP`;
198
- downloadAllBtn.onclick = () => downloadWorkspaceZip(isRoot ? undefined : currentPath);
254
+ const download = () => downloadWorkspaceZip(isRoot ? undefined : currentPath);
255
+ if (downloadAllBtn) {
256
+ downloadAllBtn.title = isRoot ? "Download all as ZIP" : `Download ${folderName}/ as ZIP`;
257
+ downloadAllBtn.onclick = download;
258
+ }
259
+ if (mobileDownload) {
260
+ mobileDownload.textContent = isRoot ? "Download ZIP (all)" : `Download ${folderName || "folder"}`;
261
+ mobileDownload.onclick = () => {
262
+ closeMobileMenu();
263
+ download();
264
+ };
265
+ }
199
266
  }
200
267
  let createMode = null;
201
268
  function listFolderPaths(nodes, base = "") {
@@ -272,7 +339,7 @@ async function handleCreateSubmit() {
272
339
  flash("File created", "success");
273
340
  }
274
341
  closeCreateModal();
275
- await loadFiles(createMode === "file" ? path : state.target?.path || undefined);
342
+ await loadFiles(createMode === "file" ? path : state.target?.path || undefined, "manual");
276
343
  if (createMode === "file") {
277
344
  state.browser?.select(path);
278
345
  }
@@ -281,12 +348,45 @@ async function handleCreateSubmit() {
281
348
  flash(err.message || "Failed to create item", "error");
282
349
  }
283
350
  }
284
- async function loadWorkspaceFile(path) {
285
- state.loading = true;
286
- setStatus("Loading…");
287
- try {
288
- const text = await readWorkspaceContent(path);
289
- state.content = text;
351
+ const workspaceTreeRefresh = createSmartRefresh({
352
+ getSignature: (payload) => workspaceTreeSignature(payload.tree || []),
353
+ render: (payload) => {
354
+ state.files = payload.tree;
355
+ const { fileList, fileSelect, breadcrumbs } = els();
356
+ if (!fileList)
357
+ return;
358
+ if (!state.browser) {
359
+ state.browser = new WorkspaceFileBrowser({
360
+ container: fileList,
361
+ selectEl: fileSelect,
362
+ breadcrumbsEl: breadcrumbs,
363
+ onSelect: (file) => {
364
+ state.target = { path: file.path, isPinned: Boolean(file.is_pinned) };
365
+ workspaceChat.setTarget(target());
366
+ void refreshWorkspaceFile(file.path, "manual");
367
+ },
368
+ onPathChange: () => updateDownloadButton(),
369
+ onRefresh: () => loadFiles(state.target?.path, "manual"),
370
+ onConfirm: (message) => window.workspaceConfirm?.(message) ?? confirmModal(message),
371
+ });
372
+ }
373
+ const defaultPath = payload.defaultPath ?? state.target?.path ?? undefined;
374
+ state.browser.setTree(payload.tree, defaultPath || undefined);
375
+ updateDownloadButton();
376
+ if (state.target) {
377
+ workspaceChat.setTarget(target());
378
+ }
379
+ },
380
+ onSkip: () => {
381
+ updateDownloadButton();
382
+ },
383
+ });
384
+ const workspaceContentRefresh = createSmartRefresh({
385
+ getSignature: (payload) => `${payload.path}::${hashString(payload.content || "")}`,
386
+ render: async (payload, ctx) => {
387
+ if (payload.path !== state.target?.path)
388
+ return;
389
+ state.content = payload.content;
290
390
  if (state.docEditor) {
291
391
  state.docEditor.destroy();
292
392
  }
@@ -298,9 +398,9 @@ async function loadWorkspaceFile(path) {
298
398
  textarea,
299
399
  saveButton: saveBtn,
300
400
  statusEl: status,
301
- onLoad: async () => text,
401
+ onLoad: async () => payload.content,
302
402
  onSave: async (content) => {
303
- const saved = await writeWorkspaceContent(path, content);
403
+ const saved = await writeWorkspaceContent(payload.path, content);
304
404
  state.content = saved;
305
405
  if (saved !== content) {
306
406
  textarea.value = saved;
@@ -309,7 +409,25 @@ async function loadWorkspaceFile(path) {
309
409
  });
310
410
  await loadPendingDraft();
311
411
  renderPatch();
312
- setStatus("Loaded");
412
+ if (ctx.reason !== "background") {
413
+ setStatus("Loaded");
414
+ }
415
+ },
416
+ });
417
+ async function refreshWorkspaceFile(path, reason = "manual") {
418
+ if (!WORKSPACE_REFRESH_REASONS.includes(reason)) {
419
+ reason = "manual";
420
+ }
421
+ const isInitial = reason === "initial";
422
+ if (isInitial) {
423
+ state.loading = true;
424
+ setStatus("Loading…");
425
+ }
426
+ else {
427
+ setWorkspaceRefreshing(true);
428
+ }
429
+ try {
430
+ await workspaceContentRefresh.refresh(async () => ({ path, content: await readWorkspaceContent(path) }), { reason });
313
431
  }
314
432
  catch (err) {
315
433
  const message = err.message || "Failed to load workspace file";
@@ -318,6 +436,9 @@ async function loadWorkspaceFile(path) {
318
436
  }
319
437
  finally {
320
438
  state.loading = false;
439
+ if (!isInitial) {
440
+ setWorkspaceRefreshing(false);
441
+ }
321
442
  }
322
443
  }
323
444
  async function loadPendingDraft() {
@@ -327,7 +448,7 @@ async function loadPendingDraft() {
327
448
  async function reloadWorkspace() {
328
449
  if (!state.target)
329
450
  return;
330
- await loadWorkspaceFile(state.target.path);
451
+ await refreshWorkspaceFile(state.target.path, "manual");
331
452
  }
332
453
  async function maybeShowGenerate() {
333
454
  try {
@@ -340,9 +461,12 @@ async function maybeShowGenerate() {
340
461
  catch {
341
462
  state.hasTickets = true;
342
463
  }
343
- const btn = els().generateBtn;
344
- if (btn)
345
- btn.classList.toggle("hidden", state.hasTickets);
464
+ const { generateBtn, mobileGenerate } = els();
465
+ const hidden = state.hasTickets;
466
+ if (generateBtn)
467
+ generateBtn.classList.toggle("hidden", hidden);
468
+ if (mobileGenerate)
469
+ mobileGenerate.classList.toggle("hidden", hidden);
346
470
  }
347
471
  async function generateTickets() {
348
472
  try {
@@ -360,7 +484,7 @@ async function applyWorkspaceDraft() {
360
484
  try {
361
485
  const isStale = Boolean(state.draft?.is_stale);
362
486
  if (isStale) {
363
- const confirmForce = window.confirm("This draft is stale because the file changed after it was created. Force apply anyway?");
487
+ const confirmForce = await confirmModal("This draft is stale because the file changed after it was created. Force apply anyway?");
364
488
  if (!confirmForce)
365
489
  return;
366
490
  }
@@ -393,6 +517,138 @@ async function discardWorkspaceDraft() {
393
517
  flash(err.message || "Failed to discard draft", "error");
394
518
  }
395
519
  }
520
+ function clearTurnEventsStream() {
521
+ if (currentTurnEventsController) {
522
+ try {
523
+ currentTurnEventsController.abort();
524
+ }
525
+ catch {
526
+ // ignore
527
+ }
528
+ currentTurnEventsController = null;
529
+ }
530
+ }
531
+ function clearPendingTurnState() {
532
+ clearTurnEventsStream();
533
+ clearPendingTurn(WORKSPACE_PENDING_KEY);
534
+ }
535
+ function maybeStartTurnEventsFromUpdate(update) {
536
+ const meta = update;
537
+ const threadId = typeof meta.thread_id === "string" ? meta.thread_id : "";
538
+ const turnId = typeof meta.turn_id === "string" ? meta.turn_id : "";
539
+ const agent = typeof meta.agent === "string" ? meta.agent : undefined;
540
+ if (!threadId || !turnId)
541
+ return;
542
+ clearTurnEventsStream();
543
+ currentTurnEventsController = streamTurnEvents({ agent, threadId, turnId }, {
544
+ onEvent: (event) => {
545
+ workspaceChat.applyAppEvent(event);
546
+ workspaceChat.renderEvents();
547
+ workspaceChat.render();
548
+ },
549
+ });
550
+ }
551
+ function applyChatUpdate(update) {
552
+ const hasDraft = update.has_draft ?? update.hasDraft;
553
+ if (hasDraft === false) {
554
+ state.draft = null;
555
+ if (typeof update.content === "string") {
556
+ state.content = update.content;
557
+ const textarea = els().textarea;
558
+ if (textarea)
559
+ textarea.value = state.content;
560
+ }
561
+ renderPatch();
562
+ }
563
+ else if (hasDraft === true || update.patch || update.content) {
564
+ state.draft = {
565
+ target: target(),
566
+ content: update.content || "",
567
+ patch: update.patch || "",
568
+ agent_message: update.agent_message,
569
+ created_at: update.created_at,
570
+ base_hash: update.base_hash,
571
+ current_hash: update.current_hash,
572
+ is_stale: Boolean(update.is_stale),
573
+ };
574
+ renderPatch();
575
+ }
576
+ if (update.message || update.agent_message) {
577
+ const text = update.message || update.agent_message || "";
578
+ if (text)
579
+ workspaceChat.addAssistantMessage(text);
580
+ }
581
+ workspaceChat.render();
582
+ }
583
+ function applyFinalResult(result) {
584
+ const chatState = workspaceChat.state;
585
+ const status = String(result.status || "");
586
+ if (status === "ok") {
587
+ applyChatUpdate(result);
588
+ chatState.status = "done";
589
+ chatState.error = "";
590
+ chatState.streamText = "";
591
+ clearPendingTurnState();
592
+ renderChat();
593
+ return;
594
+ }
595
+ if (status === "error") {
596
+ const detail = String(result.detail || "Chat failed");
597
+ chatState.status = "error";
598
+ chatState.error = detail;
599
+ renderChat();
600
+ flash(detail, "error");
601
+ clearPendingTurnState();
602
+ return;
603
+ }
604
+ if (status === "interrupted") {
605
+ chatState.status = "interrupted";
606
+ chatState.error = "";
607
+ chatState.streamText = "";
608
+ renderChat();
609
+ clearPendingTurnState();
610
+ }
611
+ }
612
+ async function resumePendingWorkspaceTurn() {
613
+ const pending = loadPendingTurn(WORKSPACE_PENDING_KEY);
614
+ if (!pending)
615
+ return;
616
+ const chatState = workspaceChat.state;
617
+ chatState.status = "running";
618
+ chatState.statusText = "Recovering previous turn…";
619
+ workspaceChat.render();
620
+ workspaceChat.renderMessages();
621
+ try {
622
+ const outcome = await resumeFileChatTurn(pending.clientTurnId, {
623
+ onEvent: (event) => {
624
+ workspaceChat.applyAppEvent(event);
625
+ workspaceChat.renderEvents();
626
+ workspaceChat.render();
627
+ },
628
+ onResult: (result) => applyFinalResult(result),
629
+ onError: (msg) => {
630
+ chatState.statusText = msg;
631
+ renderChat();
632
+ },
633
+ });
634
+ currentTurnEventsController = outcome.controller;
635
+ if (outcome.lastResult && outcome.lastResult.status) {
636
+ applyFinalResult(outcome.lastResult);
637
+ return;
638
+ }
639
+ // If still running but no event stream yet, poll again shortly.
640
+ if (!outcome.controller) {
641
+ window.setTimeout(() => {
642
+ void resumePendingWorkspaceTurn();
643
+ }, 1000);
644
+ }
645
+ }
646
+ catch (err) {
647
+ const msg = err.message || "Failed to resume turn";
648
+ chatState.statusText = msg;
649
+ renderChat();
650
+ }
651
+ }
396
652
  async function sendChat() {
397
653
  const { chatInput, chatSend, chatCancel } = els();
398
654
  const message = (chatInput?.value || "").trim();
@@ -407,6 +663,7 @@ async function sendChat() {
407
663
  chatState.error = "";
408
664
  chatState.statusText = "queued";
409
665
  chatState.streamText = "";
666
+ chatState.contextUsagePercent = null;
410
667
  workspaceChat.clearEvents();
411
668
  workspaceChat.addUserMessage(message);
412
669
  renderChat();
@@ -414,6 +671,14 @@ async function sendChat() {
414
671
  chatInput.value = "";
415
672
  chatSend?.setAttribute("disabled", "true");
416
673
  chatCancel?.classList.remove("hidden");
674
+ clearTurnEventsStream();
675
+ const clientTurnId = newClientTurnId("workspace");
676
+ savePendingTurn(WORKSPACE_PENDING_KEY, {
677
+ clientTurnId,
678
+ message,
679
+ startedAtMs: Date.now(),
680
+ target: target(),
681
+ });
417
682
  const agent = getSelectedAgent();
418
683
  const model = getSelectedModel(agent) || undefined;
419
684
  const reasoning = getSelectedReasoning(agent) || undefined;
@@ -432,43 +697,20 @@ async function sendChat() {
432
697
  workspaceChat.applyAppEvent(event);
433
698
  workspaceChat.renderEvents();
434
699
  },
435
- onUpdate: (update) => {
436
- const hasDraft = update.has_draft ?? update.hasDraft;
437
- if (hasDraft === false) {
438
- chatState.draft = null;
439
- if (typeof update.content === "string") {
440
- state.content = update.content;
441
- const textarea = els().textarea;
442
- if (textarea)
443
- textarea.value = state.content;
444
- }
445
- renderPatch();
446
- }
447
- else if (hasDraft === true || update.patch || update.content) {
448
- state.draft = {
449
- target: target(),
450
- content: update.content || "",
451
- patch: update.patch || "",
452
- agent_message: update.agent_message,
453
- created_at: update.created_at,
454
- base_hash: update.base_hash,
455
- current_hash: update.current_hash,
456
- is_stale: Boolean(update.is_stale),
457
- };
458
- renderPatch();
459
- }
460
- if (update.message || update.agent_message) {
461
- const text = update.message || update.agent_message || "";
462
- if (text)
463
- workspaceChat.addAssistantMessage(text);
464
- }
700
+ onTokenUsage: (percent) => {
701
+ chatState.contextUsagePercent = percent;
465
702
  renderChat();
466
703
  },
704
+ onUpdate: (update) => {
705
+ applyChatUpdate(update);
706
+ maybeStartTurnEventsFromUpdate(update);
707
+ },
467
708
  onError: (msg) => {
468
709
  chatState.status = "error";
469
710
  chatState.error = msg;
470
711
  renderChat();
471
712
  flash(msg, "error");
713
+ clearPendingTurnState();
472
714
  },
473
715
  onInterrupted: (msg) => {
474
716
  chatState.status = "interrupted";
@@ -476,6 +718,7 @@ async function sendChat() {
476
718
  chatState.streamText = "";
477
719
  renderChat();
478
720
  flash(msg, "info");
721
+ clearPendingTurnState();
479
722
  },
480
723
  onDone: () => {
481
724
  if (chatState.streamText) {
@@ -484,8 +727,9 @@ async function sendChat() {
484
727
  }
485
728
  chatState.status = "done";
486
729
  renderChat();
730
+ clearPendingTurnState();
487
731
  },
488
- }, { agent, model, reasoning });
732
+ }, { agent, model, reasoning, clientTurnId });
489
733
  }
490
734
  catch (err) {
491
735
  const msg = err.message || "Chat failed";
@@ -494,6 +738,7 @@ async function sendChat() {
494
738
  chatStateLocal.error = msg;
495
739
  renderChat();
496
740
  flash(msg, "error");
741
+ clearPendingTurnState();
497
742
  }
498
743
  finally {
499
744
  chatSend?.removeAttribute("disabled");
@@ -515,7 +760,9 @@ async function cancelChat() {
515
760
  }
516
761
  chatState.status = "interrupted";
517
762
  chatState.streamText = "";
763
+ chatState.contextUsagePercent = null;
518
764
  renderChat();
765
+ clearPendingTurnState();
519
766
  }
520
767
  async function resetThread() {
521
768
  if (!state.target)
@@ -528,7 +775,9 @@ async function resetThread() {
528
775
  const chatState = workspaceChat.state;
529
776
  chatState.messages = [];
530
777
  chatState.streamText = "";
778
+ chatState.contextUsagePercent = null;
531
779
  workspaceChat.clearEvents();
780
+ clearPendingTurnState();
532
781
  renderChat();
533
782
  flash("New workspace chat thread", "success");
534
783
  }
@@ -536,36 +785,25 @@ async function resetThread() {
536
785
  flash(err.message || "Failed to reset thread", "error");
537
786
  }
538
787
  }
539
- async function loadFiles(defaultPath) {
540
- const tree = await fetchWorkspaceTree();
541
- state.files = tree;
542
- const { fileList, fileSelect, breadcrumbs } = els();
543
- if (!fileList)
544
- return;
545
- if (!state.browser) {
546
- state.browser = new WorkspaceFileBrowser({
547
- container: fileList,
548
- selectEl: fileSelect,
549
- breadcrumbsEl: breadcrumbs,
550
- onSelect: (file) => {
551
- state.target = { path: file.path, isPinned: Boolean(file.is_pinned) };
552
- workspaceChat.setTarget(target());
553
- void loadWorkspaceFile(file.path);
554
- },
555
- onPathChange: () => updateDownloadButton(),
556
- onRefresh: () => loadFiles(state.target?.path),
557
- onConfirm: (message) => window.workspaceConfirm?.(message) ??
558
- Promise.resolve(confirm(message)),
559
- });
788
+ async function loadFiles(defaultPath, reason = "manual") {
789
+ if (!WORKSPACE_REFRESH_REASONS.includes(reason)) {
790
+ reason = "manual";
791
+ }
792
+ const isInitial = reason === "initial";
793
+ if (!isInitial) {
794
+ setWorkspaceRefreshing(true);
560
795
  }
561
- state.browser.setTree(tree, defaultPath || state.target?.path || undefined);
562
- updateDownloadButton();
563
- if (state.target) {
564
- workspaceChat.setTarget(target());
796
+ try {
797
+ await workspaceTreeRefresh.refresh(async () => ({ tree: await fetchWorkspaceTree(), defaultPath }), { reason });
798
+ }
799
+ finally {
800
+ if (!isInitial) {
801
+ setWorkspaceRefreshing(false);
802
+ }
565
803
  }
566
804
  }
567
805
  export async function initWorkspace() {
568
- const { generateBtn, uploadBtn, uploadInput, newFolderBtn, saveBtn, saveBtnMobile, reloadBtn, reloadBtnMobile, patchApply, patchDiscard, patchReload, chatSend, chatCancel, chatNewThread, } = els();
806
+ const { generateBtn, uploadBtn, uploadInput, mobileMenuToggle, mobileDropdown, mobileUpload, mobileNewFolder, mobileNewFile, mobileDownload, mobileGenerate, newFolderBtn, saveBtn, saveBtnMobile, reloadBtn, reloadBtnMobile, patchApply, patchDiscard, patchReload, chatSend, chatCancel, chatNewThread, } = els();
569
807
  if (!document.getElementById("workspace"))
570
808
  return;
571
809
  initAgentControls({
@@ -578,10 +816,11 @@ export async function initWorkspace() {
578
816
  inputId: "workspace-chat-input",
579
817
  });
580
818
  await maybeShowGenerate();
581
- await loadFiles();
819
+ await loadFiles(undefined, "initial");
582
820
  workspaceChat.setTarget(target());
821
+ void resumePendingWorkspaceTurn();
583
822
  const reloadEverything = async () => {
584
- await loadFiles(state.target?.path);
823
+ await loadFiles(state.target?.path, "manual");
585
824
  await reloadWorkspace();
586
825
  };
587
826
  saveBtn?.addEventListener("click", () => void state.docEditor?.save(true));
@@ -597,7 +836,7 @@ export async function initWorkspace() {
597
836
  try {
598
837
  await uploadWorkspaceFiles(files, subdir || undefined);
599
838
  flash(`Uploaded ${files.length} file${files.length === 1 ? "" : "s"}`, "success");
600
- await loadFiles(state.target?.path);
839
+ await loadFiles(state.target?.path, "manual");
601
840
  }
602
841
  catch (err) {
603
842
  flash(err.message || "Upload failed", "error");
@@ -606,6 +845,54 @@ export async function initWorkspace() {
606
845
  uploadInput.value = "";
607
846
  }
608
847
  });
848
+ // Mobile action sheet
849
+ const handleMobileToggle = (evt) => {
850
+ evt.preventDefault();
851
+ evt.stopPropagation();
852
+ toggleMobileMenu();
853
+ };
854
+ mobileMenuToggle?.addEventListener("pointerdown", handleMobileToggle);
855
+ mobileMenuToggle?.addEventListener("click", (evt) => {
856
+ evt.preventDefault(); // swallow synthetic click after pointerdown
857
+ });
858
+ mobileMenuToggle?.addEventListener("keydown", (evt) => {
859
+ if (evt.key === "Enter" || evt.key === " ") {
860
+ handleMobileToggle(evt);
861
+ }
862
+ });
863
+ document.addEventListener("pointerdown", (evt) => {
864
+ if (!mobileDropdown || mobileDropdown.classList.contains("hidden"))
865
+ return;
866
+ if (evt.target instanceof Node && mobileDropdown.contains(evt.target))
867
+ return;
868
+ closeMobileMenu();
869
+ });
870
+ document.addEventListener("keydown", (evt) => {
871
+ if (evt.key === "Escape" && mobileDropdown && !mobileDropdown.classList.contains("hidden")) {
872
+ closeMobileMenu();
873
+ }
874
+ });
875
+ mobileUpload?.addEventListener("click", () => {
876
+ closeMobileMenu();
877
+ uploadInput?.click();
878
+ });
879
+ mobileNewFolder?.addEventListener("click", () => {
880
+ closeMobileMenu();
881
+ openCreateModal("folder");
882
+ });
883
+ mobileNewFile?.addEventListener("click", () => {
884
+ closeMobileMenu();
885
+ openCreateModal("file");
886
+ });
887
+ mobileDownload?.addEventListener("click", () => {
888
+ closeMobileMenu();
889
+ const currentPath = state.browser?.getCurrentPath() || "";
890
+ downloadWorkspaceZip(currentPath || undefined);
891
+ });
892
+ mobileGenerate?.addEventListener("click", () => {
893
+ closeMobileMenu();
894
+ void generateTickets();
895
+ });
609
896
  newFolderBtn?.addEventListener("click", () => openCreateModal("folder"));
610
897
  els().newFileBtn?.addEventListener("click", () => openCreateModal("file"));
611
898
  generateBtn?.addEventListener("click", () => void generateTickets());
@@ -623,6 +910,13 @@ export async function initWorkspace() {
623
910
  void sendChat();
624
911
  }
625
912
  });
913
+ initChatPasteUpload({
914
+ textarea: chatInput,
915
+ basePath: "/api/filebox",
916
+ box: "inbox",
917
+ insertStyle: "both",
918
+ pathPrefix: ".codex-autorunner/filebox",
919
+ });
626
920
  }
627
921
  const { createModal, createClose, createCancel, createSubmit } = els();
628
922
  createClose?.addEventListener("click", () => closeCreateModal());
@@ -669,4 +963,17 @@ export async function initWorkspace() {
669
963
  if (evt.target === confirmModal)
670
964
  closeConfirm(false);
671
965
  });
966
+ subscribe("repo:health", (payload) => {
967
+ const status = payload?.status || "";
968
+ if (status !== "ok" && status !== "degraded")
969
+ return;
970
+ if (!isRepoHealthy())
971
+ return;
972
+ void loadFiles(state.target?.path, "background");
973
+ const textarea = els().textarea;
974
+ const hasLocalEdits = textarea ? textarea.value !== state.content : false;
975
+ if (state.target && !state.draft && !hasLocalEdits) {
976
+ void refreshWorkspaceFile(state.target.path, "background");
977
+ }
978
+ });
672
979
  }