codex-autorunner 0.1.2__py3-none-any.whl → 1.1.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 (276) hide show
  1. codex_autorunner/__init__.py +12 -1
  2. codex_autorunner/__main__.py +4 -0
  3. codex_autorunner/agents/codex/harness.py +1 -1
  4. codex_autorunner/agents/opencode/client.py +68 -35
  5. codex_autorunner/agents/opencode/constants.py +3 -0
  6. codex_autorunner/agents/opencode/harness.py +6 -1
  7. codex_autorunner/agents/opencode/logging.py +21 -5
  8. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  9. codex_autorunner/agents/opencode/runtime.py +176 -47
  10. codex_autorunner/agents/opencode/supervisor.py +36 -48
  11. codex_autorunner/agents/registry.py +155 -8
  12. codex_autorunner/api.py +25 -0
  13. codex_autorunner/bootstrap.py +22 -37
  14. codex_autorunner/cli.py +5 -1156
  15. codex_autorunner/codex_cli.py +20 -84
  16. codex_autorunner/core/__init__.py +4 -0
  17. codex_autorunner/core/about_car.py +49 -32
  18. codex_autorunner/core/adapter_utils.py +21 -0
  19. codex_autorunner/core/app_server_ids.py +59 -0
  20. codex_autorunner/core/app_server_logging.py +7 -3
  21. codex_autorunner/core/app_server_prompts.py +27 -260
  22. codex_autorunner/core/app_server_threads.py +26 -28
  23. codex_autorunner/core/app_server_utils.py +165 -0
  24. codex_autorunner/core/archive.py +349 -0
  25. codex_autorunner/core/codex_runner.py +12 -2
  26. codex_autorunner/core/config.py +587 -103
  27. codex_autorunner/core/docs.py +10 -2
  28. codex_autorunner/core/drafts.py +136 -0
  29. codex_autorunner/core/engine.py +1531 -866
  30. codex_autorunner/core/exceptions.py +4 -0
  31. codex_autorunner/core/flows/__init__.py +25 -0
  32. codex_autorunner/core/flows/controller.py +202 -0
  33. codex_autorunner/core/flows/definition.py +82 -0
  34. codex_autorunner/core/flows/models.py +88 -0
  35. codex_autorunner/core/flows/reasons.py +52 -0
  36. codex_autorunner/core/flows/reconciler.py +131 -0
  37. codex_autorunner/core/flows/runtime.py +382 -0
  38. codex_autorunner/core/flows/store.py +568 -0
  39. codex_autorunner/core/flows/transition.py +138 -0
  40. codex_autorunner/core/flows/ux_helpers.py +257 -0
  41. codex_autorunner/core/flows/worker_process.py +242 -0
  42. codex_autorunner/core/git_utils.py +62 -0
  43. codex_autorunner/core/hub.py +136 -16
  44. codex_autorunner/core/locks.py +4 -0
  45. codex_autorunner/core/notifications.py +14 -2
  46. codex_autorunner/core/ports/__init__.py +28 -0
  47. codex_autorunner/core/ports/agent_backend.py +150 -0
  48. codex_autorunner/core/ports/backend_orchestrator.py +41 -0
  49. codex_autorunner/core/ports/run_event.py +91 -0
  50. codex_autorunner/core/prompt.py +15 -7
  51. codex_autorunner/core/redaction.py +29 -0
  52. codex_autorunner/core/review_context.py +5 -8
  53. codex_autorunner/core/run_index.py +6 -0
  54. codex_autorunner/core/runner_process.py +5 -2
  55. codex_autorunner/core/state.py +0 -88
  56. codex_autorunner/core/state_roots.py +57 -0
  57. codex_autorunner/core/supervisor_protocol.py +15 -0
  58. codex_autorunner/core/supervisor_utils.py +67 -0
  59. codex_autorunner/core/text_delta_coalescer.py +54 -0
  60. codex_autorunner/core/ticket_linter_cli.py +201 -0
  61. codex_autorunner/core/ticket_manager_cli.py +432 -0
  62. codex_autorunner/core/update.py +24 -16
  63. codex_autorunner/core/update_paths.py +28 -0
  64. codex_autorunner/core/update_runner.py +2 -0
  65. codex_autorunner/core/usage.py +164 -12
  66. codex_autorunner/core/utils.py +120 -11
  67. codex_autorunner/discovery.py +2 -4
  68. codex_autorunner/flows/review/__init__.py +17 -0
  69. codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
  70. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  71. codex_autorunner/flows/ticket_flow/definition.py +98 -0
  72. codex_autorunner/integrations/agents/__init__.py +17 -0
  73. codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
  74. codex_autorunner/integrations/agents/codex_adapter.py +90 -0
  75. codex_autorunner/integrations/agents/codex_backend.py +448 -0
  76. codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
  77. codex_autorunner/integrations/agents/opencode_backend.py +598 -0
  78. codex_autorunner/integrations/agents/runner.py +91 -0
  79. codex_autorunner/integrations/agents/wiring.py +271 -0
  80. codex_autorunner/integrations/app_server/client.py +583 -152
  81. codex_autorunner/integrations/app_server/env.py +2 -107
  82. codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
  83. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  84. codex_autorunner/integrations/telegram/adapter.py +204 -165
  85. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  86. codex_autorunner/integrations/telegram/config.py +221 -0
  87. codex_autorunner/integrations/telegram/constants.py +17 -2
  88. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  89. codex_autorunner/integrations/telegram/doctor.py +47 -0
  90. codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
  91. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  92. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  93. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  94. codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
  95. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  96. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  97. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  98. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
  99. codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
  100. codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
  101. codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
  102. codex_autorunner/integrations/telegram/helpers.py +111 -16
  103. codex_autorunner/integrations/telegram/outbox.py +208 -37
  104. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  105. codex_autorunner/integrations/telegram/service.py +221 -42
  106. codex_autorunner/integrations/telegram/state.py +100 -2
  107. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
  108. codex_autorunner/integrations/telegram/transport.py +39 -4
  109. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  110. codex_autorunner/manifest.py +2 -0
  111. codex_autorunner/plugin_api.py +22 -0
  112. codex_autorunner/routes/__init__.py +37 -67
  113. codex_autorunner/routes/agents.py +2 -137
  114. codex_autorunner/routes/analytics.py +3 -0
  115. codex_autorunner/routes/app_server.py +2 -131
  116. codex_autorunner/routes/base.py +2 -624
  117. codex_autorunner/routes/file_chat.py +7 -0
  118. codex_autorunner/routes/flows.py +7 -0
  119. codex_autorunner/routes/messages.py +7 -0
  120. codex_autorunner/routes/repos.py +2 -196
  121. codex_autorunner/routes/review.py +2 -147
  122. codex_autorunner/routes/sessions.py +2 -175
  123. codex_autorunner/routes/settings.py +2 -168
  124. codex_autorunner/routes/shared.py +2 -275
  125. codex_autorunner/routes/system.py +4 -188
  126. codex_autorunner/routes/usage.py +3 -0
  127. codex_autorunner/routes/voice.py +2 -119
  128. codex_autorunner/routes/workspace.py +3 -0
  129. codex_autorunner/server.py +3 -2
  130. codex_autorunner/static/agentControls.js +41 -11
  131. codex_autorunner/static/agentEvents.js +248 -0
  132. codex_autorunner/static/app.js +35 -24
  133. codex_autorunner/static/archive.js +826 -0
  134. codex_autorunner/static/archiveApi.js +37 -0
  135. codex_autorunner/static/autoRefresh.js +36 -8
  136. codex_autorunner/static/bootstrap.js +1 -0
  137. codex_autorunner/static/bus.js +1 -0
  138. codex_autorunner/static/cache.js +1 -0
  139. codex_autorunner/static/constants.js +20 -4
  140. codex_autorunner/static/dashboard.js +344 -325
  141. codex_autorunner/static/diffRenderer.js +37 -0
  142. codex_autorunner/static/docChatCore.js +324 -0
  143. codex_autorunner/static/docChatStorage.js +65 -0
  144. codex_autorunner/static/docChatVoice.js +65 -0
  145. codex_autorunner/static/docEditor.js +133 -0
  146. codex_autorunner/static/env.js +1 -0
  147. codex_autorunner/static/eventSummarizer.js +166 -0
  148. codex_autorunner/static/fileChat.js +182 -0
  149. codex_autorunner/static/health.js +155 -0
  150. codex_autorunner/static/hub.js +126 -185
  151. codex_autorunner/static/index.html +839 -863
  152. codex_autorunner/static/liveUpdates.js +1 -0
  153. codex_autorunner/static/loader.js +1 -0
  154. codex_autorunner/static/messages.js +873 -0
  155. codex_autorunner/static/mobileCompact.js +2 -1
  156. codex_autorunner/static/preserve.js +17 -0
  157. codex_autorunner/static/settings.js +149 -217
  158. codex_autorunner/static/smartRefresh.js +52 -0
  159. codex_autorunner/static/styles.css +8850 -3876
  160. codex_autorunner/static/tabs.js +175 -11
  161. codex_autorunner/static/terminal.js +32 -0
  162. codex_autorunner/static/terminalManager.js +34 -59
  163. codex_autorunner/static/ticketChatActions.js +333 -0
  164. codex_autorunner/static/ticketChatEvents.js +16 -0
  165. codex_autorunner/static/ticketChatStorage.js +16 -0
  166. codex_autorunner/static/ticketChatStream.js +264 -0
  167. codex_autorunner/static/ticketEditor.js +844 -0
  168. codex_autorunner/static/ticketVoice.js +9 -0
  169. codex_autorunner/static/tickets.js +1988 -0
  170. codex_autorunner/static/utils.js +43 -3
  171. codex_autorunner/static/voice.js +1 -0
  172. codex_autorunner/static/workspace.js +765 -0
  173. codex_autorunner/static/workspaceApi.js +53 -0
  174. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  175. codex_autorunner/surfaces/__init__.py +5 -0
  176. codex_autorunner/surfaces/cli/__init__.py +6 -0
  177. codex_autorunner/surfaces/cli/cli.py +1224 -0
  178. codex_autorunner/surfaces/cli/codex_cli.py +20 -0
  179. codex_autorunner/surfaces/telegram/__init__.py +3 -0
  180. codex_autorunner/surfaces/web/__init__.py +1 -0
  181. codex_autorunner/surfaces/web/app.py +2019 -0
  182. codex_autorunner/surfaces/web/hub_jobs.py +192 -0
  183. codex_autorunner/surfaces/web/middleware.py +587 -0
  184. codex_autorunner/surfaces/web/pty_session.py +370 -0
  185. codex_autorunner/surfaces/web/review.py +6 -0
  186. codex_autorunner/surfaces/web/routes/__init__.py +78 -0
  187. codex_autorunner/surfaces/web/routes/agents.py +138 -0
  188. codex_autorunner/surfaces/web/routes/analytics.py +277 -0
  189. codex_autorunner/surfaces/web/routes/app_server.py +132 -0
  190. codex_autorunner/surfaces/web/routes/archive.py +357 -0
  191. codex_autorunner/surfaces/web/routes/base.py +615 -0
  192. codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
  193. codex_autorunner/surfaces/web/routes/flows.py +1164 -0
  194. codex_autorunner/surfaces/web/routes/messages.py +459 -0
  195. codex_autorunner/surfaces/web/routes/repos.py +197 -0
  196. codex_autorunner/surfaces/web/routes/review.py +148 -0
  197. codex_autorunner/surfaces/web/routes/sessions.py +176 -0
  198. codex_autorunner/surfaces/web/routes/settings.py +169 -0
  199. codex_autorunner/surfaces/web/routes/shared.py +280 -0
  200. codex_autorunner/surfaces/web/routes/system.py +196 -0
  201. codex_autorunner/surfaces/web/routes/usage.py +89 -0
  202. codex_autorunner/surfaces/web/routes/voice.py +120 -0
  203. codex_autorunner/surfaces/web/routes/workspace.py +271 -0
  204. codex_autorunner/surfaces/web/runner_manager.py +25 -0
  205. codex_autorunner/surfaces/web/schemas.py +417 -0
  206. codex_autorunner/surfaces/web/static_assets.py +490 -0
  207. codex_autorunner/surfaces/web/static_refresh.py +86 -0
  208. codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
  209. codex_autorunner/tickets/__init__.py +27 -0
  210. codex_autorunner/tickets/agent_pool.py +399 -0
  211. codex_autorunner/tickets/files.py +89 -0
  212. codex_autorunner/tickets/frontmatter.py +55 -0
  213. codex_autorunner/tickets/lint.py +102 -0
  214. codex_autorunner/tickets/models.py +97 -0
  215. codex_autorunner/tickets/outbox.py +244 -0
  216. codex_autorunner/tickets/replies.py +179 -0
  217. codex_autorunner/tickets/runner.py +881 -0
  218. codex_autorunner/tickets/spec_ingest.py +77 -0
  219. codex_autorunner/web/__init__.py +5 -1
  220. codex_autorunner/web/app.py +2 -1771
  221. codex_autorunner/web/hub_jobs.py +2 -191
  222. codex_autorunner/web/middleware.py +2 -587
  223. codex_autorunner/web/pty_session.py +2 -369
  224. codex_autorunner/web/runner_manager.py +2 -24
  225. codex_autorunner/web/schemas.py +2 -396
  226. codex_autorunner/web/static_assets.py +4 -484
  227. codex_autorunner/web/static_refresh.py +2 -85
  228. codex_autorunner/web/terminal_sessions.py +2 -77
  229. codex_autorunner/workspace/__init__.py +40 -0
  230. codex_autorunner/workspace/paths.py +335 -0
  231. codex_autorunner-1.1.0.dist-info/METADATA +154 -0
  232. codex_autorunner-1.1.0.dist-info/RECORD +308 -0
  233. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
  234. codex_autorunner/agents/execution/policy.py +0 -292
  235. codex_autorunner/agents/factory.py +0 -52
  236. codex_autorunner/agents/orchestrator.py +0 -358
  237. codex_autorunner/core/doc_chat.py +0 -1446
  238. codex_autorunner/core/snapshot.py +0 -580
  239. codex_autorunner/integrations/github/chatops.py +0 -268
  240. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  241. codex_autorunner/routes/docs.py +0 -381
  242. codex_autorunner/routes/github.py +0 -327
  243. codex_autorunner/routes/runs.py +0 -250
  244. codex_autorunner/spec_ingest.py +0 -812
  245. codex_autorunner/static/docChatActions.js +0 -287
  246. codex_autorunner/static/docChatEvents.js +0 -300
  247. codex_autorunner/static/docChatRender.js +0 -205
  248. codex_autorunner/static/docChatStream.js +0 -361
  249. codex_autorunner/static/docs.js +0 -20
  250. codex_autorunner/static/docsClipboard.js +0 -69
  251. codex_autorunner/static/docsCrud.js +0 -257
  252. codex_autorunner/static/docsDocUpdates.js +0 -62
  253. codex_autorunner/static/docsDrafts.js +0 -16
  254. codex_autorunner/static/docsElements.js +0 -69
  255. codex_autorunner/static/docsInit.js +0 -285
  256. codex_autorunner/static/docsParse.js +0 -160
  257. codex_autorunner/static/docsSnapshot.js +0 -87
  258. codex_autorunner/static/docsSpecIngest.js +0 -263
  259. codex_autorunner/static/docsState.js +0 -127
  260. codex_autorunner/static/docsThreadRegistry.js +0 -44
  261. codex_autorunner/static/docsUi.js +0 -153
  262. codex_autorunner/static/docsVoice.js +0 -56
  263. codex_autorunner/static/github.js +0 -504
  264. codex_autorunner/static/logs.js +0 -678
  265. codex_autorunner/static/review.js +0 -157
  266. codex_autorunner/static/runs.js +0 -418
  267. codex_autorunner/static/snapshot.js +0 -124
  268. codex_autorunner/static/state.js +0 -94
  269. codex_autorunner/static/todoPreview.js +0 -27
  270. codex_autorunner/workspace.py +0 -16
  271. codex_autorunner-0.1.2.dist-info/METADATA +0 -249
  272. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  273. /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
  274. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
  275. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
  276. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,765 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
