codex-autorunner 1.1.0__py3-none-any.whl → 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +17 -7
- codex_autorunner/bootstrap.py +219 -1
- codex_autorunner/core/__init__.py +17 -1
- codex_autorunner/core/about_car.py +124 -11
- codex_autorunner/core/app_server_threads.py +6 -0
- codex_autorunner/core/config.py +238 -3
- codex_autorunner/core/context_awareness.py +39 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +71 -1
- codex_autorunner/core/flows/reconciler.py +4 -1
- codex_autorunner/core/flows/runtime.py +22 -0
- codex_autorunner/core/flows/store.py +61 -9
- codex_autorunner/core/flows/transition.py +23 -16
- codex_autorunner/core/flows/ux_helpers.py +18 -3
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/hub.py +198 -41
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +683 -0
- codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
- codex_autorunner/core/pma_lifecycle.py +527 -0
- codex_autorunner/core/pma_queue.py +367 -0
- codex_autorunner/core/pma_safety.py +221 -0
- codex_autorunner/core/pma_state.py +115 -0
- codex_autorunner/core/ports/agent_backend.py +2 -5
- codex_autorunner/core/ports/run_event.py +1 -4
- codex_autorunner/core/prompt.py +0 -80
- codex_autorunner/core/prompts.py +56 -172
- codex_autorunner/core/redaction.py +0 -4
- codex_autorunner/core/review_context.py +11 -9
- codex_autorunner/core/runner_controller.py +35 -33
- codex_autorunner/core/runner_state.py +147 -0
- codex_autorunner/core/runtime.py +829 -0
- codex_autorunner/core/sqlite_utils.py +13 -4
- codex_autorunner/core/state.py +7 -10
- codex_autorunner/core/state_roots.py +5 -0
- codex_autorunner/core/templates/__init__.py +39 -0
- codex_autorunner/core/templates/git_mirror.py +234 -0
- codex_autorunner/core/templates/provenance.py +56 -0
- codex_autorunner/core/templates/scan_cache.py +120 -0
- codex_autorunner/core/ticket_linter_cli.py +17 -0
- codex_autorunner/core/ticket_manager_cli.py +154 -92
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/utils.py +34 -6
- codex_autorunner/flows/review/service.py +23 -25
- codex_autorunner/flows/ticket_flow/definition.py +43 -1
- codex_autorunner/integrations/agents/__init__.py +2 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +18 -0
- codex_autorunner/integrations/agents/codex_backend.py +19 -8
- codex_autorunner/integrations/agents/runner.py +3 -8
- codex_autorunner/integrations/agents/wiring.py +8 -0
- codex_autorunner/integrations/telegram/adapter.py +1 -1
- codex_autorunner/integrations/telegram/config.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
- codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +346 -58
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +202 -45
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +18 -7
- codex_autorunner/integrations/telegram/handlers/messages.py +34 -3
- codex_autorunner/integrations/telegram/helpers.py +1 -3
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +30 -0
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +10 -4
- codex_autorunner/integrations/telegram/transport.py +10 -3
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/server.py +2 -2
- codex_autorunner/static/agentControls.js +21 -5
- codex_autorunner/static/app.js +115 -11
- codex_autorunner/static/archive.js +274 -81
- codex_autorunner/static/archiveApi.js +21 -0
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/constants.js +1 -1
- codex_autorunner/static/docChatCore.js +185 -13
- codex_autorunner/static/fileChat.js +68 -40
- codex_autorunner/static/fileboxUi.js +159 -0
- codex_autorunner/static/hub.js +46 -81
- codex_autorunner/static/index.html +303 -24
- codex_autorunner/static/messages.js +82 -4
- codex_autorunner/static/notifications.js +288 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/settings.js +3 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9141 -6742
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminalManager.js +22 -3
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +41 -13
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +69 -19
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +28 -0
- codex_autorunner/static/workspace.js +258 -44
- codex_autorunner/static/workspaceFileBrowser.js +6 -4
- codex_autorunner/surfaces/cli/cli.py +1465 -155
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/web/app.py +253 -49
- codex_autorunner/surfaces/web/routes/__init__.py +4 -0
- codex_autorunner/surfaces/web/routes/analytics.py +29 -22
- codex_autorunner/surfaces/web/routes/archive.py +197 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +297 -36
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +219 -29
- codex_autorunner/surfaces/web/routes/messages.py +70 -39
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +1 -1
- codex_autorunner/surfaces/web/routes/shared.py +0 -3
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/runner_manager.py +2 -2
- codex_autorunner/surfaces/web/schemas.py +81 -18
- codex_autorunner/tickets/agent_pool.py +27 -0
- codex_autorunner/tickets/files.py +33 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +3 -0
- codex_autorunner/tickets/outbox.py +41 -5
- codex_autorunner/tickets/runner.py +350 -69
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/METADATA +15 -19
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/RECORD +132 -101
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -3302
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.1.0.dist-info → codex_autorunner-1.2.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// GENERATED FILE - do not edit directly. Source: static_src/
|
|
2
|
+
import { api, flash, resolvePath } from "./utils.js";
|
|
3
|
+
const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "webp", "heic", "heif"];
|
|
4
|
+
const IMAGE_MIME_EXT = {
|
|
5
|
+
"image/png": "png",
|
|
6
|
+
"image/jpeg": "jpg",
|
|
7
|
+
"image/gif": "gif",
|
|
8
|
+
"image/webp": "webp",
|
|
9
|
+
"image/heic": "heic",
|
|
10
|
+
"image/heif": "heif",
|
|
11
|
+
};
|
|
12
|
+
function escapeMarkdownLinkText(text) {
|
|
13
|
+
return text.replace(/\\/g, "\\\\").replace(/\[/g, "\\[").replace(/\]/g, "\\]");
|
|
14
|
+
}
|
|
15
|
+
function toAbsoluteUrl(path) {
|
|
16
|
+
const resolved = resolvePath(path);
|
|
17
|
+
try {
|
|
18
|
+
return new URL(resolved, window.location.origin).toString();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return resolved;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function isImageFile(file) {
|
|
25
|
+
if (file.type && file.type.startsWith("image/"))
|
|
26
|
+
return true;
|
|
27
|
+
const lower = (file.name || "").toLowerCase();
|
|
28
|
+
return IMAGE_EXTENSIONS.some((ext) => lower.endsWith(`.${ext}`));
|
|
29
|
+
}
|
|
30
|
+
function normalizeFilename(file, index, used) {
|
|
31
|
+
let name = (file.name || "").trim();
|
|
32
|
+
if (!name) {
|
|
33
|
+
const ext = IMAGE_MIME_EXT[file.type] || "png";
|
|
34
|
+
name = `pasted-image-${Date.now()}-${index + 1}.${ext}`;
|
|
35
|
+
}
|
|
36
|
+
let candidate = name;
|
|
37
|
+
let suffix = 1;
|
|
38
|
+
while (used.has(candidate)) {
|
|
39
|
+
const dot = name.lastIndexOf(".");
|
|
40
|
+
if (dot > 0) {
|
|
41
|
+
const base = name.slice(0, dot);
|
|
42
|
+
const ext = name.slice(dot + 1);
|
|
43
|
+
candidate = `${base}-${suffix}.${ext}`;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
candidate = `${name}-${suffix}`;
|
|
47
|
+
}
|
|
48
|
+
suffix += 1;
|
|
49
|
+
}
|
|
50
|
+
used.add(candidate);
|
|
51
|
+
return candidate;
|
|
52
|
+
}
|
|
53
|
+
function extractImageFilesFromClipboard(event) {
|
|
54
|
+
const items = event.clipboardData?.items;
|
|
55
|
+
if (!items || !items.length)
|
|
56
|
+
return [];
|
|
57
|
+
const files = [];
|
|
58
|
+
for (const item of Array.from(items)) {
|
|
59
|
+
if (item.kind !== "file")
|
|
60
|
+
continue;
|
|
61
|
+
if (item.type && !item.type.startsWith("image/"))
|
|
62
|
+
continue;
|
|
63
|
+
const file = item.getAsFile();
|
|
64
|
+
if (file && isImageFile(file))
|
|
65
|
+
files.push(file);
|
|
66
|
+
}
|
|
67
|
+
return files;
|
|
68
|
+
}
|
|
69
|
+
async function uploadImages(basePath, box, files, pathPrefix) {
|
|
70
|
+
const used = new Set();
|
|
71
|
+
const form = new FormData();
|
|
72
|
+
const entries = [];
|
|
73
|
+
const prefix = basePath.replace(/\/$/, "");
|
|
74
|
+
const normalizedPathPrefix = pathPrefix ? pathPrefix.replace(/\/$/, "") : "";
|
|
75
|
+
files.forEach((file, index) => {
|
|
76
|
+
const name = normalizeFilename(file, index, used);
|
|
77
|
+
form.append(name, file, name);
|
|
78
|
+
const path = normalizedPathPrefix ? `${normalizedPathPrefix}/${box}/${name}` : undefined;
|
|
79
|
+
const relativeUrl = `${prefix}/${box}/${encodeURIComponent(name)}`;
|
|
80
|
+
entries.push({ name, url: toAbsoluteUrl(relativeUrl), path });
|
|
81
|
+
});
|
|
82
|
+
await api(`${prefix}/${box}`, { method: "POST", body: form });
|
|
83
|
+
return entries;
|
|
84
|
+
}
|
|
85
|
+
function insertTextAtCursor(textarea, text, options = {}) {
|
|
86
|
+
const value = textarea.value || "";
|
|
87
|
+
const start = Number.isInteger(textarea.selectionStart) ? textarea.selectionStart : value.length;
|
|
88
|
+
const end = Number.isInteger(textarea.selectionEnd) ? textarea.selectionEnd : value.length;
|
|
89
|
+
const prefix = value.slice(0, start);
|
|
90
|
+
const suffix = value.slice(end);
|
|
91
|
+
const separator = options.separator || "newline";
|
|
92
|
+
let insert = text;
|
|
93
|
+
if (separator === "newline") {
|
|
94
|
+
insert = `${prefix && !prefix.endsWith("\n") ? "\n" : ""}${insert}`;
|
|
95
|
+
}
|
|
96
|
+
else if (separator === "space") {
|
|
97
|
+
insert = `${prefix && !/\s$/.test(prefix) ? " " : ""}${insert}`;
|
|
98
|
+
}
|
|
99
|
+
textarea.value = `${prefix}${insert}${suffix}`;
|
|
100
|
+
const cursor = prefix.length + insert.length;
|
|
101
|
+
textarea.setSelectionRange(cursor, cursor);
|
|
102
|
+
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
|
103
|
+
}
|
|
104
|
+
export function initChatPasteUpload(options) {
|
|
105
|
+
const { textarea } = options;
|
|
106
|
+
if (!textarea)
|
|
107
|
+
return;
|
|
108
|
+
textarea.addEventListener("paste", async (event) => {
|
|
109
|
+
const files = extractImageFilesFromClipboard(event);
|
|
110
|
+
if (!files.length)
|
|
111
|
+
return;
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
const box = options.box || "inbox";
|
|
114
|
+
const insertStyle = options.insertStyle || "markdown";
|
|
115
|
+
try {
|
|
116
|
+
const entries = await uploadImages(options.basePath, box, files, options.pathPrefix);
|
|
117
|
+
const lines = entries.flatMap((entry) => {
|
|
118
|
+
const label = escapeMarkdownLinkText(entry.name);
|
|
119
|
+
const linkLine = `[${label}](${entry.url})`;
|
|
120
|
+
if (insertStyle === "markdown")
|
|
121
|
+
return [linkLine];
|
|
122
|
+
const pathLine = entry.path || entry.name;
|
|
123
|
+
if (insertStyle === "path")
|
|
124
|
+
return [pathLine];
|
|
125
|
+
return entry.path ? [linkLine, entry.path] : [linkLine];
|
|
126
|
+
});
|
|
127
|
+
if (lines.length) {
|
|
128
|
+
insertTextAtCursor(textarea, lines.join("\n"), { separator: "newline" });
|
|
129
|
+
}
|
|
130
|
+
options.onUploaded?.(entries.map((entry) => ({ name: entry.name, url: entry.url })));
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
const message = err.message || "Image upload failed";
|
|
134
|
+
flash(message, "error");
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -37,7 +37,7 @@ export const CONSTANTS = {
|
|
|
37
37
|
},
|
|
38
38
|
PROMPTS: {
|
|
39
39
|
VOICE_TRANSCRIPT_DISCLAIMER: "Note: transcribed from user voice. If confusing or possibly inaccurate and you cannot infer the intention please clarify before proceeding.",
|
|
40
|
-
CAR_CONTEXT_HINT: "Context:
|
|
40
|
+
CAR_CONTEXT_HINT: "Context: This repo is managed by Codex Autorunner (CAR). Read `.codex-autorunner/ABOUT_CAR.md` (tickets, workspace docs, helper commands) before making workflow assumptions.",
|
|
41
41
|
},
|
|
42
42
|
KEYWORDS: {
|
|
43
43
|
CAR_CONTEXT: [
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { parseAppServerEvent } from "./agentEvents.js";
|
|
3
3
|
import { summarizeEvents, renderCompactSummary, COMPACT_MAX_ACTIONS, COMPACT_MAX_TEXT_LENGTH } from "./eventSummarizer.js";
|
|
4
4
|
import { saveChatHistory, loadChatHistory } from "./docChatStorage.js";
|
|
5
|
+
import { renderMarkdown } from "./messages.js";
|
|
5
6
|
function getElements(prefix) {
|
|
6
7
|
return {
|
|
7
8
|
input: document.getElementById(`${prefix}-input`),
|
|
@@ -33,13 +34,14 @@ function addEvent(state, entry, limits) {
|
|
|
33
34
|
});
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
|
-
function buildMessage(role, content, isFinal) {
|
|
37
|
+
function buildMessage(role, content, isFinal, meta) {
|
|
37
38
|
return {
|
|
38
39
|
id: `${role}-${Date.now()}`,
|
|
39
40
|
role,
|
|
40
41
|
content,
|
|
41
42
|
time: new Date().toISOString(),
|
|
42
43
|
isFinal,
|
|
44
|
+
meta,
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
47
|
export function createDocChat(config) {
|
|
@@ -52,11 +54,31 @@ export function createDocChat(config) {
|
|
|
52
54
|
controller: null,
|
|
53
55
|
draft: null,
|
|
54
56
|
events: [],
|
|
57
|
+
totalEvents: 0,
|
|
55
58
|
messages: [],
|
|
56
59
|
eventItemIndex: {},
|
|
57
60
|
eventsExpanded: false,
|
|
61
|
+
contextUsagePercent: null,
|
|
58
62
|
};
|
|
59
63
|
const elements = getElements(config.idPrefix);
|
|
64
|
+
function decorateFileLinks(root) {
|
|
65
|
+
const links = Array.from(root.querySelectorAll("a"));
|
|
66
|
+
for (const link of links) {
|
|
67
|
+
const href = link.getAttribute("href") || "";
|
|
68
|
+
if (!href)
|
|
69
|
+
continue;
|
|
70
|
+
// Only decorate PMA file links.
|
|
71
|
+
if (!href.includes("/hub/pma/files/"))
|
|
72
|
+
continue;
|
|
73
|
+
link.classList.add("pma-file-link");
|
|
74
|
+
link.setAttribute("download", "");
|
|
75
|
+
// Ensure downloads happen in-place (no new tab).
|
|
76
|
+
link.removeAttribute("target");
|
|
77
|
+
link.setAttribute("rel", "noopener");
|
|
78
|
+
if (!link.title)
|
|
79
|
+
link.title = "Download file";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
60
82
|
function saveHistory() {
|
|
61
83
|
if (!config.storage || !state.target)
|
|
62
84
|
return;
|
|
@@ -79,17 +101,21 @@ export function createDocChat(config) {
|
|
|
79
101
|
state.messages.push(buildMessage("user", content, true));
|
|
80
102
|
saveHistory();
|
|
81
103
|
}
|
|
82
|
-
function addAssistantMessage(content, isFinal = true) {
|
|
104
|
+
function addAssistantMessage(content, isFinal = true, meta) {
|
|
83
105
|
if (!content)
|
|
84
106
|
return;
|
|
85
107
|
const last = state.messages[state.messages.length - 1];
|
|
86
|
-
if (last && last.role === "assistant" && last.content === content)
|
|
108
|
+
if (last && last.role === "assistant" && last.content === content) {
|
|
109
|
+
if (meta)
|
|
110
|
+
last.meta = meta;
|
|
87
111
|
return;
|
|
88
|
-
|
|
112
|
+
}
|
|
113
|
+
state.messages.push(buildMessage("assistant", content, isFinal, meta));
|
|
89
114
|
saveHistory();
|
|
90
115
|
}
|
|
91
116
|
function clearEvents() {
|
|
92
117
|
state.events = [];
|
|
118
|
+
state.totalEvents = 0;
|
|
93
119
|
state.eventItemIndex = {};
|
|
94
120
|
}
|
|
95
121
|
function applyAppEvent(payload) {
|
|
@@ -111,6 +137,7 @@ export function createDocChat(config) {
|
|
|
111
137
|
return;
|
|
112
138
|
}
|
|
113
139
|
addEvent(state, { ...event }, config.limits);
|
|
140
|
+
state.totalEvents += 1;
|
|
114
141
|
if (itemId)
|
|
115
142
|
state.eventItemIndex[itemId] = state.events.length - 1;
|
|
116
143
|
}
|
|
@@ -118,18 +145,33 @@ export function createDocChat(config) {
|
|
|
118
145
|
const { eventsMain, eventsList, eventsCount, eventsToggle } = elements;
|
|
119
146
|
if (!eventsMain || !eventsList || !eventsCount)
|
|
120
147
|
return;
|
|
148
|
+
// If inlineEvents is enabled, we don't render to the separate events container
|
|
149
|
+
if (config.inlineEvents) {
|
|
150
|
+
// Still need to calculate showEvents to hide the container properly
|
|
151
|
+
// but return early before modifying innerHTML
|
|
152
|
+
if (eventsMain)
|
|
153
|
+
eventsMain.classList.add("hidden");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
121
156
|
const hasEvents = state.events.length > 0;
|
|
122
157
|
const isRunning = state.status === "running";
|
|
123
|
-
const showEvents = hasEvents || isRunning;
|
|
158
|
+
const showEvents = config.eventsOnlyWhileRunning ? isRunning : (hasEvents || isRunning);
|
|
124
159
|
const compactMode = !!config.compactMode;
|
|
125
160
|
const expanded = !!state.eventsExpanded;
|
|
126
161
|
if (config.styling.eventsHiddenClass) {
|
|
127
162
|
eventsMain.classList.toggle(config.styling.eventsHiddenClass, !showEvents);
|
|
128
163
|
}
|
|
129
164
|
else {
|
|
130
|
-
|
|
165
|
+
// In inline mode, never show the main event container since we render inline
|
|
166
|
+
if (config.inlineEvents) {
|
|
167
|
+
eventsMain.classList.add("hidden");
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
eventsMain.classList.toggle("hidden", !showEvents);
|
|
171
|
+
}
|
|
131
172
|
}
|
|
132
|
-
|
|
173
|
+
const eventCount = state.totalEvents || state.events.length;
|
|
174
|
+
eventsCount.textContent = String(eventCount);
|
|
133
175
|
if (!showEvents) {
|
|
134
176
|
eventsList.innerHTML = "";
|
|
135
177
|
return;
|
|
@@ -202,6 +244,7 @@ export function createDocChat(config) {
|
|
|
202
244
|
const summary = summarizeEvents(state.events, {
|
|
203
245
|
maxActions: config.compactOptions?.maxActions ?? COMPACT_MAX_ACTIONS,
|
|
204
246
|
maxTextLength: config.compactOptions?.maxTextLength ?? COMPACT_MAX_TEXT_LENGTH,
|
|
247
|
+
contextUsagePercent: state.contextUsagePercent ?? undefined,
|
|
205
248
|
});
|
|
206
249
|
const text = state.events.length ? renderCompactSummary(summary) : "";
|
|
207
250
|
const wrapper = document.createElement("pre");
|
|
@@ -240,17 +283,45 @@ export function createDocChat(config) {
|
|
|
240
283
|
}
|
|
241
284
|
wrapper.appendChild(roleLabel);
|
|
242
285
|
const content = document.createElement("div");
|
|
243
|
-
content.className = config.styling.messageContentClass
|
|
244
|
-
|
|
286
|
+
content.className = `${config.styling.messageContentClass} messages-markdown`;
|
|
287
|
+
// Use markdown rendering for assistant messages.
|
|
288
|
+
// For user messages, keep plain text unless the message includes PMA file links
|
|
289
|
+
// (used for "uploaded file" pills).
|
|
290
|
+
const shouldRenderMarkdown = msg.role === "assistant" ||
|
|
291
|
+
msg.content.includes("/hub/pma/files/") ||
|
|
292
|
+
msg.content.includes("/api/filebox/") ||
|
|
293
|
+
msg.content.includes("/hub/filebox/");
|
|
294
|
+
if (shouldRenderMarkdown) {
|
|
295
|
+
content.innerHTML = renderMarkdown(msg.content);
|
|
296
|
+
decorateFileLinks(content);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
content.textContent = msg.content;
|
|
300
|
+
}
|
|
245
301
|
wrapper.appendChild(content);
|
|
246
302
|
const meta = document.createElement("div");
|
|
247
303
|
meta.className = config.styling.messageMetaClass;
|
|
248
304
|
const time = msg.time ? new Date(msg.time) : new Date();
|
|
249
|
-
|
|
305
|
+
let metaText = time.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
306
|
+
if (msg.meta) {
|
|
307
|
+
const parts = [];
|
|
308
|
+
if (msg.meta.steps)
|
|
309
|
+
parts.push(`${msg.meta.steps} steps`);
|
|
310
|
+
if (msg.meta.duration)
|
|
311
|
+
parts.push(`${msg.meta.duration.toFixed(1)}s`);
|
|
312
|
+
if (state.contextUsagePercent !== null && msg.isFinal) {
|
|
313
|
+
parts.push(`ctx left ${state.contextUsagePercent}%`);
|
|
314
|
+
}
|
|
315
|
+
if (parts.length)
|
|
316
|
+
metaText += ` · ${parts.join(" · ")}`;
|
|
317
|
+
}
|
|
318
|
+
meta.textContent = metaText;
|
|
250
319
|
wrapper.appendChild(meta);
|
|
251
320
|
messagesEl.appendChild(wrapper);
|
|
252
321
|
});
|
|
253
|
-
|
|
322
|
+
// While running, show an inline "Thinking" bubble at the bottom where the
|
|
323
|
+
// final assistant message will appear (even if we don't have streamed text yet).
|
|
324
|
+
if (hasStream || state.status === "running") {
|
|
254
325
|
const streaming = document.createElement("div");
|
|
255
326
|
streaming.className = [
|
|
256
327
|
config.styling.messagesClass,
|
|
@@ -265,12 +336,100 @@ export function createDocChat(config) {
|
|
|
265
336
|
roleLabel.textContent = "Thinking";
|
|
266
337
|
streaming.appendChild(roleLabel);
|
|
267
338
|
const content = document.createElement("div");
|
|
268
|
-
content.className = config.styling.messageContentClass
|
|
269
|
-
|
|
339
|
+
content.className = `${config.styling.messageContentClass} messages-markdown`;
|
|
340
|
+
// If we have streamed text, show it. Otherwise show a compact "working" summary
|
|
341
|
+
// based on the most recent event/tool-call.
|
|
342
|
+
if (state.streamText) {
|
|
343
|
+
content.innerHTML = renderMarkdown(state.streamText);
|
|
344
|
+
decorateFileLinks(content);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
const stepCount = state.totalEvents || state.events.length;
|
|
348
|
+
const statusText = (state.statusText || "").trim();
|
|
349
|
+
const isNoiseEvent = (evt) => {
|
|
350
|
+
const title = (evt.title || "").toLowerCase();
|
|
351
|
+
const method = (evt.method || "").toLowerCase();
|
|
352
|
+
// Hide token/partial deltas; they are too granular for the UI.
|
|
353
|
+
if (title === "delta")
|
|
354
|
+
return true;
|
|
355
|
+
if (method.includes("delta"))
|
|
356
|
+
return true;
|
|
357
|
+
return false;
|
|
358
|
+
};
|
|
359
|
+
const meaningfulEvents = state.events.filter((evt) => !isNoiseEvent(evt));
|
|
360
|
+
const lastMeaningful = meaningfulEvents[meaningfulEvents.length - 1];
|
|
361
|
+
const headline = lastMeaningful
|
|
362
|
+
? (lastMeaningful.title || lastMeaningful.summary || statusText || "Working...")
|
|
363
|
+
: (statusText || "Thinking...");
|
|
364
|
+
// Build DOM so we can attach a "Show details" toggle inside the Thinking bubble.
|
|
365
|
+
content.innerHTML = "";
|
|
366
|
+
const header = document.createElement("div");
|
|
367
|
+
header.className = "chat-thinking-inline";
|
|
368
|
+
const spinner = document.createElement("span");
|
|
369
|
+
spinner.className = "chat-thinking-spinner";
|
|
370
|
+
header.appendChild(spinner);
|
|
371
|
+
const headlineSpan = document.createElement("span");
|
|
372
|
+
headlineSpan.textContent = String(headline);
|
|
373
|
+
header.appendChild(headlineSpan);
|
|
374
|
+
if (stepCount > 0) {
|
|
375
|
+
const steps = document.createElement("span");
|
|
376
|
+
steps.className = "chat-thinking-steps";
|
|
377
|
+
steps.textContent = `(${stepCount} steps)`;
|
|
378
|
+
header.appendChild(steps);
|
|
379
|
+
if (state.contextUsagePercent !== null) {
|
|
380
|
+
const context = document.createElement("span");
|
|
381
|
+
context.className = "chat-thinking-steps";
|
|
382
|
+
context.textContent = ` · ctx left ${state.contextUsagePercent}%`;
|
|
383
|
+
header.appendChild(context);
|
|
384
|
+
}
|
|
385
|
+
// Only show the toggle if we have more than a couple steps.
|
|
386
|
+
if (meaningfulEvents.length > 2) {
|
|
387
|
+
const toggle = document.createElement("button");
|
|
388
|
+
toggle.type = "button";
|
|
389
|
+
toggle.className = "ghost sm chat-thinking-details-btn";
|
|
390
|
+
toggle.textContent = state.eventsExpanded ? "Hide details" : "Show details";
|
|
391
|
+
toggle.addEventListener("click", (e) => {
|
|
392
|
+
e.preventDefault();
|
|
393
|
+
state.eventsExpanded = !state.eventsExpanded;
|
|
394
|
+
renderMessages();
|
|
395
|
+
});
|
|
396
|
+
header.appendChild(toggle);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
content.appendChild(header);
|
|
400
|
+
const maxRecent = state.eventsExpanded
|
|
401
|
+
? Math.min(meaningfulEvents.length, config.limits.eventVisible || 20)
|
|
402
|
+
: 3;
|
|
403
|
+
const recentEvents = meaningfulEvents.slice(-maxRecent);
|
|
404
|
+
if (recentEvents.length) {
|
|
405
|
+
const list = document.createElement("ul");
|
|
406
|
+
list.className = "chat-thinking-steps-list";
|
|
407
|
+
for (const evt of recentEvents) {
|
|
408
|
+
const li = document.createElement("li");
|
|
409
|
+
const title = document.createElement("span");
|
|
410
|
+
title.className = "chat-thinking-step-title";
|
|
411
|
+
title.textContent = (evt.title || evt.kind || evt.method || "step").trim();
|
|
412
|
+
li.appendChild(title);
|
|
413
|
+
const summaryText = (evt.summary || "").trim();
|
|
414
|
+
if (summaryText) {
|
|
415
|
+
const summary = document.createElement("span");
|
|
416
|
+
summary.className = "chat-thinking-step-summary";
|
|
417
|
+
summary.textContent = ` — ${summaryText}`;
|
|
418
|
+
li.appendChild(summary);
|
|
419
|
+
}
|
|
420
|
+
list.appendChild(li);
|
|
421
|
+
}
|
|
422
|
+
content.appendChild(list);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
270
425
|
streaming.appendChild(content);
|
|
271
426
|
messagesEl.appendChild(streaming);
|
|
272
427
|
}
|
|
273
428
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
429
|
+
// Also scroll the parent container if it exists
|
|
430
|
+
if (elements.streamEl) {
|
|
431
|
+
elements.streamEl.scrollTop = elements.streamEl.scrollHeight;
|
|
432
|
+
}
|
|
274
433
|
}
|
|
275
434
|
function render() {
|
|
276
435
|
const { statusEl, errorEl, cancelBtn, newThreadBtn, streamEl, } = elements;
|
|
@@ -292,12 +451,25 @@ export function createDocChat(config) {
|
|
|
292
451
|
newThreadBtn.classList.toggle("hidden", !hasHistory || state.status === "running");
|
|
293
452
|
}
|
|
294
453
|
if (streamEl) {
|
|
454
|
+
// In inline mode, we always want to show the stream element if there's any activity
|
|
455
|
+
// or history, because the "Thinking" state is rendered as a message in the history list
|
|
456
|
+
// (technically in the messagesEl container), but we need the parent container visible.
|
|
295
457
|
const hasContent = state.events.length > 0 ||
|
|
296
458
|
state.messages.length > 0 ||
|
|
297
459
|
!!state.streamText ||
|
|
298
460
|
state.status === "running";
|
|
299
461
|
streamEl.classList.toggle("hidden", !hasContent);
|
|
462
|
+
// Auto-scroll to bottom when new content appears
|
|
463
|
+
streamEl.scrollTop = streamEl.scrollHeight;
|
|
300
464
|
}
|
|
465
|
+
// Important: renderMessages handles the "Thinking" bubble creation
|
|
466
|
+
// when state.status === 'running' or we have a streamText.
|
|
467
|
+
// However, if we only have events but no streamText yet, we need to ensure
|
|
468
|
+
// renderMessages is called with a "virtual" stream state to trigger the bubble.
|
|
469
|
+
// We do this by checking if we are running.
|
|
470
|
+
// We need to pass a flag or rely on state.status in renderMessages?
|
|
471
|
+
// Actually renderMessages uses state.streamText.
|
|
472
|
+
// Let's force a "pending" indicator in renderMessages if running but no text.
|
|
301
473
|
renderEvents();
|
|
302
474
|
renderMessages();
|
|
303
475
|
}
|
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
// GENERATED FILE - do not edit directly. Source: static_src/
|
|
2
2
|
import { resolvePath, getAuthToken, api } from "./utils.js";
|
|
3
|
-
|
|
4
|
-
function parseMaybeJson(data) {
|
|
5
|
-
try {
|
|
6
|
-
return JSON.parse(data);
|
|
7
|
-
}
|
|
8
|
-
catch {
|
|
9
|
-
return data;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
3
|
+
import { extractContextRemainingPercent, readEventStream, parseMaybeJson } from "./streamUtils.js";
|
|
12
4
|
export async function sendFileChat(target, message, controller, handlers = {}, options = {}) {
|
|
13
|
-
const endpoint = resolvePath("/api/file-chat");
|
|
5
|
+
const endpoint = resolvePath(options.basePath || "/api/file-chat");
|
|
14
6
|
const headers = {
|
|
15
7
|
"Content-Type": "application/json",
|
|
16
8
|
};
|
|
@@ -22,6 +14,8 @@ export async function sendFileChat(target, message, controller, handlers = {}, o
|
|
|
22
14
|
message,
|
|
23
15
|
stream: true,
|
|
24
16
|
};
|
|
17
|
+
if (options.clientTurnId)
|
|
18
|
+
payload.client_turn_id = options.clientTurnId;
|
|
25
19
|
if (options.agent)
|
|
26
20
|
payload.agent = options.agent;
|
|
27
21
|
if (options.model)
|
|
@@ -58,36 +52,7 @@ export async function sendFileChat(target, message, controller, handlers = {}, o
|
|
|
58
52
|
}
|
|
59
53
|
}
|
|
60
54
|
async function readFileChatStream(res, handlers) {
|
|
61
|
-
|
|
62
|
-
throw new Error("Streaming not supported in this browser");
|
|
63
|
-
const reader = res.body.getReader();
|
|
64
|
-
let buffer = "";
|
|
65
|
-
for (;;) {
|
|
66
|
-
const { value, done } = await reader.read();
|
|
67
|
-
if (done)
|
|
68
|
-
break;
|
|
69
|
-
buffer += decoder.decode(value, { stream: true });
|
|
70
|
-
const chunks = buffer.split("\n\n");
|
|
71
|
-
buffer = chunks.pop() || "";
|
|
72
|
-
for (const chunk of chunks) {
|
|
73
|
-
if (!chunk.trim())
|
|
74
|
-
continue;
|
|
75
|
-
let event = "message";
|
|
76
|
-
const dataLines = [];
|
|
77
|
-
chunk.split("\n").forEach((line) => {
|
|
78
|
-
if (line.startsWith("event:")) {
|
|
79
|
-
event = line.slice(6).trim();
|
|
80
|
-
}
|
|
81
|
-
else if (line.startsWith("data:")) {
|
|
82
|
-
dataLines.push(line.slice(5).trimStart());
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
if (!dataLines.length)
|
|
86
|
-
continue;
|
|
87
|
-
const rawData = dataLines.join("\n");
|
|
88
|
-
handleStreamEvent(event, rawData, handlers);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
55
|
+
await readEventStream(res, (event, raw) => handleStreamEvent(event, raw, handlers));
|
|
91
56
|
}
|
|
92
57
|
function handleStreamEvent(event, rawData, handlers) {
|
|
93
58
|
const parsed = parseMaybeJson(rawData);
|
|
@@ -104,6 +69,16 @@ function handleStreamEvent(event, rawData, handlers) {
|
|
|
104
69
|
handlers.onToken?.(token);
|
|
105
70
|
break;
|
|
106
71
|
}
|
|
72
|
+
case "token_usage": {
|
|
73
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
74
|
+
const usage = parsed;
|
|
75
|
+
const percent = extractContextRemainingPercent(usage);
|
|
76
|
+
if (percent !== null) {
|
|
77
|
+
handlers.onTokenUsage?.(percent, usage);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
107
82
|
case "update": {
|
|
108
83
|
handlers.onUpdate?.(parsed);
|
|
109
84
|
break;
|
|
@@ -180,3 +155,56 @@ export async function discardDraft(target) {
|
|
|
180
155
|
export async function interruptFileChat(target) {
|
|
181
156
|
await api("/api/file-chat/interrupt", { method: "POST", body: { target } });
|
|
182
157
|
}
|
|
158
|
+
export function newClientTurnId(prefix = "filechat") {
|
|
159
|
+
try {
|
|
160
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto && typeof crypto.randomUUID === "function") {
|
|
161
|
+
return crypto.randomUUID();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// ignore
|
|
166
|
+
}
|
|
167
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
168
|
+
}
|
|
169
|
+
export async function fetchActiveFileChat(clientTurnId, basePath = "/api/file-chat/active") {
|
|
170
|
+
const suffix = clientTurnId ? `?client_turn_id=${encodeURIComponent(clientTurnId)}` : "";
|
|
171
|
+
const path = `${basePath}${suffix}`;
|
|
172
|
+
try {
|
|
173
|
+
const res = (await api(path));
|
|
174
|
+
return res || {};
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
export function streamTurnEvents(meta, handlers = {}) {
|
|
181
|
+
if (!meta.threadId || !meta.turnId)
|
|
182
|
+
return null;
|
|
183
|
+
const ctrl = new AbortController();
|
|
184
|
+
const token = getAuthToken();
|
|
185
|
+
const headers = {};
|
|
186
|
+
if (token)
|
|
187
|
+
headers.Authorization = `Bearer ${token}`;
|
|
188
|
+
const url = resolvePath(`${meta.basePath || "/api/file-chat/turns"}/${encodeURIComponent(meta.turnId)}/events?thread_id=${encodeURIComponent(meta.threadId)}&agent=${encodeURIComponent(meta.agent || "codex")}`);
|
|
189
|
+
void (async () => {
|
|
190
|
+
try {
|
|
191
|
+
const res = await fetch(url, { method: "GET", headers, signal: ctrl.signal });
|
|
192
|
+
if (!res.ok) {
|
|
193
|
+
handlers.onError?.("Failed to stream events");
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const contentType = res.headers.get("content-type") || "";
|
|
197
|
+
if (!contentType.includes("text/event-stream"))
|
|
198
|
+
return;
|
|
199
|
+
await readEventStream(res, (event, raw) => {
|
|
200
|
+
if (event === "app-server" || event === "event") {
|
|
201
|
+
handlers.onEvent?.(parseMaybeJson(raw));
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
handlers.onError?.(err.message || "Event stream failed");
|
|
207
|
+
}
|
|
208
|
+
})();
|
|
209
|
+
return ctrl;
|
|
210
|
+
}
|