codex-autorunner 0.1.2__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 (189) hide show
  1. codex_autorunner/__main__.py +4 -0
  2. codex_autorunner/agents/opencode/client.py +68 -35
  3. codex_autorunner/agents/opencode/logging.py +21 -5
  4. codex_autorunner/agents/opencode/run_prompt.py +1 -0
  5. codex_autorunner/agents/opencode/runtime.py +118 -30
  6. codex_autorunner/agents/opencode/supervisor.py +36 -48
  7. codex_autorunner/agents/registry.py +136 -8
  8. codex_autorunner/api.py +25 -0
  9. codex_autorunner/bootstrap.py +16 -35
  10. codex_autorunner/cli.py +157 -139
  11. codex_autorunner/core/about_car.py +44 -32
  12. codex_autorunner/core/adapter_utils.py +21 -0
  13. codex_autorunner/core/app_server_logging.py +7 -3
  14. codex_autorunner/core/app_server_prompts.py +27 -260
  15. codex_autorunner/core/app_server_threads.py +15 -26
  16. codex_autorunner/core/codex_runner.py +6 -0
  17. codex_autorunner/core/config.py +390 -100
  18. codex_autorunner/core/docs.py +10 -2
  19. codex_autorunner/core/drafts.py +82 -0
  20. codex_autorunner/core/engine.py +278 -262
  21. codex_autorunner/core/flows/__init__.py +25 -0
  22. codex_autorunner/core/flows/controller.py +178 -0
  23. codex_autorunner/core/flows/definition.py +82 -0
  24. codex_autorunner/core/flows/models.py +75 -0
  25. codex_autorunner/core/flows/runtime.py +351 -0
  26. codex_autorunner/core/flows/store.py +485 -0
  27. codex_autorunner/core/flows/transition.py +133 -0
  28. codex_autorunner/core/flows/worker_process.py +242 -0
  29. codex_autorunner/core/hub.py +15 -9
  30. codex_autorunner/core/locks.py +4 -0
  31. codex_autorunner/core/prompt.py +15 -7
  32. codex_autorunner/core/redaction.py +29 -0
  33. codex_autorunner/core/review_context.py +5 -8
  34. codex_autorunner/core/run_index.py +6 -0
  35. codex_autorunner/core/runner_process.py +5 -2
  36. codex_autorunner/core/state.py +0 -88
  37. codex_autorunner/core/static_assets.py +55 -0
  38. codex_autorunner/core/supervisor_utils.py +67 -0
  39. codex_autorunner/core/update.py +20 -11
  40. codex_autorunner/core/update_runner.py +2 -0
  41. codex_autorunner/core/utils.py +29 -2
  42. codex_autorunner/discovery.py +2 -4
  43. codex_autorunner/flows/ticket_flow/__init__.py +3 -0
  44. codex_autorunner/flows/ticket_flow/definition.py +91 -0
  45. codex_autorunner/integrations/agents/__init__.py +27 -0
  46. codex_autorunner/integrations/agents/agent_backend.py +142 -0
  47. codex_autorunner/integrations/agents/codex_backend.py +307 -0
  48. codex_autorunner/integrations/agents/opencode_backend.py +325 -0
  49. codex_autorunner/integrations/agents/run_event.py +71 -0
  50. codex_autorunner/integrations/app_server/client.py +576 -92
  51. codex_autorunner/integrations/app_server/supervisor.py +59 -33
  52. codex_autorunner/integrations/telegram/adapter.py +141 -167
  53. codex_autorunner/integrations/telegram/api_schemas.py +120 -0
  54. codex_autorunner/integrations/telegram/config.py +175 -0
  55. codex_autorunner/integrations/telegram/constants.py +16 -1
  56. codex_autorunner/integrations/telegram/dispatch.py +17 -0
  57. codex_autorunner/integrations/telegram/doctor.py +47 -0
  58. codex_autorunner/integrations/telegram/handlers/callbacks.py +0 -4
  59. codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
  60. codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
  61. codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
  62. codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
  63. codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
  64. codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
  65. codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
  66. codex_autorunner/integrations/telegram/handlers/commands_runtime.py +133 -475
  67. codex_autorunner/integrations/telegram/handlers/commands_spec.py +11 -4
  68. codex_autorunner/integrations/telegram/handlers/messages.py +120 -9
  69. codex_autorunner/integrations/telegram/helpers.py +88 -16
  70. codex_autorunner/integrations/telegram/outbox.py +208 -37
  71. codex_autorunner/integrations/telegram/progress_stream.py +3 -10
  72. codex_autorunner/integrations/telegram/service.py +214 -40
  73. codex_autorunner/integrations/telegram/state.py +100 -2
  74. codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
  75. codex_autorunner/integrations/telegram/transport.py +36 -3
  76. codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
  77. codex_autorunner/manifest.py +2 -0
  78. codex_autorunner/plugin_api.py +22 -0
  79. codex_autorunner/routes/__init__.py +23 -14
  80. codex_autorunner/routes/analytics.py +239 -0
  81. codex_autorunner/routes/base.py +81 -109
  82. codex_autorunner/routes/file_chat.py +836 -0
  83. codex_autorunner/routes/flows.py +980 -0
  84. codex_autorunner/routes/messages.py +459 -0
  85. codex_autorunner/routes/system.py +6 -1
  86. codex_autorunner/routes/usage.py +87 -0
  87. codex_autorunner/routes/workspace.py +271 -0
  88. codex_autorunner/server.py +2 -1
  89. codex_autorunner/static/agentControls.js +1 -0
  90. codex_autorunner/static/agentEvents.js +248 -0
  91. codex_autorunner/static/app.js +25 -22
  92. codex_autorunner/static/autoRefresh.js +29 -1
  93. codex_autorunner/static/bootstrap.js +1 -0
  94. codex_autorunner/static/bus.js +1 -0
  95. codex_autorunner/static/cache.js +1 -0
  96. codex_autorunner/static/constants.js +20 -4
  97. codex_autorunner/static/dashboard.js +162 -196
  98. codex_autorunner/static/diffRenderer.js +37 -0
  99. codex_autorunner/static/docChatCore.js +324 -0
  100. codex_autorunner/static/docChatStorage.js +65 -0
  101. codex_autorunner/static/docChatVoice.js +65 -0
  102. codex_autorunner/static/docEditor.js +133 -0
  103. codex_autorunner/static/env.js +1 -0
  104. codex_autorunner/static/eventSummarizer.js +166 -0
  105. codex_autorunner/static/fileChat.js +182 -0
  106. codex_autorunner/static/health.js +155 -0
  107. codex_autorunner/static/hub.js +41 -118
  108. codex_autorunner/static/index.html +787 -858
  109. codex_autorunner/static/liveUpdates.js +1 -0
  110. codex_autorunner/static/loader.js +1 -0
  111. codex_autorunner/static/messages.js +470 -0
  112. codex_autorunner/static/mobileCompact.js +2 -1
  113. codex_autorunner/static/settings.js +24 -211
  114. codex_autorunner/static/styles.css +7567 -3865
  115. codex_autorunner/static/tabs.js +28 -5
  116. codex_autorunner/static/terminal.js +14 -0
  117. codex_autorunner/static/terminalManager.js +34 -59
  118. codex_autorunner/static/ticketChatActions.js +333 -0
  119. codex_autorunner/static/ticketChatEvents.js +16 -0
  120. codex_autorunner/static/ticketChatStorage.js +16 -0
  121. codex_autorunner/static/ticketChatStream.js +264 -0
  122. codex_autorunner/static/ticketEditor.js +750 -0
  123. codex_autorunner/static/ticketVoice.js +9 -0
  124. codex_autorunner/static/tickets.js +1315 -0
  125. codex_autorunner/static/utils.js +32 -3
  126. codex_autorunner/static/voice.js +1 -0
  127. codex_autorunner/static/workspace.js +672 -0
  128. codex_autorunner/static/workspaceApi.js +53 -0
  129. codex_autorunner/static/workspaceFileBrowser.js +504 -0
  130. codex_autorunner/tickets/__init__.py +20 -0
  131. codex_autorunner/tickets/agent_pool.py +377 -0
  132. codex_autorunner/tickets/files.py +85 -0
  133. codex_autorunner/tickets/frontmatter.py +55 -0
  134. codex_autorunner/tickets/lint.py +102 -0
  135. codex_autorunner/tickets/models.py +95 -0
  136. codex_autorunner/tickets/outbox.py +232 -0
  137. codex_autorunner/tickets/replies.py +179 -0
  138. codex_autorunner/tickets/runner.py +823 -0
  139. codex_autorunner/tickets/spec_ingest.py +77 -0
  140. codex_autorunner/web/app.py +269 -91
  141. codex_autorunner/web/middleware.py +3 -4
  142. codex_autorunner/web/schemas.py +89 -109
  143. codex_autorunner/web/static_assets.py +1 -44
  144. codex_autorunner/workspace/__init__.py +40 -0
  145. codex_autorunner/workspace/paths.py +319 -0
  146. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +18 -21
  147. codex_autorunner-1.0.0.dist-info/RECORD +251 -0
  148. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
  149. codex_autorunner/agents/execution/policy.py +0 -292
  150. codex_autorunner/agents/factory.py +0 -52
  151. codex_autorunner/agents/orchestrator.py +0 -358
  152. codex_autorunner/core/doc_chat.py +0 -1446
  153. codex_autorunner/core/snapshot.py +0 -580
  154. codex_autorunner/integrations/github/chatops.py +0 -268
  155. codex_autorunner/integrations/github/pr_flow.py +0 -1314
  156. codex_autorunner/routes/docs.py +0 -381
  157. codex_autorunner/routes/github.py +0 -327
  158. codex_autorunner/routes/runs.py +0 -250
  159. codex_autorunner/spec_ingest.py +0 -812
  160. codex_autorunner/static/docChatActions.js +0 -287
  161. codex_autorunner/static/docChatEvents.js +0 -300
  162. codex_autorunner/static/docChatRender.js +0 -205
  163. codex_autorunner/static/docChatStream.js +0 -361
  164. codex_autorunner/static/docs.js +0 -20
  165. codex_autorunner/static/docsClipboard.js +0 -69
  166. codex_autorunner/static/docsCrud.js +0 -257
  167. codex_autorunner/static/docsDocUpdates.js +0 -62
  168. codex_autorunner/static/docsDrafts.js +0 -16
  169. codex_autorunner/static/docsElements.js +0 -69
  170. codex_autorunner/static/docsInit.js +0 -285
  171. codex_autorunner/static/docsParse.js +0 -160
  172. codex_autorunner/static/docsSnapshot.js +0 -87
  173. codex_autorunner/static/docsSpecIngest.js +0 -263
  174. codex_autorunner/static/docsState.js +0 -127
  175. codex_autorunner/static/docsThreadRegistry.js +0 -44
  176. codex_autorunner/static/docsUi.js +0 -153
  177. codex_autorunner/static/docsVoice.js +0 -56
  178. codex_autorunner/static/github.js +0 -504
  179. codex_autorunner/static/logs.js +0 -678
  180. codex_autorunner/static/review.js +0 -157
  181. codex_autorunner/static/runs.js +0 -418
  182. codex_autorunner/static/snapshot.js +0 -124
  183. codex_autorunner/static/state.js +0 -94
  184. codex_autorunner/static/todoPreview.js +0 -27
  185. codex_autorunner/workspace.py +0 -16
  186. codex_autorunner-0.1.2.dist-info/RECORD +0 -222
  187. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
  188. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
  189. {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,324 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
2
+ import { parseAppServerEvent } from "./agentEvents.js";
3
+ import { summarizeEvents, renderCompactSummary, COMPACT_MAX_ACTIONS, COMPACT_MAX_TEXT_LENGTH } from "./eventSummarizer.js";
4
+ import { saveChatHistory, loadChatHistory } from "./docChatStorage.js";
5
+ function getElements(prefix) {
6
+ return {
7
+ input: document.getElementById(`${prefix}-input`),
8
+ sendBtn: document.getElementById(`${prefix}-send`),
9
+ voiceBtn: document.getElementById(`${prefix}-voice`),
10
+ cancelBtn: document.getElementById(`${prefix}-cancel`),
11
+ newThreadBtn: document.getElementById(`${prefix}-new-thread`),
12
+ statusEl: document.getElementById(`${prefix}-status`),
13
+ errorEl: document.getElementById(`${prefix}-error`),
14
+ streamEl: document.getElementById(`${prefix}-stream`),
15
+ eventsMain: document.getElementById(`${prefix}-events`),
16
+ eventsList: document.getElementById(`${prefix}-events-list`),
17
+ eventsCount: document.getElementById(`${prefix}-events-count`),
18
+ eventsToggle: document.getElementById(`${prefix}-events-toggle`),
19
+ messagesEl: document.getElementById(`${prefix}-messages`) ||
20
+ document.getElementById(`${prefix}-history`),
21
+ historyHeader: document.getElementById(`${prefix}-history-header`),
22
+ voiceStatus: document.getElementById(`${prefix}-voice-status`),
23
+ };
24
+ }
25
+ function addEvent(state, entry, limits) {
26
+ state.events.push(entry);
27
+ if (state.events.length > limits.eventMax) {
28
+ state.events = state.events.slice(-limits.eventMax);
29
+ state.eventItemIndex = {};
30
+ state.events.forEach((evt, idx) => {
31
+ if (evt.itemId)
32
+ state.eventItemIndex[evt.itemId] = idx;
33
+ });
34
+ }
35
+ }
36
+ function buildMessage(role, content, isFinal) {
37
+ return {
38
+ id: `${role}-${Date.now()}`,
39
+ role,
40
+ content,
41
+ time: new Date().toISOString(),
42
+ isFinal,
43
+ };
44
+ }
45
+ export function createDocChat(config) {
46
+ const state = {
47
+ status: "idle",
48
+ target: null,
49
+ error: "",
50
+ streamText: "",
51
+ statusText: "",
52
+ controller: null,
53
+ draft: null,
54
+ events: [],
55
+ messages: [],
56
+ eventItemIndex: {},
57
+ eventsExpanded: false,
58
+ };
59
+ const elements = getElements(config.idPrefix);
60
+ function saveHistory() {
61
+ if (!config.storage || !state.target)
62
+ return;
63
+ saveChatHistory(config.storage, state.target, state.messages);
64
+ }
65
+ function loadHistory() {
66
+ if (!config.storage || !state.target) {
67
+ state.messages = [];
68
+ return;
69
+ }
70
+ state.messages = loadChatHistory(config.storage, state.target);
71
+ }
72
+ function setTarget(target) {
73
+ state.target = target;
74
+ loadHistory();
75
+ clearEvents();
76
+ render();
77
+ }
78
+ function addUserMessage(content) {
79
+ state.messages.push(buildMessage("user", content, true));
80
+ saveHistory();
81
+ }
82
+ function addAssistantMessage(content, isFinal = true) {
83
+ if (!content)
84
+ return;
85
+ const last = state.messages[state.messages.length - 1];
86
+ if (last && last.role === "assistant" && last.content === content)
87
+ return;
88
+ state.messages.push(buildMessage("assistant", content, isFinal));
89
+ saveHistory();
90
+ }
91
+ function clearEvents() {
92
+ state.events = [];
93
+ state.eventItemIndex = {};
94
+ }
95
+ function applyAppEvent(payload) {
96
+ const parsed = parseAppServerEvent(payload);
97
+ if (!parsed)
98
+ return;
99
+ const { event, mergeStrategy } = parsed;
100
+ const itemId = event.itemId;
101
+ if (mergeStrategy && itemId && state.eventItemIndex[itemId] !== undefined) {
102
+ const existingIndex = state.eventItemIndex[itemId];
103
+ const existing = state.events[existingIndex];
104
+ if (mergeStrategy === "append") {
105
+ existing.summary = `${existing.summary || ""}${event.summary}`;
106
+ }
107
+ else if (mergeStrategy === "newline") {
108
+ existing.summary = `${existing.summary || ""}\n\n`;
109
+ }
110
+ existing.time = event.time;
111
+ return;
112
+ }
113
+ addEvent(state, { ...event }, config.limits);
114
+ if (itemId)
115
+ state.eventItemIndex[itemId] = state.events.length - 1;
116
+ }
117
+ function renderEvents() {
118
+ const { eventsMain, eventsList, eventsCount, eventsToggle } = elements;
119
+ if (!eventsMain || !eventsList || !eventsCount)
120
+ return;
121
+ const hasEvents = state.events.length > 0;
122
+ const isRunning = state.status === "running";
123
+ const showEvents = hasEvents || isRunning;
124
+ const compactMode = !!config.compactMode;
125
+ const expanded = !!state.eventsExpanded;
126
+ if (config.styling.eventsHiddenClass) {
127
+ eventsMain.classList.toggle(config.styling.eventsHiddenClass, !showEvents);
128
+ }
129
+ else {
130
+ eventsMain.classList.toggle("hidden", !showEvents);
131
+ }
132
+ eventsCount.textContent = String(state.events.length);
133
+ if (!showEvents) {
134
+ eventsList.innerHTML = "";
135
+ return;
136
+ }
137
+ if (compactMode && !expanded) {
138
+ renderCompactEvents();
139
+ if (eventsToggle) {
140
+ eventsToggle.classList.toggle("hidden", !hasEvents);
141
+ eventsToggle.textContent = "Show details";
142
+ }
143
+ return;
144
+ }
145
+ const limit = config.limits.eventVisible;
146
+ const showCount = compactMode ? state.events.length : expanded ? state.events.length : Math.min(state.events.length, limit);
147
+ const visible = state.events.slice(-showCount);
148
+ if (eventsToggle) {
149
+ if (compactMode) {
150
+ eventsToggle.classList.toggle("hidden", !hasEvents);
151
+ eventsToggle.textContent = "Show compact";
152
+ }
153
+ else {
154
+ const hiddenCount = Math.max(0, state.events.length - showCount);
155
+ eventsToggle.classList.toggle("hidden", hiddenCount === 0);
156
+ eventsToggle.textContent = expanded ? "Show recent" : `Show more (${hiddenCount})`;
157
+ }
158
+ }
159
+ eventsList.innerHTML = "";
160
+ if (!hasEvents && isRunning) {
161
+ const empty = document.createElement("div");
162
+ empty.className =
163
+ config.styling.eventsWaitingClass || config.styling.eventsEmptyClass || "chat-events-empty";
164
+ empty.textContent = "Processing...";
165
+ eventsList.appendChild(empty);
166
+ return;
167
+ }
168
+ visible.forEach((entry) => {
169
+ const wrapper = document.createElement("div");
170
+ wrapper.className = `${config.styling.eventClass} ${entry.kind || ""}`.trim();
171
+ const title = document.createElement("div");
172
+ title.className = config.styling.eventTitleClass;
173
+ title.textContent = entry.title || entry.method || "Update";
174
+ wrapper.appendChild(title);
175
+ if (entry.summary) {
176
+ const summary = document.createElement("div");
177
+ summary.className = config.styling.eventSummaryClass;
178
+ summary.textContent = entry.summary;
179
+ wrapper.appendChild(summary);
180
+ }
181
+ if (entry.detail) {
182
+ const detail = document.createElement("div");
183
+ detail.className = config.styling.eventDetailClass;
184
+ detail.textContent = entry.detail;
185
+ wrapper.appendChild(detail);
186
+ }
187
+ const meta = document.createElement("div");
188
+ meta.className = config.styling.eventMetaClass;
189
+ meta.textContent = entry.time
190
+ ? new Date(entry.time).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
191
+ : "";
192
+ wrapper.appendChild(meta);
193
+ eventsList.appendChild(wrapper);
194
+ });
195
+ eventsList.scrollTop = eventsList.scrollHeight;
196
+ }
197
+ function renderCompactEvents() {
198
+ const { eventsList } = elements;
199
+ if (!eventsList)
200
+ return;
201
+ eventsList.innerHTML = "";
202
+ const summary = summarizeEvents(state.events, {
203
+ maxActions: config.compactOptions?.maxActions ?? COMPACT_MAX_ACTIONS,
204
+ maxTextLength: config.compactOptions?.maxTextLength ?? COMPACT_MAX_TEXT_LENGTH,
205
+ });
206
+ const text = state.events.length ? renderCompactSummary(summary) : "";
207
+ const wrapper = document.createElement("pre");
208
+ wrapper.className = "chat-events-compact";
209
+ wrapper.textContent = text || (state.status === "running" ? "Processing..." : "No events yet.");
210
+ eventsList.appendChild(wrapper);
211
+ }
212
+ function renderMessages() {
213
+ const { messagesEl, historyHeader } = elements;
214
+ if (!messagesEl)
215
+ return;
216
+ messagesEl.innerHTML = "";
217
+ const hasMessages = state.messages.length > 0;
218
+ const hasStream = !!state.streamText;
219
+ if (historyHeader) {
220
+ historyHeader.classList.toggle("hidden", !(hasMessages || hasStream));
221
+ }
222
+ messagesEl.classList.toggle("chat-history-empty", !(hasMessages || hasStream));
223
+ if (!hasMessages && !hasStream) {
224
+ return;
225
+ }
226
+ state.messages.forEach((msg) => {
227
+ const wrapper = document.createElement("div");
228
+ const roleClass = msg.role === "user" ? config.styling.messageUserClass : config.styling.messageAssistantClass;
229
+ const finalClass = msg.role === "assistant"
230
+ ? (msg.isFinal ? config.styling.messageAssistantFinalClass : config.styling.messageAssistantThinkingClass)
231
+ : "";
232
+ wrapper.className = [config.styling.messagesClass, roleClass, finalClass].filter(Boolean).join(" ").trim();
233
+ const roleLabel = document.createElement("div");
234
+ roleLabel.className = config.styling.messageRoleClass;
235
+ if (msg.role === "user") {
236
+ roleLabel.textContent = "You";
237
+ }
238
+ else {
239
+ roleLabel.textContent = msg.isFinal ? "Response" : "Thinking";
240
+ }
241
+ wrapper.appendChild(roleLabel);
242
+ const content = document.createElement("div");
243
+ content.className = config.styling.messageContentClass;
244
+ content.textContent = msg.content;
245
+ wrapper.appendChild(content);
246
+ const meta = document.createElement("div");
247
+ meta.className = config.styling.messageMetaClass;
248
+ const time = msg.time ? new Date(msg.time) : new Date();
249
+ meta.textContent = time.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
250
+ wrapper.appendChild(meta);
251
+ messagesEl.appendChild(wrapper);
252
+ });
253
+ if (hasStream) {
254
+ const streaming = document.createElement("div");
255
+ streaming.className = [
256
+ config.styling.messagesClass,
257
+ config.styling.messageAssistantClass,
258
+ config.styling.messageAssistantThinkingClass || "",
259
+ ]
260
+ .filter(Boolean)
261
+ .join(" ")
262
+ .trim();
263
+ const roleLabel = document.createElement("div");
264
+ roleLabel.className = config.styling.messageRoleClass;
265
+ roleLabel.textContent = "Thinking";
266
+ streaming.appendChild(roleLabel);
267
+ const content = document.createElement("div");
268
+ content.className = config.styling.messageContentClass;
269
+ content.textContent = state.streamText;
270
+ streaming.appendChild(content);
271
+ messagesEl.appendChild(streaming);
272
+ }
273
+ messagesEl.scrollTop = messagesEl.scrollHeight;
274
+ }
275
+ function render() {
276
+ const { statusEl, errorEl, cancelBtn, newThreadBtn, streamEl, } = elements;
277
+ if (statusEl) {
278
+ const status = state.error ? "error" : state.statusText || state.status;
279
+ statusEl.textContent = status;
280
+ statusEl.classList.toggle("error", !!state.error || state.status === "error");
281
+ statusEl.classList.toggle("running", state.status === "running");
282
+ }
283
+ if (errorEl) {
284
+ errorEl.textContent = state.error || "";
285
+ errorEl.classList.toggle("hidden", !state.error);
286
+ }
287
+ if (cancelBtn) {
288
+ cancelBtn.classList.toggle("hidden", state.status !== "running");
289
+ }
290
+ if (newThreadBtn) {
291
+ const hasHistory = state.messages.length > 0;
292
+ newThreadBtn.classList.toggle("hidden", !hasHistory || state.status === "running");
293
+ }
294
+ if (streamEl) {
295
+ const hasContent = state.events.length > 0 ||
296
+ state.messages.length > 0 ||
297
+ !!state.streamText ||
298
+ state.status === "running";
299
+ streamEl.classList.toggle("hidden", !hasContent);
300
+ }
301
+ renderEvents();
302
+ renderMessages();
303
+ }
304
+ // wire toggle
305
+ if (elements.eventsToggle) {
306
+ elements.eventsToggle.addEventListener("click", () => {
307
+ state.eventsExpanded = !state.eventsExpanded;
308
+ renderEvents();
309
+ });
310
+ }
311
+ return {
312
+ state,
313
+ elements,
314
+ render,
315
+ renderMessages,
316
+ renderEvents,
317
+ renderCompactEvents,
318
+ clearEvents,
319
+ applyAppEvent,
320
+ addUserMessage,
321
+ addAssistantMessage,
322
+ setTarget,
323
+ };
324
+ }
@@ -0,0 +1,65 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
2
+ const DEFAULT_VERSION = 1;
3
+ function buildKey(config, target) {
4
+ return `${config.keyPrefix}${target}`;
5
+ }
6
+ export function saveChatHistory(config, target, messages) {
7
+ const key = buildKey(config, target);
8
+ const data = {
9
+ version: config.version ?? DEFAULT_VERSION,
10
+ target,
11
+ messages: messages.slice(-(config.maxMessages || 50)),
12
+ lastUpdated: new Date().toISOString(),
13
+ };
14
+ try {
15
+ localStorage.setItem(key, JSON.stringify(data));
16
+ }
17
+ catch (err) {
18
+ console.warn("localStorage quota exceeded, clearing old chat history", err);
19
+ clearOldChatHistory(config);
20
+ try {
21
+ localStorage.setItem(key, JSON.stringify(data));
22
+ }
23
+ catch (err2) {
24
+ console.error("Failed to save chat history after cleanup", err2);
25
+ }
26
+ }
27
+ }
28
+ export function loadChatHistory(config, target) {
29
+ const key = buildKey(config, target);
30
+ try {
31
+ const raw = localStorage.getItem(key);
32
+ if (!raw)
33
+ return [];
34
+ const data = JSON.parse(raw);
35
+ const version = config.version ?? DEFAULT_VERSION;
36
+ if (data.version !== version)
37
+ return [];
38
+ return data.messages || [];
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ }
44
+ export function clearChatHistory(config, target) {
45
+ localStorage.removeItem(buildKey(config, target));
46
+ }
47
+ function clearOldChatHistory(config) {
48
+ const entries = [];
49
+ for (let i = 0; i < localStorage.length; i++) {
50
+ const key = localStorage.key(i);
51
+ if (key?.startsWith(config.keyPrefix)) {
52
+ try {
53
+ const data = JSON.parse(localStorage.getItem(key) || "{}");
54
+ entries.push({ key, lastUpdated: data.lastUpdated || "" });
55
+ }
56
+ catch {
57
+ // ignore parse errors
58
+ }
59
+ }
60
+ }
61
+ entries
62
+ .sort((a, b) => a.lastUpdated.localeCompare(b.lastUpdated))
63
+ .slice(0, Math.ceil(entries.length / 2))
64
+ .forEach((entry) => localStorage.removeItem(entry.key));
65
+ }
@@ -0,0 +1,65 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
2
+ /**
3
+ * Generic voice helper for doc/ticket chats.
4
+ */
5
+ import { flash } from "./utils.js";
6
+ import { initVoiceInput } from "./voice.js";
7
+ const VOICE_TRANSCRIPT_DISCLAIMER_TEXT = "Note: the text above was transcribed from voice input and may contain transcription errors.";
8
+ function wrapInjectedContext(text) {
9
+ return `<injected context>\n${text}\n</injected context>`;
10
+ }
11
+ function appendVoiceTranscriptDisclaimer(text) {
12
+ const base = text === undefined || text === null ? "" : String(text);
13
+ if (!base.trim())
14
+ return base;
15
+ const injection = wrapInjectedContext(VOICE_TRANSCRIPT_DISCLAIMER_TEXT);
16
+ if (base.includes(VOICE_TRANSCRIPT_DISCLAIMER_TEXT) || base.includes(injection)) {
17
+ return base;
18
+ }
19
+ const separator = base.endsWith("\n") ? "\n" : "\n\n";
20
+ return `${base}${separator}${injection}`;
21
+ }
22
+ function autoResizeTextarea(textarea) {
23
+ textarea.style.height = "auto";
24
+ textarea.style.height = Math.min(textarea.scrollHeight, 100) + "px";
25
+ }
26
+ export async function initDocChatVoice(config) {
27
+ const voiceBtn = document.getElementById(config.buttonId);
28
+ const input = document.getElementById(config.inputId);
29
+ const statusEl = config.statusId
30
+ ? document.getElementById(config.statusId)
31
+ : null;
32
+ if (!voiceBtn || !input)
33
+ return;
34
+ await initVoiceInput({
35
+ button: voiceBtn,
36
+ input,
37
+ statusEl: statusEl ?? undefined,
38
+ onTranscript: (text) => {
39
+ if (!text) {
40
+ flash("Voice capture returned no transcript", "error");
41
+ return;
42
+ }
43
+ const current = input.value.trim();
44
+ const prefix = current ? `${current} ` : "";
45
+ let next = `${prefix}${text}`.trim();
46
+ next = appendVoiceTranscriptDisclaimer(next);
47
+ input.value = next;
48
+ autoResizeTextarea(input);
49
+ input.focus();
50
+ flash("Voice transcript added");
51
+ },
52
+ onError: (msg) => {
53
+ if (msg) {
54
+ flash(msg, "error");
55
+ if (statusEl) {
56
+ statusEl.textContent = msg;
57
+ statusEl.classList.remove("hidden");
58
+ }
59
+ }
60
+ },
61
+ }).catch((err) => {
62
+ console.error("Voice init failed", err);
63
+ flash("Voice capture unavailable", "error");
64
+ });
65
+ }
@@ -0,0 +1,133 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
2
+ export class DocEditor {
3
+ constructor(config) {
4
+ this.saveTimer = null;
5
+ this.lastSavedContent = "";
6
+ this.status = "idle";
7
+ this.destroyed = false;
8
+ this.handleKeydown = (evt) => {
9
+ const active = document.activeElement;
10
+ const isTextarea = active === this.config.textarea;
11
+ if (!isTextarea)
12
+ return;
13
+ if ((evt.metaKey || evt.ctrlKey) && evt.key.toLowerCase() === "s") {
14
+ evt.preventDefault();
15
+ void this.save(true);
16
+ }
17
+ };
18
+ this.handleBeforeUnload = (evt) => {
19
+ if (this.status === "dirty" || this.status === "saving") {
20
+ evt.preventDefault();
21
+ evt.returnValue = "Unsaved changes";
22
+ }
23
+ };
24
+ const { autoSaveDelay = 2000, enableKeyboardSave = true, saveButton = null, statusEl = null, } = config;
25
+ this.config = {
26
+ ...config,
27
+ autoSaveDelay,
28
+ enableKeyboardSave,
29
+ saveButton,
30
+ statusEl,
31
+ };
32
+ this.init();
33
+ }
34
+ init() {
35
+ void this.load();
36
+ this.config.textarea.addEventListener("input", () => {
37
+ this.markDirty();
38
+ this.scheduleSave();
39
+ });
40
+ this.config.textarea.addEventListener("blur", () => {
41
+ void this.save();
42
+ });
43
+ if (this.config.saveButton) {
44
+ this.config.saveButton.addEventListener("click", () => void this.save(true));
45
+ }
46
+ if (this.config.enableKeyboardSave) {
47
+ document.addEventListener("keydown", this.handleKeydown);
48
+ window.addEventListener("beforeunload", this.handleBeforeUnload);
49
+ }
50
+ }
51
+ destroy() {
52
+ this.destroyed = true;
53
+ if (this.saveTimer)
54
+ clearTimeout(this.saveTimer);
55
+ document.removeEventListener("keydown", this.handleKeydown);
56
+ window.removeEventListener("beforeunload", this.handleBeforeUnload);
57
+ }
58
+ async load() {
59
+ const content = await this.config.onLoad();
60
+ this.lastSavedContent = content ?? "";
61
+ this.config.textarea.value = this.lastSavedContent;
62
+ this.setStatus("saved");
63
+ }
64
+ scheduleSave() {
65
+ if (this.saveTimer)
66
+ clearTimeout(this.saveTimer);
67
+ this.saveTimer = setTimeout(() => void this.save(), this.config.autoSaveDelay);
68
+ }
69
+ markDirty() {
70
+ if (this.status !== "dirty") {
71
+ this.setStatus("dirty");
72
+ }
73
+ }
74
+ setStatus(status) {
75
+ this.status = status;
76
+ const { statusEl, saveButton } = this.config;
77
+ if (statusEl) {
78
+ statusEl.textContent = this.statusLabel(status);
79
+ statusEl.classList.toggle("muted", status === "saved" || status === "idle");
80
+ statusEl.classList.toggle("error", status === "error");
81
+ statusEl.classList.toggle("dirty", status === "dirty");
82
+ }
83
+ if (saveButton) {
84
+ if (status === "saving")
85
+ saveButton.setAttribute("disabled", "true");
86
+ else
87
+ saveButton.removeAttribute("disabled");
88
+ }
89
+ }
90
+ statusLabel(status) {
91
+ switch (status) {
92
+ case "saving":
93
+ return "Saving…";
94
+ case "saved":
95
+ return "Saved";
96
+ case "error":
97
+ return "Save failed";
98
+ case "dirty":
99
+ return "Unsaved changes";
100
+ default:
101
+ return "";
102
+ }
103
+ }
104
+ async save(force = false) {
105
+ if (this.destroyed)
106
+ return;
107
+ if (this.saveTimer) {
108
+ clearTimeout(this.saveTimer);
109
+ this.saveTimer = null;
110
+ }
111
+ const value = this.config.textarea.value;
112
+ if (!force && value === this.lastSavedContent)
113
+ return;
114
+ this.setStatus("saving");
115
+ try {
116
+ const maybeHash = await this.config.onSave(value, this.baseHash);
117
+ if (typeof maybeHash === "string") {
118
+ this.baseHash = maybeHash;
119
+ }
120
+ this.lastSavedContent = value;
121
+ this.setStatus("saved");
122
+ // Clear saved indicator after a short delay to keep UI calm
123
+ setTimeout(() => {
124
+ if (this.status === "saved")
125
+ this.setStatus("idle");
126
+ }, 1200);
127
+ }
128
+ catch (err) {
129
+ console.error("DocEditor save failed", err);
130
+ this.setStatus("error");
131
+ }
132
+ }
133
+ }
@@ -1,3 +1,4 @@
1
+ // GENERATED FILE - do not edit directly. Source: static_src/
1
2
  const hasWindow = typeof window !== "undefined" && typeof window.location !== "undefined";
2
3
  const pathname = hasWindow ? window.location.pathname || "/" : "/";
3
4
  function normalizeBase(base) {