2
+ import { api, flash, setButtonLoading } from "./utils.js";
3
+ import { initAgentControls, getSelectedAgent, getSelectedModel, getSelectedReasoning } from "./agentControls.js";
4
+ import { fetchWorkspace, ingestSpecToTickets, listTickets, fetchWorkspaceTree, uploadWorkspaceFiles, downloadWorkspaceZip, createWorkspaceFolder, writeWorkspace, } from "./workspaceApi.js";
5
+ import { applyDraft, discardDraft, fetchPendingDraft, sendFileChat, interruptFileChat, } from "./fileChat.js";
6
+ import { DocEditor } from "./docEditor.js";
7
+ import { WorkspaceFileBrowser } from "./workspaceFileBrowser.js";
8
+ import { createDocChat } from "./docChatCore.js";
9
+ import { initDocChatVoice } from "./docChatVoice.js";
10
+ import { renderDiff } from "./diffRenderer.js";
11
+ import { createSmartRefresh } from "./smartRefresh.js";
12
+ import { subscribe } from "./bus.js";
13
+ import { isRepoHealthy } from "./health.js";
14
+ const state = {
15
+ target: null,
16
+ content: "",
17
+ draft: null,
18
+ loading: false,
19
+ hasTickets: true,
20
+ files: [],
21
+ docEditor: null,
22
+ browser: null,
23
+ };
24
+ const WORKSPACE_CHAT_EVENT_LIMIT = 8;
25
+ const WORKSPACE_CHAT_EVENT_MAX = 50;
26
+ const workspaceChat = createDocChat({
27
+ idPrefix: "workspace-chat",
28
+ storage: { keyPrefix: "car-workspace-chat-", maxMessages: 50, version: 1 },
29
+ limits: { eventVisible: WORKSPACE_CHAT_EVENT_LIMIT, eventMax: WORKSPACE_CHAT_EVENT_MAX },
30
+ styling: {
31
+ eventClass: "doc-chat-event",
32
+ eventTitleClass: "doc-chat-event-title",
33
+ eventSummaryClass: "doc-chat-event-summary",
34
+ eventDetailClass: "doc-chat-event-detail",
35
+ eventMetaClass: "doc-chat-event-meta",
36
+ eventsEmptyClass: "doc-chat-events-empty",
37
+ eventsHiddenClass: "hidden",
38
+ messagesClass: "doc-chat-message",
39
+ messageRoleClass: "doc-chat-message-role",
40
+ messageContentClass: "doc-chat-message-content",
41
+ messageMetaClass: "doc-chat-message-meta",
42
+ messageUserClass: "user",
43
+ messageAssistantClass: "assistant",
44
+ messageAssistantThinkingClass: "streaming",
45
+ messageAssistantFinalClass: "final",
46
+ },
47
+ });
48
+ const WORKSPACE_DOC_KINDS = new Set(["active_context", "decisions", "spec"]);
49
+ const WORKSPACE_REFRESH_REASONS = ["initial", "background", "manual"];
50
+ let workspaceRefreshCount = 0;
51
+ function hashString(value) {
52
+ let hash = 5381;
53
+ for (let i = 0; i < value.length; i += 1) {
54
+ hash = (hash * 33) ^ value.charCodeAt(i);
55
+ }
56
+ return (hash >>> 0).toString(36);
57
+ }
58
+ function workspaceTreeSignature(nodes) {
59
+ const parts = [];
60
+ const walk = (list) => {
61
+ list.forEach((node) => {
62
+ parts.push([
63
+ node.path || "",
64
+ node.type || "",
65
+ node.is_pinned ? "1" : "0",
66
+ node.modified_at || "",
67
+ node.size ?? "",
68
+ ].join("|"));
69
+ if (node.children?.length)
70
+ walk(node.children);
71
+ });
72
+ };
73
+ walk(nodes || []);
74
+ return parts.join("::");
75
+ }
76
+ function els() {
77
+ return {
78
+ fileList: document.getElementById("workspace-file-list"),
79
+ fileSelect: document.getElementById("workspace-file-select"),
80
+ breadcrumbs: document.getElementById("workspace-breadcrumbs"),
81
+ status: document.getElementById("workspace-status"),
82
+ statusMobile: document.getElementById("workspace-status-mobile"),
83
+ uploadBtn: document.getElementById("workspace-upload"),
84
+ uploadInput: document.getElementById("workspace-upload-input"),
85
+ newFolderBtn: document.getElementById("workspace-new-folder"),
86
+ newFileBtn: document.getElementById("workspace-new-file"),
87
+ downloadAllBtn: document.getElementById("workspace-download-all"),
88
+ generateBtn: document.getElementById("workspace-generate-tickets"),
89
+ textarea: document.getElementById("workspace-content"),
90
+ saveBtn: document.getElementById("workspace-save"),
91
+ saveBtnMobile: document.getElementById("workspace-save-mobile"),
92
+ reloadBtn: document.getElementById("workspace-reload"),
93
+ reloadBtnMobile: document.getElementById("workspace-reload-mobile"),
94
+ patchMain: document.getElementById("workspace-patch-main"),
95
+ patchBody: document.getElementById("workspace-patch-body"),
96
+ patchSummary: document.getElementById("workspace-patch-summary"),
97
+ patchMeta: document.getElementById("workspace-patch-meta"),
98
+ patchApply: document.getElementById("workspace-patch-apply"),
99
+ patchReload: document.getElementById("workspace-patch-reload"),
100
+ patchDiscard: document.getElementById("workspace-patch-discard"),
101
+ chatInput: document.getElementById("workspace-chat-input"),
102
+ chatSend: document.getElementById("workspace-chat-send"),
103
+ chatCancel: document.getElementById("workspace-chat-cancel"),
104
+ chatNewThread: document.getElementById("workspace-chat-new-thread"),
105
+ chatStatus: document.getElementById("workspace-chat-status"),
106
+ chatError: document.getElementById("workspace-chat-error"),
107
+ chatMessages: document.getElementById("workspace-chat-history"),
108
+ chatEvents: document.getElementById("workspace-chat-events"),
109
+ chatEventsList: document.getElementById("workspace-chat-events-list"),
110
+ chatEventsToggle: document.getElementById("workspace-chat-events-toggle"),
111
+ agentSelect: document.getElementById("workspace-chat-agent-select"),
112
+ modelSelect: document.getElementById("workspace-chat-model-select"),
113
+ reasoningSelect: document.getElementById("workspace-chat-reasoning-select"),
114
+ createModal: document.getElementById("workspace-create-modal"),
115
+ createTitle: document.getElementById("workspace-create-title"),
116
+ createInput: document.getElementById("workspace-create-name"),
117
+ createHint: document.getElementById("workspace-create-hint"),
118
+ createPath: document.getElementById("workspace-create-path"),
119
+ createClose: document.getElementById("workspace-create-close"),
120
+ createCancel: document.getElementById("workspace-create-cancel"),
121
+ createSubmit: document.getElementById("workspace-create-submit"),
122
+ };
123
+ }
124
+ function workspaceKindFromPath(path) {
125
+ const normalized = (path || "").replace(/\\/g, "/").trim();
126
+ if (!normalized)
127
+ return null;
128
+ const baseName = normalized.split("/").pop() || normalized;
129
+ const match = baseName.match(/^([a-z_]+)\.md$/i);
130
+ const kind = match ? match[1].toLowerCase() : "";
131
+ if (WORKSPACE_DOC_KINDS.has(kind)) {
132
+ return kind;
133
+ }
134
+ return null;
135
+ }
136
+ async function readWorkspaceContent(path) {
137
+ const kind = workspaceKindFromPath(path);
138
+ if (kind) {
139
+ const res = await fetchWorkspace();
140
+ return res[kind] || "";
141
+ }
142
+ return (await api(`/api/workspace/file?path=${encodeURIComponent(path)}`));
143
+ }
144
+ async function writeWorkspaceContent(path, content) {
145
+ const kind = workspaceKindFromPath(path);
146
+ if (kind) {
147
+ try {
148
+ const res = await writeWorkspace(kind, content);
149
+ return res[kind] || "";
150
+ }
151
+ catch (err) {
152
+ const msg = err.message || "";
153
+ if (!msg.toLowerCase().includes("invalid workspace doc kind")) {
154
+ throw err;
155
+ }
156
+ // Fallback to generic file write in case detection misfires
157
+ }
158
+ }
159
+ return (await api(`/api/workspace/file?path=${encodeURIComponent(path)}`, {
160
+ method: "PUT",
161
+ body: { content },
162
+ }));
163
+ }
164
+ function target() {
165
+ if (!state.target)
166
+ return "workspace:active_context";
167
+ return `workspace:${state.target.path}`;
168
+ }
169
+ function setStatus(text) {
170
+ const { status, statusMobile } = els();
171
+ if (status)
172
+ status.textContent = text;
173
+ if (statusMobile)
174
+ statusMobile.textContent = text;
175
+ }
176
+ function setWorkspaceRefreshing(active) {
177
+ const { reloadBtn, reloadBtnMobile } = els();
178
+ workspaceRefreshCount = Math.max(0, workspaceRefreshCount + (active ? 1 : -1));
179
+ const isRefreshing = workspaceRefreshCount > 0;
180
+ setButtonLoading(reloadBtn, isRefreshing);
181
+ setButtonLoading(reloadBtnMobile, isRefreshing);
182
+ }
183
+ function renderPatch() {
184
+ const { patchMain, patchBody, patchSummary, patchMeta, textarea, saveBtn, reloadBtn } = els();
185
+ if (!patchMain || !patchBody)
186
+ return;
187
+ const draft = state.draft;
188
+ if (draft) {
189
+ patchMain.classList.remove("hidden");
190
+ patchMain.classList.toggle("stale", Boolean(draft.is_stale));
191
+ renderDiff(draft.patch || "(no diff)", patchBody);
192
+ if (patchSummary) {
193
+ patchSummary.textContent = draft.is_stale
194
+ ? "Stale draft — file changed since this draft was created."
195
+ : draft.agent_message || "Changes ready";
196
+ patchSummary.classList.toggle("warn", Boolean(draft.is_stale));
197
+ }
198
+ if (patchMeta) {
199
+ const created = draft.created_at || "";
200
+ patchMeta.textContent = draft.is_stale
201
+ ? `${created} · base ${draft.base_hash || ""} vs current ${draft.current_hash || ""}`.trim()
202
+ : created;
203
+ }
204
+ if (textarea) {
205
+ textarea.classList.add("hidden");
206
+ textarea.disabled = true;
207
+ }
208
+ const patchApply = els().patchApply;
209
+ if (patchApply)
210
+ patchApply.textContent = draft.is_stale ? "Force Apply" : "Apply Draft";
211
+ saveBtn?.setAttribute("disabled", "true");
212
+ reloadBtn?.setAttribute("disabled", "true");
213
+ }
214
+ else {
215
+ patchMain.classList.add("hidden");
216
+ if (textarea) {
217
+ textarea.classList.remove("hidden");
218
+ textarea.disabled = false;
219
+ }
220
+ saveBtn?.removeAttribute("disabled");
221
+ reloadBtn?.removeAttribute("disabled");
222
+ }
223
+ }
224
+ function renderChat() {
225
+ workspaceChat.render();
226
+ }
227
+ function updateDownloadButton() {
228
+ const { downloadAllBtn } = els();
229
+ if (!downloadAllBtn)
230
+ return;
231
+ const currentPath = state.browser?.getCurrentPath() || "";
232
+ const isRoot = !currentPath;
233
+ const folderName = currentPath.split("/").pop() || "";
234
+ downloadAllBtn.title = isRoot ? "Download all as ZIP" : `Download ${folderName}/ as ZIP`;
235
+ downloadAllBtn.onclick = () => downloadWorkspaceZip(isRoot ? undefined : currentPath);
236
+ }
237
+ let createMode = null;
238
+ function listFolderPaths(nodes, base = "") {
239
+ const paths = [];
240
+ nodes.forEach((node) => {
241
+ if (node.type !== "folder")
242
+ return;
243
+ const current = base ? `${base}/${node.name}` : node.name;
244
+ paths.push(current);
245
+ if (node.children?.length) {
246
+ paths.push(...listFolderPaths(node.children, current));
247
+ }
248
+ });
249
+ return paths;
250
+ }
251
+ function openCreateModal(mode) {
252
+ const { createModal, createTitle, createInput, createHint, createPath } = els();
253
+ if (!createModal || !createInput || !createTitle || !createHint || !createPath)
254
+ return;
255
+ createMode = mode;
256
+ createTitle.textContent = mode === "folder" ? "New Folder" : "New Markdown File";
257
+ createInput.value = "";
258
+ createInput.placeholder = mode === "folder" ? "folder-name" : "note.md";
259
+ createHint.textContent =
260
+ mode === "folder"
261
+ ? "Folder will be created under the current path"
262
+ : "File will be created under the current path ('.md' appended if missing)";
263
+ // Populate location selector with root + folders
264
+ createPath.innerHTML = "";
265
+ const rootOption = document.createElement("option");
266
+ rootOption.value = "";
267
+ rootOption.textContent = "Workspace (root)";
268
+ createPath.appendChild(rootOption);
269
+ const folders = listFolderPaths(state.files);
270
+ folders.forEach((path) => {
271
+ const opt = document.createElement("option");
272
+ opt.value = path;
273
+ opt.textContent = path;
274
+ createPath.appendChild(opt);
275
+ });
276
+ const currentPath = state.browser?.getCurrentPath() || "";
277
+ createPath.value = currentPath;
278
+ if (createPath.value !== currentPath) {
279
+ createPath.value = "";
280
+ }
281
+ createModal.hidden = false;
282
+ setTimeout(() => createInput.focus(), 10);
283
+ }
284
+ function closeCreateModal() {
285
+ const { createModal } = els();
286
+ createMode = null;
287
+ if (createModal)
288
+ createModal.hidden = true;
289
+ }
290
+ async function handleCreateSubmit() {
291
+ const { createInput, createPath } = els();
292
+ if (!createMode || !createInput || !createPath)
293
+ return;
294
+ const rawName = (createInput.value || "").trim();
295
+ if (!rawName) {
296
+ flash("Name is required", "error");
297
+ return;
298
+ }
299
+ const base = createPath.value ?? state.browser?.getCurrentPath() ?? "";
300
+ const name = createMode === "file" && !rawName.toLowerCase().endsWith(".md") ? `${rawName}.md` : rawName;
301
+ const path = base ? `${base}/${name}` : name;
302
+ try {
303
+ if (createMode === "folder") {
304
+ await createWorkspaceFolder(path);
305
+ flash("Folder created", "success");
306
+ }
307
+ else {
308
+ await writeWorkspaceContent(path, "");
309
+ flash("File created", "success");
310
+ }
311
+ closeCreateModal();
312
+ await loadFiles(createMode === "file" ? path : state.target?.path || undefined, "manual");
313
+ if (createMode === "file") {
314
+ state.browser?.select(path);
315
+ }
316
+ }
317
+ catch (err) {
318
+ flash(err.message || "Failed to create item", "error");
319
+ }
320
+ }
321
+ const workspaceTreeRefresh = createSmartRefresh({
322
+ getSignature: (payload) => workspaceTreeSignature(payload.tree || []),
323
+ render: (payload) => {
324
+ state.files = payload.tree;
325
+ const { fileList, fileSelect, breadcrumbs } = els();
326
+ if (!fileList)
327
+ return;
328
+ if (!state.browser) {
329
+ state.browser = new WorkspaceFileBrowser({
330
+ container: fileList,
331
+ selectEl: fileSelect,
332
+ breadcrumbsEl: breadcrumbs,
333
+ onSelect: (file) => {
334
+ state.target = { path: file.path, isPinned: Boolean(file.is_pinned) };
335
+ workspaceChat.setTarget(target());
336
+ void refreshWorkspaceFile(file.path, "manual");
337
+ },
338
+ onPathChange: () => updateDownloadButton(),
339
+ onRefresh: () => loadFiles(state.target?.path, "manual"),
340
+ onConfirm: (message) => window.workspaceConfirm?.(message) ?? Promise.resolve(confirm(message)),
341
+ });
342
+ }
343
+ const defaultPath = payload.defaultPath ?? state.target?.path ?? undefined;
344
+ state.browser.setTree(payload.tree, defaultPath || undefined);
345
+ updateDownloadButton();
346
+ if (state.target) {
347
+ workspaceChat.setTarget(target());
348
+ }
349
+ },
350
+ onSkip: () => {
351
+ updateDownloadButton();
352
+ },
353
+ });
354
+ const workspaceContentRefresh = createSmartRefresh({
355
+ getSignature: (payload) => `${payload.path}::${hashString(payload.content || "")}`,
356
+ render: async (payload, ctx) => {
357
+ if (payload.path !== state.target?.path)
358
+ return;
359
+ state.content = payload.content;
360
+ if (state.docEditor) {
361
+ state.docEditor.destroy();
362
+ }
363
+ const { textarea, saveBtn, status } = els();
364
+ if (!textarea)
365
+ return;
366
+ state.docEditor = new DocEditor({
367
+ target: target(),
368
+ textarea,
369
+ saveButton: saveBtn,
370
+ statusEl: status,
371
+ onLoad: async () => payload.content,
372
+ onSave: async (content) => {
373
+ const saved = await writeWorkspaceContent(payload.path, content);
374
+ state.content = saved;
375
+ if (saved !== content) {
376
+ textarea.value = saved;
377
+ }
378
+ },
379
+ });
380
+ await loadPendingDraft();
381
+ renderPatch();
382
+ if (ctx.reason !== "background") {
383
+ setStatus("Loaded");
384
+ }
385
+ },
386
+ });
387
+ async function refreshWorkspaceFile(path, reason = "manual") {
388
+ if (!WORKSPACE_REFRESH_REASONS.includes(reason)) {
389
+ reason = "manual";
390
+ }
391
+ const isInitial = reason === "initial";
392
+ if (isInitial) {
393
+ state.loading = true;
394
+ setStatus("Loading…");
395
+ }
396
+ else {
397
+ setWorkspaceRefreshing(true);
398
+ }
399
+ try {
400
+ await workspaceContentRefresh.refresh(async () => ({ path, content: await readWorkspaceContent(path) }), { reason });
401
+ }
402
+ catch (err) {
403
+ const message = err.message || "Failed to load workspace file";
404
+ flash(message, "error");
405
+ setStatus(message);
406
+ }
407
+ finally {
408
+ state.loading = false;
409
+ if (!isInitial) {
410
+ setWorkspaceRefreshing(false);
411
+ }
412
+ }
413
+ }
414
+ async function loadPendingDraft() {
415
+ state.draft = await fetchPendingDraft(target());
416
+ renderPatch();
417
+ }
418
+ async function reloadWorkspace() {
419
+ if (!state.target)
420
+ return;
421
+ await refreshWorkspaceFile(state.target.path, "manual");
422
+ }
423
+ async function maybeShowGenerate() {
424
+ try {
425
+ const res = await listTickets();
426
+ const tickets = Array.isArray(res.tickets)
427
+ ? res.tickets
428
+ : [];
429
+ state.hasTickets = tickets.length > 0;
430
+ }
431
+ catch {
432
+ state.hasTickets = true;
433
+ }
434
+ const btn = els().generateBtn;
435
+ if (btn)
436
+ btn.classList.toggle("hidden", state.hasTickets);
437
+ }
438
+ async function generateTickets() {
439
+ try {
440
+ const res = await ingestSpecToTickets();
441
+ flash(res.created > 0
442
+ ? `Created ${res.created} ticket${res.created === 1 ? "" : "s"}`
443
+ : "No tickets created", "success");
444
+ await maybeShowGenerate();
445
+ }
446
+ catch (err) {
447
+ flash(err.message || "Failed to generate tickets", "error");
448
+ }
449
+ }
450
+ async function applyWorkspaceDraft() {
451
+ try {
452
+ const isStale = Boolean(state.draft?.is_stale);
453
+ if (isStale) {
454
+ const confirmForce = window.confirm("This draft is stale because the file changed after it was created. Force apply anyway?");
455
+ if (!confirmForce)
456
+ return;
457
+ }
458
+ const res = await applyDraft(target(), { force: isStale });
459
+ const textarea = els().textarea;
460
+ if (textarea) {
461
+ textarea.value = res.content || "";
462
+ }
463
+ state.content = res.content || "";
464
+ state.draft = null;
465
+ renderPatch();
466
+ flash(res.agent_message || "Draft applied", "success");
467
+ }
468
+ catch (err) {
469
+ flash(err.message || "Failed to apply draft", "error");
470
+ }
471
+ }
472
+ async function discardWorkspaceDraft() {
473
+ try {
474
+ const res = await discardDraft(target());
475
+ const textarea = els().textarea;
476
+ if (textarea)
477
+ textarea.value = res.content || "";
478
+ state.content = res.content || "";
479
+ state.draft = null;
480
+ renderPatch();
481
+ flash("Draft discarded", "success");
482
+ }
483
+ catch (err) {
484
+ flash(err.message || "Failed to discard draft", "error");
485
+ }
486
+ }
487
+ async function sendChat() {
488
+ const { chatInput, chatSend, chatCancel } = els();
489
+ const message = (chatInput?.value || "").trim();
490
+ if (!message)
491
+ return;
492
+ const chatState = workspaceChat.state;
493
+ // Abort any in-flight chat first
494
+ if (chatState.controller)
495
+ chatState.controller.abort();
496
+ chatState.controller = new AbortController();
497
+ chatState.status = "running";
498
+ chatState.error = "";
499
+ chatState.statusText = "queued";
500
+ chatState.streamText = "";
501
+ workspaceChat.clearEvents();
502
+ workspaceChat.addUserMessage(message);
503
+ renderChat();
504
+ if (chatInput)
505
+ chatInput.value = "";
506
+ chatSend?.setAttribute("disabled", "true");
507
+ chatCancel?.classList.remove("hidden");
508
+ const agent = getSelectedAgent();
509
+ const model = getSelectedModel(agent) || undefined;
510
+ const reasoning = getSelectedReasoning(agent) || undefined;
511
+ try {
512
+ await sendFileChat(target(), message, chatState.controller, {
513
+ onStatus: (status) => {
514
+ chatState.statusText = status;
515
+ setStatus(status || "Running…");
516
+ renderChat();
517
+ },
518
+ onToken: (token) => {
519
+ chatState.streamText = (chatState.streamText || "") + token;
520
+ workspaceChat.renderMessages();
521
+ },
522
+ onEvent: (event) => {
523
+ workspaceChat.applyAppEvent(event);
524
+ workspaceChat.renderEvents();
525
+ },
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
+ }
556
+ renderChat();
557
+ },
558
+ onError: (msg) => {
559
+ chatState.status = "error";
560
+ chatState.error = msg;
561
+ renderChat();
562
+ flash(msg, "error");
563
+ },
564
+ onInterrupted: (msg) => {
565
+ chatState.status = "interrupted";
566
+ chatState.error = "";
567
+ chatState.streamText = "";
568
+ renderChat();
569
+ flash(msg, "info");
570
+ },
571
+ onDone: () => {
572
+ if (chatState.streamText) {
573
+ workspaceChat.addAssistantMessage(chatState.streamText);
574
+ chatState.streamText = "";
575
+ }
576
+ chatState.status = "done";
577
+ renderChat();
578
+ },
579
+ }, { agent, model, reasoning });
580
+ }
581
+ catch (err) {
582
+ const msg = err.message || "Chat failed";
583
+ const chatStateLocal = workspaceChat.state;
584
+ chatStateLocal.status = "error";
585
+ chatStateLocal.error = msg;
586
+ renderChat();
587
+ flash(msg, "error");
588
+ }
589
+ finally {
590
+ chatSend?.removeAttribute("disabled");
591
+ chatCancel?.classList.add("hidden");
592
+ const chatStateLocal = workspaceChat.state;
593
+ chatStateLocal.controller = null;
594
+ }
595
+ }
596
+ async function cancelChat() {
597
+ const chatState = workspaceChat.state;
598
+ if (chatState.controller) {
599
+ chatState.controller.abort();
600
+ }
601
+ try {
602
+ await interruptFileChat(target());
603
+ }
604
+ catch {
605
+ // ignore
606
+ }
607
+ chatState.status = "interrupted";
608
+ chatState.streamText = "";
609
+ renderChat();
610
+ }
611
+ async function resetThread() {
612
+ if (!state.target)
613
+ return;
614
+ try {
615
+ await api("/api/app-server/threads/reset", {
616
+ method: "POST",
617
+ body: { key: `file_chat.workspace.${state.target.path}` },
618
+ });
619
+ const chatState = workspaceChat.state;
620
+ chatState.messages = [];
621
+ chatState.streamText = "";
622
+ workspaceChat.clearEvents();
623
+ renderChat();
624
+ flash("New workspace chat thread", "success");
625
+ }
626
+ catch (err) {
627
+ flash(err.message || "Failed to reset thread", "error");
628
+ }
629
+ }
630
+ async function loadFiles(defaultPath, reason = "manual") {
631
+ if (!WORKSPACE_REFRESH_REASONS.includes(reason)) {
632
+ reason = "manual";
633
+ }
634
+ const isInitial = reason === "initial";
635
+ if (!isInitial) {
636
+ setWorkspaceRefreshing(true);
637
+ }
638
+ try {
639
+ await workspaceTreeRefresh.refresh(async () => ({ tree: await fetchWorkspaceTree(), defaultPath }), { reason });
640
+ }
641
+ finally {
642
+ if (!isInitial) {
643
+ setWorkspaceRefreshing(false);
644
+ }
645
+ }
646
+ }
647
+ export async function initWorkspace() {
648
+ const { generateBtn, uploadBtn, uploadInput, newFolderBtn, saveBtn, saveBtnMobile, reloadBtn, reloadBtnMobile, patchApply, patchDiscard, patchReload, chatSend, chatCancel, chatNewThread, } = els();
649
+ if (!document.getElementById("workspace"))
650
+ return;
651
+ initAgentControls({
652
+ agentSelect: els().agentSelect,
653
+ modelSelect: els().modelSelect,
654
+ reasoningSelect: els().reasoningSelect,
655
+ });
656
+ await initDocChatVoice({
657
+ buttonId: "workspace-chat-voice",
658
+ inputId: "workspace-chat-input",
659
+ });
660
+ await maybeShowGenerate();
661
+ await loadFiles(undefined, "initial");
662
+ workspaceChat.setTarget(target());
663
+ const reloadEverything = async () => {
664
+ await loadFiles(state.target?.path, "manual");
665
+ await reloadWorkspace();
666
+ };
667
+ saveBtn?.addEventListener("click", () => void state.docEditor?.save(true));
668
+ saveBtnMobile?.addEventListener("click", () => void state.docEditor?.save(true));
669
+ reloadBtn?.addEventListener("click", () => void reloadEverything());
670
+ reloadBtnMobile?.addEventListener("click", () => void reloadEverything());
671
+ uploadBtn?.addEventListener("click", () => uploadInput?.click());
672
+ uploadInput?.addEventListener("change", async () => {
673
+ const files = uploadInput.files;
674
+ if (!files || !files.length)
675
+ return;
676
+ const subdir = state.browser?.getCurrentPath() || "";
677
+ try {
678
+ await uploadWorkspaceFiles(files, subdir || undefined);
679
+ flash(`Uploaded ${files.length} file${files.length === 1 ? "" : "s"}`, "success");
680
+ await loadFiles(state.target?.path, "manual");
681
+ }
682
+ catch (err) {
683
+ flash(err.message || "Upload failed", "error");
684
+ }
685
+ finally {
686
+ uploadInput.value = "";
687
+ }
688
+ });
689
+ newFolderBtn?.addEventListener("click", () => openCreateModal("folder"));
690
+ els().newFileBtn?.addEventListener("click", () => openCreateModal("file"));
691
+ generateBtn?.addEventListener("click", () => void generateTickets());
692
+ patchApply?.addEventListener("click", () => void applyWorkspaceDraft());
693
+ patchDiscard?.addEventListener("click", () => void discardWorkspaceDraft());
694
+ patchReload?.addEventListener("click", () => void loadPendingDraft());
695
+ chatSend?.addEventListener("click", () => void sendChat());
696
+ chatCancel?.addEventListener("click", () => void cancelChat());
697
+ chatNewThread?.addEventListener("click", () => void resetThread());
698
+ const chatInput = els().chatInput;
699
+ if (chatInput) {
700
+ chatInput.addEventListener("keydown", (evt) => {
701
+ if ((evt.metaKey || evt.ctrlKey) && evt.key === "Enter") {
702
+ evt.preventDefault();
703
+ void sendChat();
704
+ }
705
+ });
706
+ }
707
+ const { createModal, createClose, createCancel, createSubmit } = els();
708
+ createClose?.addEventListener("click", () => closeCreateModal());
709
+ createCancel?.addEventListener("click", () => closeCreateModal());
710
+ createSubmit?.addEventListener("click", () => void handleCreateSubmit());
711
+ els().createInput?.addEventListener("keydown", (evt) => {
712
+ if (evt.key === "Enter") {
713
+ evt.preventDefault();
714
+ void handleCreateSubmit();
715
+ }
716
+ });
717
+ createModal?.addEventListener("click", (evt) => {
718
+ if (evt.target === createModal)
719
+ closeCreateModal();
720
+ });
721
+ document.addEventListener("keydown", (evt) => {
722
+ if (evt.key === "Escape" && createModal && !createModal.hidden) {
723
+ closeCreateModal();
724
+ }
725
+ });
726
+ // Confirm modal wiring
727
+ const confirmModal = document.getElementById("workspace-confirm-modal");
728
+ const confirmText = document.getElementById("workspace-confirm-text");
729
+ const confirmYes = document.getElementById("workspace-confirm-yes");
730
+ const confirmCancel = document.getElementById("workspace-confirm-cancel");
731
+ let confirmResolver = null;
732
+ const closeConfirm = (result) => {
733
+ if (confirmModal)
734
+ confirmModal.hidden = true;
735
+ confirmResolver?.(result);
736
+ confirmResolver = null;
737
+ };
738
+ window.workspaceConfirm = (message) => new Promise((resolve) => {
739
+ confirmResolver = resolve;
740
+ if (confirmText)
741
+ confirmText.textContent = message;
742
+ if (confirmModal)
743
+ confirmModal.hidden = false;
744
+ confirmYes?.focus();
745
+ });
746
+ confirmYes?.addEventListener("click", () => closeConfirm(true));
747
+ confirmCancel?.addEventListener("click", () => closeConfirm(false));
748
+ confirmModal?.addEventListener("click", (evt) => {
749
+ if (evt.target === confirmModal)
750
+ closeConfirm(false);
751
+ });
752
+ subscribe("repo:health", (payload) => {
753
+ const status = payload?.status || "";
754
+ if (status !== "ok" && status !== "degraded")
755
+ return;
756
+ if (!isRepoHealthy())
757
+ return;
758
+ void loadFiles(state.target?.path, "background");
759
+ const textarea = els().textarea;
760
+ const hasLocalEdits = textarea ? textarea.value !== state.content : false;
761
+ if (state.target && !state.draft && !hasLocalEdits) {
762
+ void refreshWorkspaceFile(state.target.path, "background");
763
+ }
764
+ });
765
+ }