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