codex-autorunner 1.0.0__py3-none-any.whl → 1.2.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.
- codex_autorunner/__init__.py +12 -1
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/runtime.py +59 -18
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +36 -7
- codex_autorunner/bootstrap.py +226 -4
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +20 -0
- codex_autorunner/core/about_car.py +119 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +17 -2
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +6 -2
- codex_autorunner/core/config.py +433 -4
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +96 -2
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +134 -0
- codex_autorunner/core/flows/runtime.py +57 -4
- codex_autorunner/core/flows/store.py +142 -7
- codex_autorunner/core/flows/transition.py +27 -15
- codex_autorunner/core/flows/ux_helpers.py +272 -0
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +291 -20
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +496 -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/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
- 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 +62 -0
- codex_autorunner/core/supervisor_protocol.py +15 -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/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +218 -0
- codex_autorunner/core/ticket_manager_cli.py +494 -0
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/update.py +4 -5
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +125 -15
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
- codex_autorunner/flows/ticket_flow/definition.py +52 -3
- codex_autorunner/integrations/agents/__init__.py +11 -19
- codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +177 -25
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +86 -0
- codex_autorunner/integrations/agents/wiring.py +279 -0
- codex_autorunner/integrations/app_server/client.py +7 -60
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/telegram/adapter.py +65 -0
- codex_autorunner/integrations/telegram/config.py +46 -0
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
- 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 +1496 -71
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
- codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +22 -1
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +45 -10
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
- codex_autorunner/integrations/telegram/transport.py +13 -4
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/routes/__init__.py +37 -76
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +2 -238
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -596
- codex_autorunner/routes/file_chat.py +4 -833
- codex_autorunner/routes/flows.py +4 -977
- codex_autorunner/routes/messages.py +4 -456
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -193
- codex_autorunner/routes/usage.py +2 -86
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +2 -270
- codex_autorunner/server.py +4 -4
- codex_autorunner/static/agentControls.js +61 -16
- codex_autorunner/static/app.js +126 -14
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/dashboard.js +224 -171
- 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 +114 -131
- codex_autorunner/static/index.html +375 -49
- codex_autorunner/static/messages.js +568 -87
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +128 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9798 -6143
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +137 -15
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +821 -98
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +39 -0
- codex_autorunner/static/workspace.js +389 -82
- codex_autorunner/static/workspaceFileBrowser.js +15 -13
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +2534 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2223 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +82 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +284 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +1117 -0
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +1354 -0
- codex_autorunner/surfaces/web/routes/messages.py +490 -0
- codex_autorunner/surfaces/web/routes/pma.py +1652 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +277 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/templates.py +634 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +469 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +8 -1
- codex_autorunner/tickets/agent_pool.py +53 -4
- codex_autorunner/tickets/files.py +37 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +6 -1
- codex_autorunner/tickets/outbox.py +50 -2
- codex_autorunner/tickets/runner.py +396 -57
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1949
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -586
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -376
- codex_autorunner/web/static_assets.py +4 -441
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/paths.py +49 -33
- codex_autorunner-1.2.0.dist-info/METADATA +150 -0
- codex_autorunner-1.2.0.dist-info/RECORD +339 -0
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -2653
- codex_autorunner/core/static_assets.py +0 -55
- codex_autorunner-1.0.0.dist-info/METADATA +0 -246
- codex_autorunner-1.0.0.dist-info/RECORD +0 -251
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1167 @@
|
|
|
1
|
+
// GENERATED FILE - do not edit directly. Source: static_src/
|
|
2
|
+
/**
|
|
3
|
+
* PMA (Project Management Agent) - Hub-level chat interface
|
|
4
|
+
*/
|
|
5
|
+
import { api, confirmModal, resolvePath, getAuthToken, flash } from "./utils.js";
|
|
6
|
+
import { createDocChat, } from "./docChatCore.js";
|
|
7
|
+
import { initChatPasteUpload } from "./chatUploads.js";
|
|
8
|
+
import { clearAgentSelectionStorage, getSelectedAgent, getSelectedModel, getSelectedReasoning, initAgentControls, refreshAgentControls, } from "./agentControls.js";
|
|
9
|
+
import { createFileBoxWidget } from "./fileboxUi.js";
|
|
10
|
+
import { extractContextRemainingPercent } from "./streamUtils.js";
|
|
11
|
+
const pmaStyling = {
|
|
12
|
+
eventClass: "chat-event",
|
|
13
|
+
eventTitleClass: "chat-event-title",
|
|
14
|
+
eventSummaryClass: "chat-event-summary",
|
|
15
|
+
eventDetailClass: "chat-event-detail",
|
|
16
|
+
eventMetaClass: "chat-event-meta",
|
|
17
|
+
eventsEmptyClass: "chat-events-empty",
|
|
18
|
+
messagesClass: "chat-message",
|
|
19
|
+
messageRoleClass: "chat-message-role",
|
|
20
|
+
messageContentClass: "chat-message-content",
|
|
21
|
+
messageMetaClass: "chat-message-meta",
|
|
22
|
+
messageUserClass: "chat-message-user",
|
|
23
|
+
messageAssistantClass: "chat-message-assistant",
|
|
24
|
+
messageAssistantThinkingClass: "chat-message-assistant-thinking",
|
|
25
|
+
messageAssistantFinalClass: "chat-message-assistant-final",
|
|
26
|
+
};
|
|
27
|
+
const EDITABLE_DOCS = ["AGENTS.md", "active_context.md"];
|
|
28
|
+
let activeContextMaxLines = 200;
|
|
29
|
+
const pmaConfig = {
|
|
30
|
+
idPrefix: "pma-chat",
|
|
31
|
+
storage: { keyPrefix: "car.pma.", maxMessages: 100, version: 1 },
|
|
32
|
+
limits: {
|
|
33
|
+
eventVisible: 20,
|
|
34
|
+
eventMax: 50,
|
|
35
|
+
},
|
|
36
|
+
styling: pmaStyling,
|
|
37
|
+
compactMode: true,
|
|
38
|
+
inlineEvents: true,
|
|
39
|
+
};
|
|
40
|
+
let pmaChat = null;
|
|
41
|
+
let currentController = null;
|
|
42
|
+
let currentOutboxBaseline = null;
|
|
43
|
+
let isUnloading = false;
|
|
44
|
+
let unloadHandlerInstalled = false;
|
|
45
|
+
let currentEventsController = null;
|
|
46
|
+
const PMA_PENDING_TURN_KEY = "car.pma.pendingTurn";
|
|
47
|
+
const DEFAULT_PMA_LANE_ID = "pma:default";
|
|
48
|
+
let fileBoxCtrl = null;
|
|
49
|
+
let pendingUploadNames = [];
|
|
50
|
+
let currentDocName = null;
|
|
51
|
+
const docsInfo = new Map();
|
|
52
|
+
let isSavingDoc = false;
|
|
53
|
+
function newClientTurnId() {
|
|
54
|
+
// crypto.randomUUID is not guaranteed everywhere; keep a safe fallback.
|
|
55
|
+
try {
|
|
56
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto && typeof crypto.randomUUID === "function") {
|
|
57
|
+
return crypto.randomUUID();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// ignore
|
|
62
|
+
}
|
|
63
|
+
return `pma-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
64
|
+
}
|
|
65
|
+
function loadPendingTurn() {
|
|
66
|
+
try {
|
|
67
|
+
const raw = localStorage.getItem(PMA_PENDING_TURN_KEY);
|
|
68
|
+
if (!raw)
|
|
69
|
+
return null;
|
|
70
|
+
const parsed = JSON.parse(raw);
|
|
71
|
+
if (!parsed || typeof parsed !== "object")
|
|
72
|
+
return null;
|
|
73
|
+
if (!parsed.clientTurnId || !parsed.message || !parsed.startedAtMs)
|
|
74
|
+
return null;
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function savePendingTurn(turn) {
|
|
82
|
+
try {
|
|
83
|
+
localStorage.setItem(PMA_PENDING_TURN_KEY, JSON.stringify(turn));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// ignore
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function clearPendingTurn() {
|
|
90
|
+
try {
|
|
91
|
+
localStorage.removeItem(PMA_PENDING_TURN_KEY);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// ignore
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function initFileBoxUI() {
|
|
98
|
+
const elements = getElements();
|
|
99
|
+
if (!elements.inboxFiles || !elements.outboxFiles)
|
|
100
|
+
return;
|
|
101
|
+
fileBoxCtrl = createFileBoxWidget({
|
|
102
|
+
scope: "pma",
|
|
103
|
+
basePath: "/hub/pma/files",
|
|
104
|
+
inboxEl: elements.inboxFiles,
|
|
105
|
+
outboxEl: elements.outboxFiles,
|
|
106
|
+
uploadInput: elements.chatUploadInput,
|
|
107
|
+
uploadBtn: elements.chatUploadBtn,
|
|
108
|
+
refreshBtn: elements.outboxRefresh,
|
|
109
|
+
uploadBox: "inbox",
|
|
110
|
+
emptyMessage: "No files",
|
|
111
|
+
onChange: (listing) => {
|
|
112
|
+
if (pendingUploadNames.length && pmaChat) {
|
|
113
|
+
const links = pendingUploadNames
|
|
114
|
+
.map((name) => {
|
|
115
|
+
const match = listing.inbox.find((e) => e.name === name);
|
|
116
|
+
const href = match?.url ? resolvePath(match.url) : "";
|
|
117
|
+
const text = escapeMarkdownLinkText(name);
|
|
118
|
+
return href ? `[${text}](${href})` : text;
|
|
119
|
+
})
|
|
120
|
+
.join("\n");
|
|
121
|
+
if (links) {
|
|
122
|
+
pmaChat.addUserMessage(`**Inbox files (uploaded):**\n${links}`);
|
|
123
|
+
pmaChat.render();
|
|
124
|
+
}
|
|
125
|
+
pendingUploadNames = [];
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
onUpload: (names) => {
|
|
129
|
+
pendingUploadNames = names;
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
await fileBoxCtrl.refresh();
|
|
133
|
+
}
|
|
134
|
+
function stopTurnEventsStream() {
|
|
135
|
+
if (currentEventsController) {
|
|
136
|
+
try {
|
|
137
|
+
currentEventsController.abort();
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// ignore
|
|
141
|
+
}
|
|
142
|
+
currentEventsController = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function sleep(ms) {
|
|
146
|
+
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
147
|
+
}
|
|
148
|
+
async function loadPMADocs() {
|
|
149
|
+
try {
|
|
150
|
+
const payload = (await api("/hub/pma/docs", { method: "GET" }));
|
|
151
|
+
docsInfo.clear();
|
|
152
|
+
if (payload?.docs) {
|
|
153
|
+
payload.docs.forEach((doc) => {
|
|
154
|
+
docsInfo.set(doc.name, doc);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
activeContextMaxLines =
|
|
158
|
+
typeof payload?.active_context_max_lines === "number"
|
|
159
|
+
? payload.active_context_max_lines
|
|
160
|
+
: 200;
|
|
161
|
+
renderPMADocsMeta();
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
flash("Failed to load PMA docs", "error");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function loadPMADocContent(name) {
|
|
168
|
+
try {
|
|
169
|
+
const payload = (await api(`/hub/pma/docs/${encodeURIComponent(name)}`, {
|
|
170
|
+
method: "GET",
|
|
171
|
+
}));
|
|
172
|
+
return payload?.content || "";
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
flash(`Failed to load ${name}`, "error");
|
|
176
|
+
return "";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function loadPMADocDefaultContent(name) {
|
|
180
|
+
try {
|
|
181
|
+
const payload = (await api(`/hub/pma/docs/default/${encodeURIComponent(name)}`, {
|
|
182
|
+
method: "GET",
|
|
183
|
+
}));
|
|
184
|
+
return payload?.content || "";
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
flash(`Failed to load default ${name}`, "error");
|
|
188
|
+
return "";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async function savePMADoc(name, content) {
|
|
192
|
+
if (isSavingDoc)
|
|
193
|
+
return;
|
|
194
|
+
isSavingDoc = true;
|
|
195
|
+
try {
|
|
196
|
+
const payload = (await api(`/hub/pma/docs/${encodeURIComponent(name)}`, {
|
|
197
|
+
method: "PUT",
|
|
198
|
+
body: { content },
|
|
199
|
+
}));
|
|
200
|
+
if (payload?.status === "ok") {
|
|
201
|
+
flash(`Saved ${name}`, "info");
|
|
202
|
+
await loadPMADocs();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
flash(`Failed to save ${name}`, "error");
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
isSavingDoc = false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function renderPMADocsMeta() {
|
|
213
|
+
const metaEl = document.getElementById("pma-docs-meta");
|
|
214
|
+
if (!metaEl)
|
|
215
|
+
return;
|
|
216
|
+
const activeInfo = docsInfo.get("active_context.md");
|
|
217
|
+
if (!activeInfo) {
|
|
218
|
+
metaEl.innerHTML = "";
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const lineCount = activeInfo.line_count || 0;
|
|
222
|
+
const maxLines = activeContextMaxLines || 200;
|
|
223
|
+
const percent = Math.min(100, Math.round((lineCount / maxLines) * 100));
|
|
224
|
+
const statusClass = percent >= 90 ? "pill-warn" : percent >= 70 ? "pill-caution" : "pill-idle";
|
|
225
|
+
metaEl.innerHTML = `
|
|
226
|
+
<div class="pma-docs-meta-item">
|
|
227
|
+
<span class="muted">Active context:</span>
|
|
228
|
+
<span class="pill pill-small ${statusClass}">${lineCount} / ${maxLines} lines</span>
|
|
229
|
+
</div>
|
|
230
|
+
`;
|
|
231
|
+
}
|
|
232
|
+
function switchPMADoc(name) {
|
|
233
|
+
currentDocName = name;
|
|
234
|
+
const tabs = document.querySelectorAll(".pma-docs-tab");
|
|
235
|
+
tabs.forEach((tab) => {
|
|
236
|
+
if (tab instanceof HTMLElement && tab.dataset.doc === name) {
|
|
237
|
+
tab.classList.add("active");
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
tab.classList.remove("active");
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
const editor = document.getElementById("pma-docs-editor");
|
|
244
|
+
const resetBtn = document.getElementById("pma-docs-reset");
|
|
245
|
+
const snapshotBtn = document.getElementById("pma-docs-snapshot");
|
|
246
|
+
const saveBtn = document.getElementById("pma-docs-save");
|
|
247
|
+
if (!editor)
|
|
248
|
+
return;
|
|
249
|
+
const isEditable = EDITABLE_DOCS.includes(name);
|
|
250
|
+
editor.readOnly = !isEditable;
|
|
251
|
+
if (resetBtn)
|
|
252
|
+
resetBtn.disabled = name !== "active_context.md";
|
|
253
|
+
if (snapshotBtn)
|
|
254
|
+
snapshotBtn.disabled = name !== "active_context.md";
|
|
255
|
+
if (saveBtn)
|
|
256
|
+
saveBtn.disabled = !isEditable;
|
|
257
|
+
void loadPMADocContent(name).then((content) => {
|
|
258
|
+
editor.value = content;
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
async function snapshotActiveContext() {
|
|
262
|
+
const editor = document.getElementById("pma-docs-editor");
|
|
263
|
+
if (!editor)
|
|
264
|
+
return;
|
|
265
|
+
try {
|
|
266
|
+
const payload = (await api("/hub/pma/context/snapshot", {
|
|
267
|
+
method: "POST",
|
|
268
|
+
body: { reset: true },
|
|
269
|
+
}));
|
|
270
|
+
if (payload?.status !== "ok") {
|
|
271
|
+
throw new Error("snapshot failed");
|
|
272
|
+
}
|
|
273
|
+
const resetContent = await loadPMADocContent("active_context.md");
|
|
274
|
+
editor.value = resetContent;
|
|
275
|
+
const message = payload?.warning
|
|
276
|
+
? `Active context snapshot saved (${payload.warning})`
|
|
277
|
+
: "Active context snapshot saved";
|
|
278
|
+
flash(message, "info");
|
|
279
|
+
await loadPMADocs();
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
flash("Failed to snapshot active context", "error");
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function resetActiveContext() {
|
|
286
|
+
const confirmed = await confirmModal("Reset active context to default?");
|
|
287
|
+
if (!confirmed)
|
|
288
|
+
return;
|
|
289
|
+
const editor = document.getElementById("pma-docs-editor");
|
|
290
|
+
if (!editor)
|
|
291
|
+
return;
|
|
292
|
+
void loadPMADocDefaultContent("active_context.md").then((resetContent) => {
|
|
293
|
+
if (!resetContent)
|
|
294
|
+
return;
|
|
295
|
+
editor.value = resetContent;
|
|
296
|
+
void savePMADoc("active_context.md", resetContent);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
async function startTurnEventsStream(meta) {
|
|
300
|
+
stopTurnEventsStream();
|
|
301
|
+
if (!meta.threadId || !meta.turnId)
|
|
302
|
+
return;
|
|
303
|
+
const ctrl = new AbortController();
|
|
304
|
+
currentEventsController = ctrl;
|
|
305
|
+
const token = getAuthToken();
|
|
306
|
+
const headers = {};
|
|
307
|
+
if (token)
|
|
308
|
+
headers.Authorization = `Bearer ${token}`;
|
|
309
|
+
const url = resolvePath(`/hub/pma/turns/${encodeURIComponent(meta.turnId)}/events?thread_id=${encodeURIComponent(meta.threadId)}&agent=${encodeURIComponent(meta.agent || "codex")}`);
|
|
310
|
+
try {
|
|
311
|
+
const res = await fetch(url, {
|
|
312
|
+
method: "GET",
|
|
313
|
+
headers,
|
|
314
|
+
signal: ctrl.signal,
|
|
315
|
+
});
|
|
316
|
+
if (!res.ok)
|
|
317
|
+
return;
|
|
318
|
+
const contentType = res.headers.get("content-type") || "";
|
|
319
|
+
if (!contentType.includes("text/event-stream"))
|
|
320
|
+
return;
|
|
321
|
+
await readPMAStream(res, false);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// ignore (abort / network)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
async function pollForTurnMeta(clientTurnId, options = {}) {
|
|
328
|
+
if (!clientTurnId)
|
|
329
|
+
return;
|
|
330
|
+
const timeoutMs = options.timeoutMs ?? 8000;
|
|
331
|
+
const started = Date.now();
|
|
332
|
+
while (Date.now() - started < timeoutMs) {
|
|
333
|
+
if (!pmaChat || pmaChat.state.status !== "running")
|
|
334
|
+
return;
|
|
335
|
+
if (currentEventsController)
|
|
336
|
+
return;
|
|
337
|
+
if (options.signal?.aborted)
|
|
338
|
+
return;
|
|
339
|
+
try {
|
|
340
|
+
const payload = (await api(`/hub/pma/active?client_turn_id=${encodeURIComponent(clientTurnId)}`, { method: "GET" }));
|
|
341
|
+
const cur = (payload.current || {});
|
|
342
|
+
const threadId = typeof cur.thread_id === "string" ? cur.thread_id : "";
|
|
343
|
+
const turnId = typeof cur.turn_id === "string" ? cur.turn_id : "";
|
|
344
|
+
const agent = typeof cur.agent === "string" ? cur.agent : "codex";
|
|
345
|
+
if (threadId && turnId) {
|
|
346
|
+
void startTurnEventsStream({ agent, threadId, turnId });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
// ignore and retry
|
|
352
|
+
}
|
|
353
|
+
await sleep(250);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function getElements() {
|
|
357
|
+
return {
|
|
358
|
+
shell: document.getElementById("pma-shell"),
|
|
359
|
+
input: document.getElementById("pma-chat-input"),
|
|
360
|
+
sendBtn: document.getElementById("pma-chat-send"),
|
|
361
|
+
cancelBtn: document.getElementById("pma-chat-cancel"),
|
|
362
|
+
newThreadBtn: document.getElementById("pma-chat-new-thread"),
|
|
363
|
+
statusEl: document.getElementById("pma-chat-status"),
|
|
364
|
+
errorEl: document.getElementById("pma-chat-error"),
|
|
365
|
+
streamEl: document.getElementById("pma-chat-stream"),
|
|
366
|
+
eventsMain: document.getElementById("pma-chat-events"),
|
|
367
|
+
eventsList: document.getElementById("pma-chat-events-list"),
|
|
368
|
+
eventsToggle: document.getElementById("pma-chat-events-toggle"),
|
|
369
|
+
messagesEl: document.getElementById("pma-chat-messages"),
|
|
370
|
+
historyHeader: document.getElementById("pma-chat-history-header"),
|
|
371
|
+
agentSelect: document.getElementById("pma-chat-agent-select"),
|
|
372
|
+
modelSelect: document.getElementById("pma-chat-model-select"),
|
|
373
|
+
reasoningSelect: document.getElementById("pma-chat-reasoning-select"),
|
|
374
|
+
chatUploadInput: document.getElementById("pma-chat-upload-input"),
|
|
375
|
+
chatUploadBtn: document.getElementById("pma-chat-upload-btn"),
|
|
376
|
+
inboxFiles: document.getElementById("pma-inbox-files"),
|
|
377
|
+
outboxFiles: document.getElementById("pma-outbox-files"),
|
|
378
|
+
outboxRefresh: document.getElementById("pma-outbox-refresh"),
|
|
379
|
+
threadInfo: document.getElementById("pma-thread-info"),
|
|
380
|
+
threadInfoAgent: document.getElementById("pma-thread-info-agent"),
|
|
381
|
+
threadInfoThreadId: document.getElementById("pma-thread-info-thread-id"),
|
|
382
|
+
threadInfoTurnId: document.getElementById("pma-thread-info-turn-id"),
|
|
383
|
+
threadInfoStatus: document.getElementById("pma-thread-info-status"),
|
|
384
|
+
repoActions: document.getElementById("pma-repo-actions"),
|
|
385
|
+
scanReposBtn: document.getElementById("pma-scan-repos-btn"),
|
|
386
|
+
docsSection: document.getElementById("pma-docs-section"),
|
|
387
|
+
docsEditor: document.getElementById("pma-docs-editor"),
|
|
388
|
+
docsSaveBtn: document.getElementById("pma-docs-save"),
|
|
389
|
+
docsResetBtn: document.getElementById("pma-docs-reset"),
|
|
390
|
+
docsSnapshotBtn: document.getElementById("pma-docs-snapshot"),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const decoder = new TextDecoder();
|
|
394
|
+
function escapeMarkdownLinkText(text) {
|
|
395
|
+
// Keep this ES2019-compatible (no String.prototype.replaceAll).
|
|
396
|
+
return text.replace(/\[/g, "\\[").replace(/\]/g, "\\]");
|
|
397
|
+
}
|
|
398
|
+
function formatOutboxAttachments(listing, names) {
|
|
399
|
+
if (!listing || !names.length)
|
|
400
|
+
return "";
|
|
401
|
+
const lines = names.map((name) => {
|
|
402
|
+
const entry = listing.outbox.find((e) => e.name === name);
|
|
403
|
+
const href = entry?.url ? new URL(resolvePath(entry.url), window.location.origin).toString() : "";
|
|
404
|
+
const label = escapeMarkdownLinkText(name);
|
|
405
|
+
return href ? `[${label}](${href})` : label;
|
|
406
|
+
});
|
|
407
|
+
return lines.length ? `**Outbox files (download):**\n${lines.join("\n")}` : "";
|
|
408
|
+
}
|
|
409
|
+
async function finalizePMAResponse(responseText) {
|
|
410
|
+
if (!pmaChat)
|
|
411
|
+
return;
|
|
412
|
+
let attachments = "";
|
|
413
|
+
try {
|
|
414
|
+
if (fileBoxCtrl) {
|
|
415
|
+
const current = await fileBoxCtrl.refresh();
|
|
416
|
+
if (currentOutboxBaseline) {
|
|
417
|
+
const baseline = currentOutboxBaseline;
|
|
418
|
+
const added = (current.outbox || []).map((e) => e.name).filter((name) => !baseline.has(name));
|
|
419
|
+
attachments = formatOutboxAttachments(current, added);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
attachments = "";
|
|
425
|
+
}
|
|
426
|
+
finally {
|
|
427
|
+
currentOutboxBaseline = null;
|
|
428
|
+
clearPendingTurn();
|
|
429
|
+
stopTurnEventsStream();
|
|
430
|
+
}
|
|
431
|
+
const trimmed = (responseText || "").trim();
|
|
432
|
+
const content = trimmed
|
|
433
|
+
? (attachments ? `${trimmed}\n\n---\n\n${attachments}` : trimmed)
|
|
434
|
+
: attachments;
|
|
435
|
+
const startTime = pmaChat.state.startTime;
|
|
436
|
+
const duration = startTime ? (Date.now() - startTime) / 1000 : undefined;
|
|
437
|
+
const steps = pmaChat.state.totalEvents || pmaChat.state.events.length;
|
|
438
|
+
if (content) {
|
|
439
|
+
pmaChat.addAssistantMessage(content, true, { steps, duration });
|
|
440
|
+
}
|
|
441
|
+
pmaChat.state.streamText = "";
|
|
442
|
+
pmaChat.state.status = "done";
|
|
443
|
+
pmaChat.render();
|
|
444
|
+
pmaChat.renderMessages();
|
|
445
|
+
pmaChat.renderEvents();
|
|
446
|
+
void fileBoxCtrl?.refresh();
|
|
447
|
+
}
|
|
448
|
+
async function initPMA() {
|
|
449
|
+
const elements = getElements();
|
|
450
|
+
if (!elements.shell)
|
|
451
|
+
return;
|
|
452
|
+
pmaChat = createDocChat(pmaConfig);
|
|
453
|
+
pmaChat.setTarget("pma");
|
|
454
|
+
pmaChat.render();
|
|
455
|
+
// Ensure we start at the bottom
|
|
456
|
+
setTimeout(() => {
|
|
457
|
+
const stream = document.getElementById("pma-chat-stream");
|
|
458
|
+
const messages = document.getElementById("pma-chat-messages");
|
|
459
|
+
if (stream)
|
|
460
|
+
stream.scrollTop = stream.scrollHeight;
|
|
461
|
+
if (messages)
|
|
462
|
+
messages.scrollTop = messages.scrollHeight;
|
|
463
|
+
}, 100);
|
|
464
|
+
initAgentControls({
|
|
465
|
+
agentSelect: elements.agentSelect,
|
|
466
|
+
modelSelect: elements.modelSelect,
|
|
467
|
+
reasoningSelect: elements.reasoningSelect,
|
|
468
|
+
});
|
|
469
|
+
await refreshAgentControls({ force: true, reason: "initial" });
|
|
470
|
+
await loadPMAThreadInfo();
|
|
471
|
+
await initFileBoxUI();
|
|
472
|
+
await loadPMADocs();
|
|
473
|
+
attachHandlers();
|
|
474
|
+
// If we refreshed mid-turn, recover the final output from the server.
|
|
475
|
+
await resumePendingTurn();
|
|
476
|
+
// If the page refreshes/navigates while a turn is running, avoid showing a noisy
|
|
477
|
+
// "network error" and proactively interrupt the running turn on the server to
|
|
478
|
+
// prevent the next request from receiving a stale/previous response.
|
|
479
|
+
if (!unloadHandlerInstalled) {
|
|
480
|
+
unloadHandlerInstalled = true;
|
|
481
|
+
window.addEventListener("beforeunload", () => {
|
|
482
|
+
isUnloading = true;
|
|
483
|
+
// Abort any in-flight request immediately.
|
|
484
|
+
// Note: we do NOT send an interrupt request to the server; the run continues
|
|
485
|
+
// in the background and can be recovered after reload via /hub/pma/active.
|
|
486
|
+
if (currentController) {
|
|
487
|
+
try {
|
|
488
|
+
currentController.abort();
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
// ignore
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
// Periodically refresh thread info
|
|
497
|
+
setInterval(() => {
|
|
498
|
+
void loadPMAThreadInfo();
|
|
499
|
+
void fileBoxCtrl?.refresh();
|
|
500
|
+
}, 30000);
|
|
501
|
+
}
|
|
502
|
+
async function loadPMAThreadInfo() {
|
|
503
|
+
const elements = getElements();
|
|
504
|
+
if (!elements.threadInfo)
|
|
505
|
+
return;
|
|
506
|
+
try {
|
|
507
|
+
const payload = (await api("/hub/pma/active", { method: "GET" }));
|
|
508
|
+
const current = payload.current || {};
|
|
509
|
+
const last = payload.last_result || {};
|
|
510
|
+
const info = (payload.active && current.thread_id) ? current : last;
|
|
511
|
+
if (!info || !info.thread_id) {
|
|
512
|
+
elements.threadInfo.classList.add("hidden");
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (elements.threadInfoAgent) {
|
|
516
|
+
elements.threadInfoAgent.textContent = String(info.agent || "unknown");
|
|
517
|
+
}
|
|
518
|
+
if (elements.threadInfoThreadId) {
|
|
519
|
+
const threadId = String(info.thread_id || "");
|
|
520
|
+
elements.threadInfoThreadId.textContent = threadId.slice(0, 12);
|
|
521
|
+
elements.threadInfoThreadId.title = threadId;
|
|
522
|
+
}
|
|
523
|
+
if (elements.threadInfoTurnId) {
|
|
524
|
+
const turnId = String(info.turn_id || "");
|
|
525
|
+
elements.threadInfoTurnId.textContent = turnId.slice(0, 12);
|
|
526
|
+
elements.threadInfoTurnId.title = turnId;
|
|
527
|
+
}
|
|
528
|
+
if (elements.threadInfoStatus) {
|
|
529
|
+
const status = String(info.status || (payload.active ? "active" : "idle"));
|
|
530
|
+
elements.threadInfoStatus.textContent = status;
|
|
531
|
+
if (payload.active) {
|
|
532
|
+
elements.threadInfoStatus.classList.add("pill-warn");
|
|
533
|
+
elements.threadInfoStatus.classList.remove("pill-idle");
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
elements.threadInfoStatus.classList.add("pill-idle");
|
|
537
|
+
elements.threadInfoStatus.classList.remove("pill-warn");
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
elements.threadInfo.classList.remove("hidden");
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
elements.threadInfo?.classList.add("hidden");
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async function sendMessage() {
|
|
547
|
+
const elements = getElements();
|
|
548
|
+
if (!elements.input || !pmaChat)
|
|
549
|
+
return;
|
|
550
|
+
const message = elements.input.value?.trim() || "";
|
|
551
|
+
if (!message)
|
|
552
|
+
return;
|
|
553
|
+
if (currentController) {
|
|
554
|
+
void cancelRequest({ clearPending: true, interruptServer: true });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
// Ensure prior turn event streams are cleared so we don't render stale actions.
|
|
558
|
+
stopTurnEventsStream();
|
|
559
|
+
elements.input.value = "";
|
|
560
|
+
elements.input.style.height = "auto";
|
|
561
|
+
const agent = elements.agentSelect?.value || getSelectedAgent();
|
|
562
|
+
const model = elements.modelSelect?.value || getSelectedModel(agent);
|
|
563
|
+
const reasoning = elements.reasoningSelect?.value || getSelectedReasoning(agent);
|
|
564
|
+
const clientTurnId = newClientTurnId();
|
|
565
|
+
savePendingTurn({ clientTurnId, message, startedAtMs: Date.now() });
|
|
566
|
+
currentController = new AbortController();
|
|
567
|
+
pmaChat.state.controller = currentController;
|
|
568
|
+
pmaChat.state.status = "running";
|
|
569
|
+
pmaChat.state.error = "";
|
|
570
|
+
pmaChat.state.statusText = "";
|
|
571
|
+
pmaChat.state.contextUsagePercent = null;
|
|
572
|
+
pmaChat.state.streamText = "";
|
|
573
|
+
pmaChat.state.contextUsagePercent = null;
|
|
574
|
+
pmaChat.state.startTime = Date.now();
|
|
575
|
+
pmaChat.clearEvents();
|
|
576
|
+
pmaChat.addUserMessage(message);
|
|
577
|
+
pmaChat.render();
|
|
578
|
+
pmaChat.renderMessages();
|
|
579
|
+
try {
|
|
580
|
+
try {
|
|
581
|
+
const listing = fileBoxCtrl ? await fileBoxCtrl.refresh() : null;
|
|
582
|
+
const names = listing?.outbox?.map((e) => e.name) || [];
|
|
583
|
+
currentOutboxBaseline = new Set(names);
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
currentOutboxBaseline = new Set();
|
|
587
|
+
}
|
|
588
|
+
const endpoint = resolvePath("/hub/pma/chat");
|
|
589
|
+
const headers = {
|
|
590
|
+
"Content-Type": "application/json",
|
|
591
|
+
};
|
|
592
|
+
const token = getAuthToken();
|
|
593
|
+
if (token) {
|
|
594
|
+
headers.Authorization = `Bearer ${token}`;
|
|
595
|
+
}
|
|
596
|
+
const payload = {
|
|
597
|
+
message,
|
|
598
|
+
stream: true,
|
|
599
|
+
client_turn_id: clientTurnId,
|
|
600
|
+
};
|
|
601
|
+
if (agent)
|
|
602
|
+
payload.agent = agent;
|
|
603
|
+
if (model)
|
|
604
|
+
payload.model = model;
|
|
605
|
+
if (reasoning)
|
|
606
|
+
payload.reasoning = reasoning;
|
|
607
|
+
const responsePromise = fetch(endpoint, {
|
|
608
|
+
method: "POST",
|
|
609
|
+
headers,
|
|
610
|
+
body: JSON.stringify(payload),
|
|
611
|
+
signal: currentController.signal,
|
|
612
|
+
});
|
|
613
|
+
// Stream tool calls/events separately as soon as we have (thread_id, turn_id).
|
|
614
|
+
// The main /hub/pma/chat stream only emits a final "update"/"done" today.
|
|
615
|
+
void pollForTurnMeta(clientTurnId, { signal: currentController.signal });
|
|
616
|
+
const res = await responsePromise;
|
|
617
|
+
if (!res.ok) {
|
|
618
|
+
const text = await res.text();
|
|
619
|
+
let detail = text;
|
|
620
|
+
try {
|
|
621
|
+
const parsed = JSON.parse(text);
|
|
622
|
+
detail = parsed.detail || parsed.error || text;
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
// ignore parse errors
|
|
626
|
+
}
|
|
627
|
+
throw new Error(detail || `Request failed (${res.status})`);
|
|
628
|
+
}
|
|
629
|
+
const contentType = res.headers.get("content-type") || "";
|
|
630
|
+
if (contentType.includes("text/event-stream")) {
|
|
631
|
+
await readPMAStream(res, true);
|
|
632
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
const responsePayload = contentType.includes("application/json")
|
|
635
|
+
? await res.json()
|
|
636
|
+
: await res.text();
|
|
637
|
+
applyPMAResult(responsePayload);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
catch (err) {
|
|
641
|
+
// Aborts (including page refresh) shouldn't create an error message that pollutes history.
|
|
642
|
+
const name = err && typeof err === "object" && "name" in err
|
|
643
|
+
? String(err.name || "")
|
|
644
|
+
: "";
|
|
645
|
+
if (isUnloading || name === "AbortError") {
|
|
646
|
+
pmaChat.state.status = "interrupted";
|
|
647
|
+
pmaChat.state.error = "";
|
|
648
|
+
pmaChat.state.statusText = isUnloading ? "Cancelled (page reload)" : "Cancelled";
|
|
649
|
+
pmaChat.render();
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const errorMsg = err.message || "Request failed";
|
|
653
|
+
pmaChat.state.status = "error";
|
|
654
|
+
pmaChat.state.error = errorMsg;
|
|
655
|
+
pmaChat.addAssistantMessage(`Error: ${errorMsg}`, true);
|
|
656
|
+
pmaChat.render();
|
|
657
|
+
pmaChat.renderMessages();
|
|
658
|
+
clearPendingTurn();
|
|
659
|
+
stopTurnEventsStream();
|
|
660
|
+
}
|
|
661
|
+
finally {
|
|
662
|
+
currentController = null;
|
|
663
|
+
pmaChat.state.controller = null;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async function readPMAStream(res, finalizeOnClose = false) {
|
|
667
|
+
if (!res.body)
|
|
668
|
+
throw new Error("Streaming not supported in this browser");
|
|
669
|
+
const reader = res.body.getReader();
|
|
670
|
+
let buffer = "";
|
|
671
|
+
let escapedNewlines = false;
|
|
672
|
+
for (;;) {
|
|
673
|
+
const { value, done } = await reader.read();
|
|
674
|
+
if (done)
|
|
675
|
+
break;
|
|
676
|
+
const decoded = decoder.decode(value, { stream: true });
|
|
677
|
+
if (!escapedNewlines) {
|
|
678
|
+
const combined = buffer + decoded;
|
|
679
|
+
if (!combined.includes("\n") && combined.includes("\\n")) {
|
|
680
|
+
escapedNewlines = true;
|
|
681
|
+
buffer = buffer.replace(/\\n(?=event:|data:|\\n)/g, "\n");
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
buffer += escapedNewlines
|
|
685
|
+
? decoded.replace(/\\n(?=event:|data:|\\n)/g, "\n")
|
|
686
|
+
: decoded;
|
|
687
|
+
const chunks = buffer.split("\n\n");
|
|
688
|
+
buffer = chunks.pop() || "";
|
|
689
|
+
for (const chunk of chunks) {
|
|
690
|
+
if (!chunk.trim())
|
|
691
|
+
continue;
|
|
692
|
+
let event = "message";
|
|
693
|
+
const dataLines = [];
|
|
694
|
+
chunk.split("\n").forEach((line) => {
|
|
695
|
+
if (line.startsWith("event:")) {
|
|
696
|
+
event = line.slice(6).trim();
|
|
697
|
+
}
|
|
698
|
+
else if (line.startsWith("data:")) {
|
|
699
|
+
dataLines.push(line.slice(5).trimStart());
|
|
700
|
+
}
|
|
701
|
+
else if (line.trim()) {
|
|
702
|
+
dataLines.push(line);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
if (dataLines.length === 0)
|
|
706
|
+
continue;
|
|
707
|
+
const data = dataLines.join("\n");
|
|
708
|
+
handlePMAStreamEvent(event, data);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if (buffer.trim()) {
|
|
712
|
+
let event = "message";
|
|
713
|
+
const dataLines = [];
|
|
714
|
+
buffer.split("\n").forEach((line) => {
|
|
715
|
+
if (line.startsWith("event:")) {
|
|
716
|
+
event = line.slice(6).trim();
|
|
717
|
+
}
|
|
718
|
+
else if (line.startsWith("data:")) {
|
|
719
|
+
dataLines.push(line.slice(5).trimStart());
|
|
720
|
+
}
|
|
721
|
+
else if (line.trim()) {
|
|
722
|
+
dataLines.push(line);
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
if (dataLines.length) {
|
|
726
|
+
handlePMAStreamEvent(event, dataLines.join("\n"));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (finalizeOnClose && pmaChat && pmaChat.state.status === "running") {
|
|
730
|
+
const responseText = pmaChat.state.streamText || "";
|
|
731
|
+
if (responseText.trim()) {
|
|
732
|
+
void finalizePMAResponse(responseText);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function handlePMAStreamEvent(event, rawData) {
|
|
737
|
+
const parsed = parseMaybeJson(rawData);
|
|
738
|
+
switch (event) {
|
|
739
|
+
case "status": {
|
|
740
|
+
const status = typeof parsed === "string"
|
|
741
|
+
? parsed
|
|
742
|
+
: parsed.status || "";
|
|
743
|
+
pmaChat.state.statusText = status;
|
|
744
|
+
pmaChat.render();
|
|
745
|
+
pmaChat.renderEvents();
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
case "token": {
|
|
749
|
+
const token = typeof parsed === "string"
|
|
750
|
+
? parsed
|
|
751
|
+
: parsed.token ||
|
|
752
|
+
parsed.text ||
|
|
753
|
+
rawData ||
|
|
754
|
+
"";
|
|
755
|
+
pmaChat.state.streamText = (pmaChat.state.streamText || "") + token;
|
|
756
|
+
// Force status to "responding" if we have tokens, so the stream loop picks it up
|
|
757
|
+
if (!pmaChat.state.statusText || pmaChat.state.statusText === "starting") {
|
|
758
|
+
pmaChat.state.statusText = "responding";
|
|
759
|
+
}
|
|
760
|
+
// Ensure we're in "running" state if receiving tokens
|
|
761
|
+
if (pmaChat.state.status !== "running") {
|
|
762
|
+
pmaChat.state.status = "running";
|
|
763
|
+
}
|
|
764
|
+
pmaChat.render();
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
case "event":
|
|
768
|
+
case "app-server": {
|
|
769
|
+
if (pmaChat) {
|
|
770
|
+
// Ensure we're in "running" state if receiving events
|
|
771
|
+
if (pmaChat.state.status !== "running") {
|
|
772
|
+
pmaChat.state.status = "running";
|
|
773
|
+
}
|
|
774
|
+
// If we are receiving events but still show "starting", bump status so UI
|
|
775
|
+
// reflects progress even before token streaming starts.
|
|
776
|
+
if (!pmaChat.state.statusText || pmaChat.state.statusText === "starting") {
|
|
777
|
+
pmaChat.state.statusText = "working";
|
|
778
|
+
}
|
|
779
|
+
pmaChat.applyAppEvent(parsed);
|
|
780
|
+
pmaChat.renderEvents();
|
|
781
|
+
// Force a full render to update the inline thinking indicator
|
|
782
|
+
pmaChat.render();
|
|
783
|
+
}
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
case "token_usage": {
|
|
787
|
+
// Token usage events - context window usage
|
|
788
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
789
|
+
const percentRemaining = extractContextRemainingPercent(parsed);
|
|
790
|
+
if (percentRemaining !== null && pmaChat) {
|
|
791
|
+
pmaChat.state.contextUsagePercent = percentRemaining;
|
|
792
|
+
pmaChat.render();
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
797
|
+
case "error": {
|
|
798
|
+
const message = typeof parsed === "object" && parsed !== null
|
|
799
|
+
? parsed.detail ||
|
|
800
|
+
parsed.error ||
|
|
801
|
+
rawData
|
|
802
|
+
: rawData || "PMA chat failed";
|
|
803
|
+
pmaChat.state.status = "error";
|
|
804
|
+
pmaChat.state.error = String(message);
|
|
805
|
+
pmaChat.addAssistantMessage(`Error: ${message}`, true);
|
|
806
|
+
pmaChat.render();
|
|
807
|
+
pmaChat.renderMessages();
|
|
808
|
+
throw new Error(String(message));
|
|
809
|
+
}
|
|
810
|
+
case "interrupted": {
|
|
811
|
+
const message = typeof parsed === "object" && parsed !== null
|
|
812
|
+
? parsed.detail || rawData
|
|
813
|
+
: rawData || "PMA chat interrupted";
|
|
814
|
+
pmaChat.state.status = "interrupted";
|
|
815
|
+
pmaChat.state.error = "";
|
|
816
|
+
pmaChat.state.statusText = String(message);
|
|
817
|
+
pmaChat.addAssistantMessage("Request interrupted", true);
|
|
818
|
+
pmaChat.render();
|
|
819
|
+
pmaChat.renderMessages();
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
case "update": {
|
|
823
|
+
const data = typeof parsed === "string" ? {} : parsed;
|
|
824
|
+
// If server echoes client_turn_id, we can clear pending when we receive the final payload.
|
|
825
|
+
if (data.client_turn_id) {
|
|
826
|
+
clearPendingTurn();
|
|
827
|
+
}
|
|
828
|
+
if (data.message) {
|
|
829
|
+
pmaChat.state.streamText = data.message;
|
|
830
|
+
}
|
|
831
|
+
pmaChat.render();
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
case "done":
|
|
835
|
+
case "finish": {
|
|
836
|
+
void finalizePMAResponse(pmaChat.state.streamText || "");
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
default:
|
|
840
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
841
|
+
const messageObj = parsed;
|
|
842
|
+
if (messageObj.method || messageObj.message) {
|
|
843
|
+
pmaChat.applyAppEvent(parsed);
|
|
844
|
+
pmaChat.renderEvents();
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
async function resumePendingTurn() {
|
|
851
|
+
const pending = loadPendingTurn();
|
|
852
|
+
if (!pending || !pmaChat)
|
|
853
|
+
return;
|
|
854
|
+
// Show a running indicator immediately.
|
|
855
|
+
pmaChat.state.status = "running";
|
|
856
|
+
pmaChat.state.statusText = "Recovering previous turn…";
|
|
857
|
+
pmaChat.render();
|
|
858
|
+
pmaChat.renderMessages();
|
|
859
|
+
const poll = async () => {
|
|
860
|
+
try {
|
|
861
|
+
const payload = (await api(`/hub/pma/active?client_turn_id=${encodeURIComponent(pending.clientTurnId)}`, { method: "GET" }));
|
|
862
|
+
const cur = (payload.current || {});
|
|
863
|
+
const threadId = typeof cur.thread_id === "string" ? cur.thread_id : "";
|
|
864
|
+
const turnId = typeof cur.turn_id === "string" ? cur.turn_id : "";
|
|
865
|
+
const agent = typeof cur.agent === "string" ? cur.agent : "codex";
|
|
866
|
+
if (threadId && turnId && !currentEventsController) {
|
|
867
|
+
void startTurnEventsStream({ agent, threadId, turnId });
|
|
868
|
+
}
|
|
869
|
+
const last = (payload.last_result || {});
|
|
870
|
+
const status = String(last.status || "");
|
|
871
|
+
if (status === "ok" && typeof last.message === "string") {
|
|
872
|
+
await finalizePMAResponse(last.message);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (status === "error") {
|
|
876
|
+
const detail = String(last.detail || "PMA chat failed");
|
|
877
|
+
pmaChat.state.status = "error";
|
|
878
|
+
pmaChat.state.error = detail;
|
|
879
|
+
pmaChat.addAssistantMessage(`Error: ${detail}`, true);
|
|
880
|
+
pmaChat.render();
|
|
881
|
+
pmaChat.renderMessages();
|
|
882
|
+
clearPendingTurn();
|
|
883
|
+
stopTurnEventsStream();
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
if (status === "interrupted") {
|
|
887
|
+
pmaChat.state.status = "interrupted";
|
|
888
|
+
pmaChat.state.error = "";
|
|
889
|
+
pmaChat.addAssistantMessage("Request interrupted", true);
|
|
890
|
+
pmaChat.render();
|
|
891
|
+
pmaChat.renderMessages();
|
|
892
|
+
clearPendingTurn();
|
|
893
|
+
stopTurnEventsStream();
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
// Still running; keep polling.
|
|
897
|
+
pmaChat.state.status = "running";
|
|
898
|
+
pmaChat.state.statusText = "Recovering previous turn…";
|
|
899
|
+
pmaChat.render();
|
|
900
|
+
window.setTimeout(() => void poll(), 1000);
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
// If recovery fails, don't spam errors; just stop trying.
|
|
904
|
+
pmaChat.state.statusText = "Recovering previous turn…";
|
|
905
|
+
pmaChat.render();
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
await poll();
|
|
909
|
+
}
|
|
910
|
+
function applyPMAResult(payload) {
|
|
911
|
+
if (!payload || typeof payload !== "object")
|
|
912
|
+
return;
|
|
913
|
+
const result = payload;
|
|
914
|
+
if (result.status === "interrupted") {
|
|
915
|
+
pmaChat.state.status = "interrupted";
|
|
916
|
+
pmaChat.state.error = "";
|
|
917
|
+
pmaChat.addAssistantMessage("Request interrupted", true);
|
|
918
|
+
pmaChat.render();
|
|
919
|
+
pmaChat.renderMessages();
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (result.status === "error" || result.error) {
|
|
923
|
+
pmaChat.state.status = "error";
|
|
924
|
+
pmaChat.state.error =
|
|
925
|
+
result.detail || result.error || "Chat failed";
|
|
926
|
+
pmaChat.addAssistantMessage(`Error: ${pmaChat.state.error}`, true);
|
|
927
|
+
pmaChat.render();
|
|
928
|
+
pmaChat.renderMessages();
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
if (result.message) {
|
|
932
|
+
pmaChat.state.streamText = result.message;
|
|
933
|
+
}
|
|
934
|
+
const responseText = (pmaChat.state.streamText || pmaChat.state.statusText || "Done");
|
|
935
|
+
void finalizePMAResponse(responseText);
|
|
936
|
+
}
|
|
937
|
+
function parseMaybeJson(data) {
|
|
938
|
+
try {
|
|
939
|
+
return JSON.parse(data);
|
|
940
|
+
}
|
|
941
|
+
catch {
|
|
942
|
+
return data;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
async function interruptActiveTurn(options = {}) {
|
|
946
|
+
try {
|
|
947
|
+
if (options.stopLane) {
|
|
948
|
+
await api("/hub/pma/stop", {
|
|
949
|
+
method: "POST",
|
|
950
|
+
body: { lane_id: DEFAULT_PMA_LANE_ID },
|
|
951
|
+
});
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
await api("/hub/pma/interrupt", { method: "POST" });
|
|
955
|
+
}
|
|
956
|
+
catch {
|
|
957
|
+
// Best-effort; UI state already reflects cancellation.
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async function cancelRequest(options = {}) {
|
|
961
|
+
const { clearPending = false, interruptServer = false, stopLane = false, statusText } = options;
|
|
962
|
+
if (currentController) {
|
|
963
|
+
currentController.abort();
|
|
964
|
+
currentController = null;
|
|
965
|
+
}
|
|
966
|
+
stopTurnEventsStream();
|
|
967
|
+
if (interruptServer || stopLane) {
|
|
968
|
+
await interruptActiveTurn({ stopLane });
|
|
969
|
+
}
|
|
970
|
+
if (clearPending) {
|
|
971
|
+
clearPendingTurn();
|
|
972
|
+
}
|
|
973
|
+
if (pmaChat) {
|
|
974
|
+
pmaChat.state.controller = null;
|
|
975
|
+
pmaChat.state.status = "interrupted";
|
|
976
|
+
pmaChat.state.statusText = statusText || "Cancelled";
|
|
977
|
+
pmaChat.state.contextUsagePercent = null;
|
|
978
|
+
pmaChat.render();
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
function resetThread() {
|
|
982
|
+
clearPendingTurn();
|
|
983
|
+
stopTurnEventsStream();
|
|
984
|
+
if (pmaChat) {
|
|
985
|
+
pmaChat.state.messages = [];
|
|
986
|
+
pmaChat.state.events = [];
|
|
987
|
+
pmaChat.state.eventItemIndex = {};
|
|
988
|
+
pmaChat.state.error = "";
|
|
989
|
+
pmaChat.state.streamText = "";
|
|
990
|
+
pmaChat.state.statusText = "";
|
|
991
|
+
pmaChat.state.status = "idle";
|
|
992
|
+
pmaChat.state.contextUsagePercent = null;
|
|
993
|
+
pmaChat.render();
|
|
994
|
+
pmaChat.renderMessages();
|
|
995
|
+
}
|
|
996
|
+
flash("Thread reset", "info");
|
|
997
|
+
}
|
|
998
|
+
async function startNewThreadOnServer() {
|
|
999
|
+
const elements = getElements();
|
|
1000
|
+
const rawAgent = (elements.agentSelect?.value || getSelectedAgent() || "").trim().toLowerCase();
|
|
1001
|
+
const selectedAgent = rawAgent === "codex" || rawAgent === "opencode" ? rawAgent : undefined;
|
|
1002
|
+
await api("/hub/pma/new", {
|
|
1003
|
+
method: "POST",
|
|
1004
|
+
body: { agent: selectedAgent, lane_id: DEFAULT_PMA_LANE_ID },
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
function attachHandlers() {
|
|
1008
|
+
const elements = getElements();
|
|
1009
|
+
if (elements.sendBtn) {
|
|
1010
|
+
elements.sendBtn.addEventListener("click", () => {
|
|
1011
|
+
void sendMessage();
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
if (elements.cancelBtn) {
|
|
1015
|
+
elements.cancelBtn.addEventListener("click", () => {
|
|
1016
|
+
void cancelRequest({ clearPending: true, interruptServer: true });
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
if (elements.newThreadBtn) {
|
|
1020
|
+
elements.newThreadBtn.addEventListener("click", () => {
|
|
1021
|
+
void (async () => {
|
|
1022
|
+
await cancelRequest({
|
|
1023
|
+
clearPending: true,
|
|
1024
|
+
interruptServer: true,
|
|
1025
|
+
stopLane: true,
|
|
1026
|
+
statusText: "Cancelled (new thread)",
|
|
1027
|
+
});
|
|
1028
|
+
try {
|
|
1029
|
+
await startNewThreadOnServer();
|
|
1030
|
+
}
|
|
1031
|
+
catch (err) {
|
|
1032
|
+
flash("Failed to start new session", "error");
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
clearAgentSelectionStorage();
|
|
1036
|
+
await refreshAgentControls({ force: true, reason: "manual" });
|
|
1037
|
+
resetThread();
|
|
1038
|
+
})();
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
if (elements.input) {
|
|
1042
|
+
elements.input.addEventListener("keydown", (e) => {
|
|
1043
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
1044
|
+
e.preventDefault();
|
|
1045
|
+
void sendMessage();
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
elements.input.addEventListener("input", () => {
|
|
1049
|
+
if (elements.input) {
|
|
1050
|
+
elements.input.style.height = "auto";
|
|
1051
|
+
elements.input.style.height = `${elements.input.scrollHeight}px`;
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
initChatPasteUpload({
|
|
1055
|
+
textarea: elements.input,
|
|
1056
|
+
basePath: "/hub/pma/files",
|
|
1057
|
+
box: "inbox",
|
|
1058
|
+
insertStyle: "markdown",
|
|
1059
|
+
onUploaded: () => {
|
|
1060
|
+
void fileBoxCtrl?.refresh();
|
|
1061
|
+
},
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
if (elements.outboxRefresh) {
|
|
1065
|
+
elements.outboxRefresh.addEventListener("click", () => {
|
|
1066
|
+
void fileBoxCtrl?.refresh();
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
if (elements.scanReposBtn) {
|
|
1070
|
+
elements.scanReposBtn.addEventListener("click", async () => {
|
|
1071
|
+
try {
|
|
1072
|
+
const btn = elements.scanReposBtn;
|
|
1073
|
+
btn.disabled = true;
|
|
1074
|
+
btn.textContent = "Scanning…";
|
|
1075
|
+
await api("/hub/repos/scan", { method: "POST" });
|
|
1076
|
+
flash("Repositories scanned", "info");
|
|
1077
|
+
}
|
|
1078
|
+
catch (err) {
|
|
1079
|
+
flash("Failed to scan repos", "error");
|
|
1080
|
+
}
|
|
1081
|
+
finally {
|
|
1082
|
+
const btn = elements.scanReposBtn;
|
|
1083
|
+
btn.disabled = false;
|
|
1084
|
+
btn.textContent = "Scan repos";
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
if (elements.threadInfoThreadId) {
|
|
1089
|
+
elements.threadInfoThreadId.addEventListener("click", () => {
|
|
1090
|
+
const fullId = elements.threadInfoThreadId?.title || "";
|
|
1091
|
+
if (fullId) {
|
|
1092
|
+
void navigator.clipboard.writeText(fullId).then(() => {
|
|
1093
|
+
flash("Copied thread ID", "info");
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
elements.threadInfoThreadId.style.cursor = "pointer";
|
|
1098
|
+
}
|
|
1099
|
+
if (elements.threadInfoTurnId) {
|
|
1100
|
+
elements.threadInfoTurnId.addEventListener("click", () => {
|
|
1101
|
+
const fullId = elements.threadInfoTurnId?.title || "";
|
|
1102
|
+
if (fullId) {
|
|
1103
|
+
void navigator.clipboard.writeText(fullId).then(() => {
|
|
1104
|
+
flash("Copied turn ID", "info");
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
elements.threadInfoTurnId.style.cursor = "pointer";
|
|
1109
|
+
}
|
|
1110
|
+
document.querySelectorAll(".pma-docs-tab").forEach((tab) => {
|
|
1111
|
+
if (tab instanceof HTMLElement) {
|
|
1112
|
+
tab.addEventListener("click", () => {
|
|
1113
|
+
const docName = tab.dataset.doc;
|
|
1114
|
+
if (docName)
|
|
1115
|
+
switchPMADoc(docName);
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
if (elements.docsSaveBtn) {
|
|
1120
|
+
elements.docsSaveBtn.addEventListener("click", () => {
|
|
1121
|
+
const editor = elements.docsEditor;
|
|
1122
|
+
if (editor && currentDocName && EDITABLE_DOCS.includes(currentDocName)) {
|
|
1123
|
+
void savePMADoc(currentDocName, editor.value);
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
if (elements.docsResetBtn) {
|
|
1128
|
+
elements.docsResetBtn.addEventListener("click", resetActiveContext);
|
|
1129
|
+
}
|
|
1130
|
+
if (elements.docsSnapshotBtn) {
|
|
1131
|
+
elements.docsSnapshotBtn.addEventListener("click", () => {
|
|
1132
|
+
void snapshotActiveContext();
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
const pmaModeManual = document.getElementById("pma-mode-manual");
|
|
1136
|
+
const pmaModePma = document.getElementById("pma-mode-pma");
|
|
1137
|
+
const docsSection = document.getElementById("pma-docs-section");
|
|
1138
|
+
if (pmaModeManual && pmaModePma) {
|
|
1139
|
+
const handleModeChange = (mode) => {
|
|
1140
|
+
if (!docsSection)
|
|
1141
|
+
return;
|
|
1142
|
+
if (mode === "manual") {
|
|
1143
|
+
docsSection.classList.remove("hidden");
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
docsSection.classList.add("hidden");
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
pmaModeManual.addEventListener("click", () => {
|
|
1150
|
+
if (pmaModeManual.dataset.hubMode === "manual") {
|
|
1151
|
+
handleModeChange("manual");
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
pmaModePma.addEventListener("click", () => {
|
|
1155
|
+
if (pmaModePma.dataset.hubMode === "pma") {
|
|
1156
|
+
handleModeChange("pma");
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
if (elements.docsEditor) {
|
|
1161
|
+
elements.docsEditor.addEventListener("input", () => {
|
|
1162
|
+
elements.docsEditor.style.height = "auto";
|
|
1163
|
+
elements.docsEditor.style.height = `${elements.docsEditor.scrollHeight}px`;
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
export { initPMA };
|