codex-autorunner 1.1.0__py3-none-any.whl → 1.2.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 (134) hide show
  1. codex_autorunner/agents/opencode/client.py +113 -4
  2. codex_autorunner/agents/opencode/supervisor.py +4 -0
  3. codex_autorunner/agents/registry.py +17 -7
  4. codex_autorunner/bootstrap.py +219 -1
  5. codex_autorunner/core/__init__.py +17 -1
  6. codex_autorunner/core/about_car.py +124 -11
  7. codex_autorunner/core/app_server_threads.py +6 -0
  8. codex_autorunner/core/config.py +238 -3
  9. codex_autorunner/core/context_awareness.py +39 -0
  10. codex_autorunner/core/docs.py +0 -122
  11. codex_autorunner/core/filebox.py +265 -0
  12. codex_autorunner/core/flows/controller.py +71 -1
  13. codex_autorunner/core/flows/reconciler.py +4 -1
  14. codex_autorunner/core/flows/runtime.py +22 -0
  15. codex_autorunner/core/flows/store.py +61 -9
  16. codex_autorunner/core/flows/transition.py +23 -16
  17. codex_autorunner/core/flows/ux_helpers.py +18 -3
  18. codex_autorunner/core/flows/worker_process.py +32 -6
  19. codex_autorunner/core/hub.py +198 -41
  20. codex_autorunner/core/lifecycle_events.py +253 -0
  21. codex_autorunner/core/path_utils.py +2 -1
  22. codex_autorunner/core/pma_audit.py +224 -0
  23. codex_autorunner/core/pma_context.py +683 -0
  24. codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
  25. codex_autorunner/core/pma_lifecycle.py +527 -0
  26. codex_autorunner/core/pma_queue.py +367 -0
  27. codex_autorunner/core/pma_safety.py +221 -0
  28. codex_autorunner/core/pma_state.py +115 -0
  29. codex_autorunner/core/ports/agent_backend.py +2 -5
  30. codex_autorunner/core/ports/run_event.py +1 -4
  31. codex_autorunner/core/prompt.py +0 -80
  32. codex_autorunner/core/prompts.py +56 -172
  33. codex_autorunner/core/redaction.py +0 -4
  34. codex_autorunner/core/review_context.py +11 -9
  35. codex_autorunner/core/runner_controller.py +35 -33
  36. codex_autorunner/core/runner_state.py +147 -0
  37. codex_autorunner/core/runtime.py +829 -0
  38. codex_autorunner/core/sqlite_utils.py +13 -4
  39. codex_autorunner/core/state.py +7 -10
  40. codex_autorunner/core/state_roots.py +5 -0
  41. codex_autorunner/core/templates/__init__.py +39 -0
  42. codex_autorunner/core/templates/git_mirror.py +234 -0
  43. codex_autorunner/core/templates/provenance.py +56 -0
  44. codex_autorunner/core/templates/scan_cache.py +120 -0
  45. codex_autorunner/core/ticket_linter_cli.py +17 -0
  46. codex_autorunner/core/ticket_manager_cli.py +154 -92
  47. codex_autorunner/core/time_utils.py +11 -0
  48. codex_autorunner/core/types.py +18 -0
  49. codex_autorunner/core/utils.py +34 -6
  50. codex_autorunner/flows/review/service.py +23 -25
  51. codex_autorunner/flows/ticket_flow/definition.py +43 -1
  52. codex_autorunner/integrations/agents/__init__.py +2 -0
  53. codex_autorunner/integrations/agents/backend_orchestrator.py +18 -0
  54. codex_autorunner/integrations/agents/codex_backend.py +19 -8
  55. codex_autorunner/integrations/agents/runner.py +3 -8
  56. codex_autorunner/integrations/agents/wiring.py +8 -0
  57. codex_autorunner/integrations/telegram/adapter.py +1 -1
  58. codex_autorunner/integrations/telegram/config.py +1 -1
  59. codex_autorunner/integrations/telegram/doctor.py +228 -6
  60. codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
  61. codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
  62. codex_autorunner/integrations/telegram/handlers/commands/flows.py +346 -58
  63. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
  64. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +202 -45
  65. codex_autorunner/integrations/telegram/handlers/commands_spec.py +18 -7
  66. codex_autorunner/integrations/telegram/handlers/messages.py +34 -3
  67. codex_autorunner/integrations/telegram/helpers.py +1 -3
  68. codex_autorunner/integrations/telegram/runtime.py +9 -4
  69. codex_autorunner/integrations/telegram/service.py +30 -0
  70. codex_autorunner/integrations/telegram/state.py +38 -0
  71. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +10 -4
  72. codex_autorunner/integrations/telegram/transport.py +10 -3
  73. codex_autorunner/integrations/templates/__init__.py +27 -0
  74. codex_autorunner/integrations/templates/scan_agent.py +312 -0
  75. codex_autorunner/server.py +2 -2
  76. codex_autorunner/static/agentControls.js +21 -5
  77. codex_autorunner/static/app.js +115 -11
  78. codex_autorunner/static/archive.js +274 -81
  79. codex_autorunner/static/archiveApi.js +21 -0
  80. codex_autorunner/static/chatUploads.js +137 -0
  81. codex_autorunner/static/constants.js +1 -1
  82. codex_autorunner/static/docChatCore.js +185 -13
  83. codex_autorunner/static/fileChat.js +68 -40
  84. codex_autorunner/static/fileboxUi.js +159 -0
  85. codex_autorunner/static/hub.js +46 -81
  86. codex_autorunner/static/index.html +303 -24
  87. codex_autorunner/static/messages.js +82 -4
  88. codex_autorunner/static/notifications.js +288 -0
  89. codex_autorunner/static/pma.js +1167 -0
  90. codex_autorunner/static/settings.js +3 -0
  91. codex_autorunner/static/streamUtils.js +57 -0
  92. codex_autorunner/static/styles.css +9141 -6742
  93. codex_autorunner/static/templateReposSettings.js +225 -0
  94. codex_autorunner/static/terminalManager.js +22 -3
  95. codex_autorunner/static/ticketChatActions.js +165 -3
  96. codex_autorunner/static/ticketChatStream.js +17 -119
  97. codex_autorunner/static/ticketEditor.js +41 -13
  98. codex_autorunner/static/ticketTemplates.js +798 -0
  99. codex_autorunner/static/tickets.js +69 -19
  100. codex_autorunner/static/turnEvents.js +27 -0
  101. codex_autorunner/static/turnResume.js +33 -0
  102. codex_autorunner/static/utils.js +28 -0
  103. codex_autorunner/static/workspace.js +258 -44
  104. codex_autorunner/static/workspaceFileBrowser.js +6 -4
  105. codex_autorunner/surfaces/cli/cli.py +1465 -155
  106. codex_autorunner/surfaces/cli/pma_cli.py +817 -0
  107. codex_autorunner/surfaces/web/app.py +253 -49
  108. codex_autorunner/surfaces/web/routes/__init__.py +4 -0
  109. codex_autorunner/surfaces/web/routes/analytics.py +29 -22
  110. codex_autorunner/surfaces/web/routes/archive.py +197 -0
  111. codex_autorunner/surfaces/web/routes/file_chat.py +297 -36
  112. codex_autorunner/surfaces/web/routes/filebox.py +227 -0
  113. codex_autorunner/surfaces/web/routes/flows.py +219 -29
  114. codex_autorunner/surfaces/web/routes/messages.py +70 -39
  115. codex_autorunner/surfaces/web/routes/pma.py +1652 -0
  116. codex_autorunner/surfaces/web/routes/repos.py +1 -1
  117. codex_autorunner/surfaces/web/routes/shared.py +0 -3
  118. codex_autorunner/surfaces/web/routes/templates.py +634 -0
  119. codex_autorunner/surfaces/web/runner_manager.py +2 -2
  120. codex_autorunner/surfaces/web/schemas.py +81 -18
  121. codex_autorunner/tickets/agent_pool.py +27 -0
  122. codex_autorunner/tickets/files.py +33 -16
  123. codex_autorunner/tickets/lint.py +50 -0
  124. codex_autorunner/tickets/models.py +3 -0
  125. codex_autorunner/tickets/outbox.py +41 -5
  126. codex_autorunner/tickets/runner.py +350 -69
  127. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/METADATA +15 -19
  128. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/RECORD +132 -101
  129. codex_autorunner/core/adapter_utils.py +0 -21
  130. codex_autorunner/core/engine.py +0 -3302
  131. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/WHEEL +0 -0
  132. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/entry_points.txt +0 -0
  133. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/licenses/LICENSE +0 -0
  134. {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/top_level.txt +0 -0
@@ -1,16 +1,19 @@
1
1
  // GENERATED FILE - do not edit directly. Source: static_src/
2
- import { api, flash, setButtonLoading } 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";
11
12
  import { createSmartRefresh } from "./smartRefresh.js";
12
13
  import { subscribe } from "./bus.js";
13
14
  import { isRepoHealthy } from "./health.js";
15
+ import { loadPendingTurn, savePendingTurn, clearPendingTurn } from "./turnResume.js";
16
+ import { resumeFileChatTurn } from "./turnEvents.js";
14
17
  const state = {
15
18
  target: null,
16
19
  content: "",
@@ -23,6 +26,7 @@ const state = {
23
26
  };
24
27
  const WORKSPACE_CHAT_EVENT_LIMIT = 8;
25
28
  const WORKSPACE_CHAT_EVENT_MAX = 50;
29
+ const WORKSPACE_PENDING_KEY = "car.workspace.pendingTurn";
26
30
  const workspaceChat = createDocChat({
27
31
  idPrefix: "workspace-chat",
28
32
  storage: { keyPrefix: "car-workspace-chat-", maxMessages: 50, version: 1 },
@@ -48,6 +52,7 @@ const workspaceChat = createDocChat({
48
52
  const WORKSPACE_DOC_KINDS = new Set(["active_context", "decisions", "spec"]);
49
53
  const WORKSPACE_REFRESH_REASONS = ["initial", "background", "manual"];
50
54
  let workspaceRefreshCount = 0;
55
+ let currentTurnEventsController = null;
51
56
  function hashString(value) {
52
57
  let hash = 5381;
53
58
  for (let i = 0; i < value.length; i += 1) {
@@ -82,6 +87,13 @@ function els() {
82
87
  statusMobile: document.getElementById("workspace-status-mobile"),
83
88
  uploadBtn: document.getElementById("workspace-upload"),
84
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"),
85
97
  newFolderBtn: document.getElementById("workspace-new-folder"),
86
98
  newFileBtn: document.getElementById("workspace-new-file"),
87
99
  downloadAllBtn: document.getElementById("workspace-download-all"),
@@ -224,15 +236,33 @@ function renderPatch() {
224
236
  function renderChat() {
225
237
  workspaceChat.render();
226
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
+ }
227
249
  function updateDownloadButton() {
228
- const { downloadAllBtn } = els();
229
- if (!downloadAllBtn)
230
- return;
250
+ const { downloadAllBtn, mobileDownload } = els();
231
251
  const currentPath = state.browser?.getCurrentPath() || "";
232
252
  const isRoot = !currentPath;
233
253
  const folderName = currentPath.split("/").pop() || "";
234
- downloadAllBtn.title = isRoot ? "Download all as ZIP" : `Download ${folderName}/ as ZIP`;
235
- 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
+ }
236
266
  }
237
267
  let createMode = null;
238
268
  function listFolderPaths(nodes, base = "") {
@@ -337,7 +367,7 @@ const workspaceTreeRefresh = createSmartRefresh({
337
367
  },
338
368
  onPathChange: () => updateDownloadButton(),
339
369
  onRefresh: () => loadFiles(state.target?.path, "manual"),
340
- onConfirm: (message) => window.workspaceConfirm?.(message) ?? Promise.resolve(confirm(message)),
370
+ onConfirm: (message) => window.workspaceConfirm?.(message) ?? confirmModal(message),
341
371
  });
342
372
  }
343
373
  const defaultPath = payload.defaultPath ?? state.target?.path ?? undefined;
@@ -431,9 +461,12 @@ async function maybeShowGenerate() {
431
461
  catch {
432
462
  state.hasTickets = true;
433
463
  }
434
- const btn = els().generateBtn;
435
- if (btn)
436
- 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);
437
470
  }
438
471
  async function generateTickets() {
439
472
  try {
@@ -451,7 +484,7 @@ async function applyWorkspaceDraft() {
451
484
  try {
452
485
  const isStale = Boolean(state.draft?.is_stale);
453
486
  if (isStale) {
454
- 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?");
455
488
  if (!confirmForce)
456
489
  return;
457
490
  }
@@ -484,6 +517,138 @@ async function discardWorkspaceDraft() {
484
517
  flash(err.message || "Failed to discard draft", "error");
485
518
  }
486
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
+ }
487
652
  async function sendChat() {
488
653
  const { chatInput, chatSend, chatCancel } = els();
489
654
  const message = (chatInput?.value || "").trim();
@@ -498,6 +663,7 @@ async function sendChat() {
498
663
  chatState.error = "";
499
664
  chatState.statusText = "queued";
500
665
  chatState.streamText = "";
666
+ chatState.contextUsagePercent = null;
501
667
  workspaceChat.clearEvents();
502
668
  workspaceChat.addUserMessage(message);
503
669
  renderChat();
@@ -505,6 +671,14 @@ async function sendChat() {
505
671
  chatInput.value = "";
506
672
  chatSend?.setAttribute("disabled", "true");
507
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
+ });
508
682
  const agent = getSelectedAgent();
509
683
  const model = getSelectedModel(agent) || undefined;
510
684
  const reasoning = getSelectedReasoning(agent) || undefined;
@@ -523,43 +697,20 @@ async function sendChat() {
523
697
  workspaceChat.applyAppEvent(event);
524
698
  workspaceChat.renderEvents();
525
699
  },
526
- onUpdate: (update) => {
527
- const hasDraft = update.has_draft ?? update.hasDraft;
528
- if (hasDraft === false) {
529
- chatState.draft = null;
530
- if (typeof update.content === "string") {
531
- state.content = update.content;
532
- const textarea = els().textarea;
533
- if (textarea)
534
- textarea.value = state.content;
535
- }
536
- renderPatch();
537
- }
538
- else if (hasDraft === true || update.patch || update.content) {
539
- state.draft = {
540
- target: target(),
541
- content: update.content || "",
542
- patch: update.patch || "",
543
- agent_message: update.agent_message,
544
- created_at: update.created_at,
545
- base_hash: update.base_hash,
546
- current_hash: update.current_hash,
547
- is_stale: Boolean(update.is_stale),
548
- };
549
- renderPatch();
550
- }
551
- if (update.message || update.agent_message) {
552
- const text = update.message || update.agent_message || "";
553
- if (text)
554
- workspaceChat.addAssistantMessage(text);
555
- }
700
+ onTokenUsage: (percent) => {
701
+ chatState.contextUsagePercent = percent;
556
702
  renderChat();
557
703
  },
704
+ onUpdate: (update) => {
705
+ applyChatUpdate(update);
706
+ maybeStartTurnEventsFromUpdate(update);
707
+ },
558
708
  onError: (msg) => {
559
709
  chatState.status = "error";
560
710
  chatState.error = msg;
561
711
  renderChat();
562
712
  flash(msg, "error");
713
+ clearPendingTurnState();
563
714
  },
564
715
  onInterrupted: (msg) => {
565
716
  chatState.status = "interrupted";
@@ -567,6 +718,7 @@ async function sendChat() {
567
718
  chatState.streamText = "";
568
719
  renderChat();
569
720
  flash(msg, "info");
721
+ clearPendingTurnState();
570
722
  },
571
723
  onDone: () => {
572
724
  if (chatState.streamText) {
@@ -575,8 +727,9 @@ async function sendChat() {
575
727
  }
576
728
  chatState.status = "done";
577
729
  renderChat();
730
+ clearPendingTurnState();
578
731
  },
579
- }, { agent, model, reasoning });
732
+ }, { agent, model, reasoning, clientTurnId });
580
733
  }
581
734
  catch (err) {
582
735
  const msg = err.message || "Chat failed";
@@ -585,6 +738,7 @@ async function sendChat() {
585
738
  chatStateLocal.error = msg;
586
739
  renderChat();
587
740
  flash(msg, "error");
741
+ clearPendingTurnState();
588
742
  }
589
743
  finally {
590
744
  chatSend?.removeAttribute("disabled");
@@ -606,7 +760,9 @@ async function cancelChat() {
606
760
  }
607
761
  chatState.status = "interrupted";
608
762
  chatState.streamText = "";
763
+ chatState.contextUsagePercent = null;
609
764
  renderChat();
765
+ clearPendingTurnState();
610
766
  }
611
767
  async function resetThread() {
612
768
  if (!state.target)
@@ -619,7 +775,9 @@ async function resetThread() {
619
775
  const chatState = workspaceChat.state;
620
776
  chatState.messages = [];
621
777
  chatState.streamText = "";
778
+ chatState.contextUsagePercent = null;
622
779
  workspaceChat.clearEvents();
780
+ clearPendingTurnState();
623
781
  renderChat();
624
782
  flash("New workspace chat thread", "success");
625
783
  }
@@ -645,7 +803,7 @@ async function loadFiles(defaultPath, reason = "manual") {
645
803
  }
646
804
  }
647
805
  export async function initWorkspace() {
648
- 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();
649
807
  if (!document.getElementById("workspace"))
650
808
  return;
651
809
  initAgentControls({
@@ -660,6 +818,7 @@ export async function initWorkspace() {
660
818
  await maybeShowGenerate();
661
819
  await loadFiles(undefined, "initial");
662
820
  workspaceChat.setTarget(target());
821
+ void resumePendingWorkspaceTurn();
663
822
  const reloadEverything = async () => {
664
823
  await loadFiles(state.target?.path, "manual");
665
824
  await reloadWorkspace();
@@ -686,6 +845,54 @@ export async function initWorkspace() {
686
845
  uploadInput.value = "";
687
846
  }
688
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
+ });
689
896
  newFolderBtn?.addEventListener("click", () => openCreateModal("folder"));
690
897
  els().newFileBtn?.addEventListener("click", () => openCreateModal("file"));
691
898
  generateBtn?.addEventListener("click", () => void generateTickets());
@@ -703,6 +910,13 @@ export async function initWorkspace() {
703
910
  void sendChat();
704
911
  }
705
912
  });
913
+ initChatPasteUpload({
914
+ textarea: chatInput,
915
+ basePath: "/api/filebox",
916
+ box: "inbox",
917
+ insertStyle: "both",
918
+ pathPrefix: ".codex-autorunner/filebox",
919
+ });
706
920
  }
707
921
  const { createModal, createClose, createCancel, createSubmit } = els();
708
922
  createClose?.addEventListener("click", () => closeCreateModal());
@@ -1,6 +1,6 @@
1
1
  // GENERATED FILE - do not edit directly. Source: static_src/
2
2
  import { deleteWorkspaceFile, deleteWorkspaceFolder, downloadWorkspaceFile, downloadWorkspaceZip } from "./workspaceApi.js";
3
- import { flash } from "./utils.js";
3
+ import { confirmModal, flash } from "./utils.js";
4
4
  export class WorkspaceFileBrowser {
5
5
  constructor(options) {
6
6
  this.tree = [];
@@ -274,7 +274,9 @@ export class WorkspaceFileBrowser {
274
274
  delBtn.title = "Delete file";
275
275
  delBtn.addEventListener("click", async (evt) => {
276
276
  evt.stopPropagation();
277
- const ok = this.onConfirm ? await this.onConfirm(`Delete ${node.name}?`) : confirm(`Delete ${node.name}?`);
277
+ const ok = this.onConfirm
278
+ ? await this.onConfirm(`Delete ${node.name}?`)
279
+ : await confirmModal(`Delete ${node.name}?`);
278
280
  if (!ok)
279
281
  return;
280
282
  try {
@@ -301,7 +303,7 @@ export class WorkspaceFileBrowser {
301
303
  evt.stopPropagation();
302
304
  const ok = this.onConfirm
303
305
  ? await this.onConfirm(`Delete folder ${node.name}? (must be empty)`)
304
- : confirm(`Delete folder ${node.name}? (must be empty)`);
306
+ : await confirmModal(`Delete folder ${node.name}? (must be empty)`);
305
307
  if (!ok)
306
308
  return;
307
309
  try {
@@ -418,7 +420,7 @@ export class WorkspaceFileBrowser {
418
420
  e.stopPropagation();
419
421
  const ok = this.onConfirm
420
422
  ? await this.onConfirm(`Delete ${node.name}${node.type === "folder" ? " (must be empty)" : ""}?`)
421
- : confirm(`Delete ${node.name}${node.type === "folder" ? " (must be empty)" : ""}?`);
423
+ : await confirmModal(`Delete ${node.name}${node.type === "folder" ? " (must be empty)" : ""}?`);
422
424
  if (!ok)
423
425
  return;
424
426
  try {