codex-autorunner 1.2.1__py3-none-any.whl → 1.3.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/bootstrap.py +26 -5
- codex_autorunner/core/config.py +176 -59
- codex_autorunner/core/filesystem.py +24 -0
- codex_autorunner/core/flows/controller.py +50 -12
- codex_autorunner/core/flows/runtime.py +8 -3
- codex_autorunner/core/hub.py +293 -16
- codex_autorunner/core/lifecycle_events.py +44 -5
- codex_autorunner/core/pma_delivery.py +81 -0
- codex_autorunner/core/pma_dispatches.py +224 -0
- codex_autorunner/core/pma_lane_worker.py +122 -0
- codex_autorunner/core/pma_queue.py +167 -18
- codex_autorunner/core/pma_reactive.py +91 -0
- codex_autorunner/core/pma_safety.py +58 -0
- codex_autorunner/core/pma_sink.py +104 -0
- codex_autorunner/core/pma_transcripts.py +183 -0
- codex_autorunner/core/safe_paths.py +117 -0
- codex_autorunner/housekeeping.py +77 -23
- codex_autorunner/integrations/agents/codex_backend.py +18 -12
- codex_autorunner/integrations/agents/wiring.py +2 -0
- codex_autorunner/integrations/app_server/client.py +31 -0
- codex_autorunner/integrations/app_server/supervisor.py +3 -0
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +16 -15
- codex_autorunner/integrations/telegram/handlers/commands/files.py +5 -8
- codex_autorunner/integrations/telegram/handlers/commands/github.py +10 -6
- codex_autorunner/integrations/telegram/handlers/commands/shared.py +9 -8
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +85 -2
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +29 -8
- codex_autorunner/integrations/telegram/helpers.py +30 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +54 -3
- codex_autorunner/static/docChatCore.js +2 -0
- codex_autorunner/static/hub.js +59 -0
- codex_autorunner/static/index.html +70 -54
- codex_autorunner/static/notificationBell.js +173 -0
- codex_autorunner/static/notifications.js +154 -36
- codex_autorunner/static/pma.js +96 -35
- codex_autorunner/static/styles.css +415 -4
- codex_autorunner/static/utils.js +5 -1
- codex_autorunner/surfaces/cli/cli.py +206 -129
- codex_autorunner/surfaces/cli/template_repos.py +157 -0
- codex_autorunner/surfaces/web/app.py +193 -5
- codex_autorunner/surfaces/web/routes/file_chat.py +109 -61
- codex_autorunner/surfaces/web/routes/flows.py +125 -67
- codex_autorunner/surfaces/web/routes/pma.py +638 -57
- codex_autorunner/tickets/agent_pool.py +6 -1
- codex_autorunner/tickets/outbox.py +27 -14
- codex_autorunner/tickets/replies.py +4 -10
- codex_autorunner/tickets/runner.py +1 -0
- codex_autorunner/workspace/paths.py +8 -3
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/METADATA +1 -1
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/RECORD +55 -45
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.2.1.dist-info → codex_autorunner-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -18,6 +18,7 @@ import httpx
|
|
|
18
18
|
from ....agents.opencode.client import OpenCodeProtocolError
|
|
19
19
|
from ....agents.opencode.supervisor import OpenCodeSupervisorError
|
|
20
20
|
from ....core.logging_utils import log_event
|
|
21
|
+
from ....core.pma_sink import PmaActiveSinkStore
|
|
21
22
|
from ....core.state import now_iso
|
|
22
23
|
from ....core.update import _normalize_update_target, _spawn_update_process
|
|
23
24
|
from ....core.update_paths import resolve_update_paths
|
|
@@ -81,6 +82,7 @@ from ..helpers import (
|
|
|
81
82
|
_with_conversation_id,
|
|
82
83
|
derive_codex_features_command,
|
|
83
84
|
format_codex_features,
|
|
85
|
+
format_public_error,
|
|
84
86
|
parse_codex_features_list,
|
|
85
87
|
)
|
|
86
88
|
from ..state import (
|
|
@@ -158,12 +160,12 @@ def _format_opencode_exception(exc: Exception) -> Optional[str]:
|
|
|
158
160
|
if isinstance(exc, OpenCodeSupervisorError):
|
|
159
161
|
detail = str(exc).strip()
|
|
160
162
|
if detail:
|
|
161
|
-
return f"OpenCode backend unavailable ({detail})."
|
|
163
|
+
return f"OpenCode backend unavailable ({format_public_error(detail)})."
|
|
162
164
|
return "OpenCode backend unavailable."
|
|
163
165
|
if isinstance(exc, OpenCodeProtocolError):
|
|
164
166
|
detail = str(exc).strip()
|
|
165
167
|
if detail:
|
|
166
|
-
return f"OpenCode protocol error: {detail}"
|
|
168
|
+
return f"OpenCode protocol error: {format_public_error(detail)}"
|
|
167
169
|
return "OpenCode protocol error."
|
|
168
170
|
if isinstance(exc, json.JSONDecodeError):
|
|
169
171
|
return "OpenCode returned invalid JSON."
|
|
@@ -174,15 +176,15 @@ def _format_opencode_exception(exc: Exception) -> Optional[str]:
|
|
|
174
176
|
except Exception:
|
|
175
177
|
detail = None
|
|
176
178
|
if detail:
|
|
177
|
-
return f"OpenCode error: {detail}"
|
|
179
|
+
return f"OpenCode error: {format_public_error(detail)}"
|
|
178
180
|
response_text = exc.response.text.strip()
|
|
179
181
|
if response_text:
|
|
180
|
-
return f"OpenCode error: {response_text}"
|
|
182
|
+
return f"OpenCode error: {format_public_error(response_text)}"
|
|
181
183
|
return f"OpenCode request failed (HTTP {exc.response.status_code})."
|
|
182
184
|
if isinstance(exc, httpx.RequestError):
|
|
183
185
|
detail = str(exc).strip()
|
|
184
186
|
if detail:
|
|
185
|
-
return f"OpenCode request failed: {detail}"
|
|
187
|
+
return f"OpenCode request failed: {format_public_error(detail)}"
|
|
186
188
|
return "OpenCode request failed."
|
|
187
189
|
return None
|
|
188
190
|
|
|
@@ -239,15 +241,15 @@ def _format_httpx_exception(exc: Exception) -> Optional[str]:
|
|
|
239
241
|
payload.get("detail") or payload.get("message") or payload.get("error")
|
|
240
242
|
)
|
|
241
243
|
if isinstance(detail, str) and detail:
|
|
242
|
-
return detail
|
|
244
|
+
return format_public_error(detail)
|
|
243
245
|
response_text = exc.response.text.strip()
|
|
244
246
|
if response_text:
|
|
245
|
-
return response_text
|
|
247
|
+
return format_public_error(response_text)
|
|
246
248
|
return f"Request failed (HTTP {exc.response.status_code})."
|
|
247
249
|
if isinstance(exc, httpx.RequestError):
|
|
248
250
|
detail = str(exc).strip()
|
|
249
251
|
if detail:
|
|
250
|
-
return detail
|
|
252
|
+
return format_public_error(detail)
|
|
251
253
|
return "Request failed."
|
|
252
254
|
return None
|
|
253
255
|
|
|
@@ -1202,6 +1204,25 @@ class TelegramCommandHandlers(
|
|
|
1202
1204
|
message.thread_id,
|
|
1203
1205
|
apply_pma,
|
|
1204
1206
|
)
|
|
1207
|
+
try:
|
|
1208
|
+
sink_store = PmaActiveSinkStore(Path(self._hub_root))
|
|
1209
|
+
if enabled:
|
|
1210
|
+
sink_store.set_telegram(
|
|
1211
|
+
chat_id=message.chat_id,
|
|
1212
|
+
thread_id=message.thread_id,
|
|
1213
|
+
topic_key=topic_key(message.chat_id, message.thread_id),
|
|
1214
|
+
)
|
|
1215
|
+
else:
|
|
1216
|
+
sink_store.clear()
|
|
1217
|
+
except Exception:
|
|
1218
|
+
log_event(
|
|
1219
|
+
self._logger,
|
|
1220
|
+
logging.WARNING,
|
|
1221
|
+
"telegram.pma.active_sink.update_failed",
|
|
1222
|
+
chat_id=message.chat_id,
|
|
1223
|
+
thread_id=message.thread_id,
|
|
1224
|
+
enabled=enabled,
|
|
1225
|
+
)
|
|
1205
1226
|
status = "enabled" if enabled else "disabled"
|
|
1206
1227
|
if enabled:
|
|
1207
1228
|
hint = "Use /pma off to exit. Previous repo binding saved."
|
|
@@ -9,6 +9,7 @@ from datetime import datetime, timedelta, timezone
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import Any, Callable, Iterable, Optional, Sequence
|
|
11
11
|
|
|
12
|
+
from ...core.redaction import redact_text
|
|
12
13
|
from ...core.state_roots import resolve_global_state_root
|
|
13
14
|
from ...core.utils import (
|
|
14
15
|
RepoNotFoundError,
|
|
@@ -2009,12 +2010,17 @@ def _extract_first_bold_span(text: str) -> Optional[str]:
|
|
|
2009
2010
|
|
|
2010
2011
|
|
|
2011
2012
|
def _compose_agent_response(
|
|
2012
|
-
|
|
2013
|
+
final_message: Optional[str] = None,
|
|
2013
2014
|
*,
|
|
2015
|
+
messages: Optional[list[str]] = None,
|
|
2014
2016
|
errors: Optional[list[str]] = None,
|
|
2015
2017
|
status: Optional[str] = None,
|
|
2016
2018
|
) -> str:
|
|
2017
|
-
|
|
2019
|
+
if isinstance(final_message, str) and final_message.strip():
|
|
2020
|
+
return final_message.strip()
|
|
2021
|
+
cleaned = [
|
|
2022
|
+
msg.strip() for msg in (messages or []) if isinstance(msg, str) and msg.strip()
|
|
2023
|
+
]
|
|
2018
2024
|
if not cleaned:
|
|
2019
2025
|
cleaned_errors = [
|
|
2020
2026
|
err.strip()
|
|
@@ -2183,3 +2189,25 @@ def _format_selection_prompt(base: str, page: int, total_pages: int) -> str:
|
|
|
2183
2189
|
return base
|
|
2184
2190
|
trimmed = base.rstrip(".")
|
|
2185
2191
|
return f"{trimmed} (page {page + 1}/{total_pages})."
|
|
2192
|
+
|
|
2193
|
+
|
|
2194
|
+
def format_public_error(detail: str, *, limit: int = 200) -> str:
|
|
2195
|
+
"""Format error detail for public Telegram messages with redaction and truncation.
|
|
2196
|
+
|
|
2197
|
+
This helper ensures all user-visible error text sent via Telegram is:
|
|
2198
|
+
- Short and readable
|
|
2199
|
+
- Redacted for known secret patterns
|
|
2200
|
+
- Does not include raw file contents or stack traces
|
|
2201
|
+
|
|
2202
|
+
Args:
|
|
2203
|
+
detail: Error detail string to format.
|
|
2204
|
+
limit: Maximum length of output (default 200).
|
|
2205
|
+
|
|
2206
|
+
Returns:
|
|
2207
|
+
Formatted error string with secrets redacted and length limited.
|
|
2208
|
+
"""
|
|
2209
|
+
normalized = " ".join(detail.split())
|
|
2210
|
+
redacted = redact_text(normalized)
|
|
2211
|
+
if len(redacted) > limit:
|
|
2212
|
+
return f"{redacted[: limit - 3]}..."
|
|
2213
|
+
return redacted
|
|
@@ -18,6 +18,7 @@ from ...manifest import load_manifest
|
|
|
18
18
|
from ...tickets import AgentPool
|
|
19
19
|
from .adapter import chunk_message
|
|
20
20
|
from .constants import TELEGRAM_MAX_MESSAGE_LENGTH
|
|
21
|
+
from .helpers import format_public_error
|
|
21
22
|
from .state import parse_topic_key
|
|
22
23
|
|
|
23
24
|
|
|
@@ -163,7 +164,7 @@ class TelegramTicketFlowBridge:
|
|
|
163
164
|
key, self._set_ticket_dispatch_marker(marker)
|
|
164
165
|
)
|
|
165
166
|
|
|
166
|
-
primary_key,
|
|
167
|
+
primary_key, primary_record = primary
|
|
167
168
|
try:
|
|
168
169
|
chat_id, thread_id, _scope = parse_topic_key(primary_key)
|
|
169
170
|
except Exception as exc:
|
|
@@ -182,6 +183,8 @@ class TelegramTicketFlowBridge:
|
|
|
182
183
|
seq=seq,
|
|
183
184
|
content=content,
|
|
184
185
|
archived_dir=archived_dir,
|
|
186
|
+
workspace_root=workspace_root,
|
|
187
|
+
repo_id=getattr(primary_record, "repo_id", None),
|
|
185
188
|
)
|
|
186
189
|
self._pause_targets[str(workspace_root)] = run_id
|
|
187
190
|
except Exception as exc:
|
|
@@ -296,9 +299,14 @@ class TelegramTicketFlowBridge:
|
|
|
296
299
|
def _format_ticket_flow_pause_reason(record: FlowRunRecord) -> str:
|
|
297
300
|
state = record.state or {}
|
|
298
301
|
engine = state.get("ticket_engine") or {}
|
|
299
|
-
|
|
302
|
+
reason_raw = (
|
|
300
303
|
engine.get("reason") or record.error_message or "Paused without details."
|
|
301
304
|
)
|
|
305
|
+
reason = (
|
|
306
|
+
format_public_error(str(reason_raw))
|
|
307
|
+
if reason_raw
|
|
308
|
+
else "Paused without details."
|
|
309
|
+
)
|
|
302
310
|
return f"Reason: {reason}"
|
|
303
311
|
|
|
304
312
|
def get_paused_ticket_flow(
|
|
@@ -373,6 +381,7 @@ class TelegramTicketFlowBridge:
|
|
|
373
381
|
seq=seq,
|
|
374
382
|
content=content,
|
|
375
383
|
archived_dir=archived_dir,
|
|
384
|
+
workspace_root=workspace_root,
|
|
376
385
|
)
|
|
377
386
|
self._last_default_notification[workspace_root] = marker
|
|
378
387
|
self._pause_targets[str(workspace_root)] = run_id
|
|
@@ -396,6 +405,8 @@ class TelegramTicketFlowBridge:
|
|
|
396
405
|
seq: str,
|
|
397
406
|
content: str,
|
|
398
407
|
archived_dir: Optional[Path],
|
|
408
|
+
workspace_root: Optional[Path],
|
|
409
|
+
repo_id: Optional[str] = None,
|
|
399
410
|
) -> None:
|
|
400
411
|
await self._send_dispatch_text(
|
|
401
412
|
chat_id,
|
|
@@ -403,6 +414,8 @@ class TelegramTicketFlowBridge:
|
|
|
403
414
|
run_id=run_id,
|
|
404
415
|
seq=seq,
|
|
405
416
|
content=content,
|
|
417
|
+
workspace_root=workspace_root,
|
|
418
|
+
repo_id=repo_id,
|
|
406
419
|
)
|
|
407
420
|
if self._pause_config.send_attachments and archived_dir:
|
|
408
421
|
await self._send_dispatch_attachments(
|
|
@@ -421,9 +434,15 @@ class TelegramTicketFlowBridge:
|
|
|
421
434
|
run_id: str,
|
|
422
435
|
seq: str,
|
|
423
436
|
content: str,
|
|
437
|
+
workspace_root: Optional[Path],
|
|
438
|
+
repo_id: Optional[str] = None,
|
|
424
439
|
) -> None:
|
|
425
440
|
body = content.strip() or "(no dispatch message)"
|
|
426
|
-
|
|
441
|
+
source = self._format_dispatch_source(workspace_root, repo_id)
|
|
442
|
+
header = (
|
|
443
|
+
f"Ticket flow paused (run {run_id}). Latest dispatch #{seq}:\n"
|
|
444
|
+
f"Source: {source}\n\n"
|
|
445
|
+
)
|
|
427
446
|
footer = "\n\nUse /flow resume to continue."
|
|
428
447
|
full_text = f"{header}{body}{footer}"
|
|
429
448
|
|
|
@@ -446,6 +465,38 @@ class TelegramTicketFlowBridge:
|
|
|
446
465
|
if idx == 0:
|
|
447
466
|
await asyncio.sleep(0)
|
|
448
467
|
|
|
468
|
+
def _format_dispatch_source(
|
|
469
|
+
self, workspace_root: Optional[Path], repo_id: Optional[str]
|
|
470
|
+
) -> str:
|
|
471
|
+
workspace_label = None
|
|
472
|
+
if isinstance(workspace_root, Path):
|
|
473
|
+
workspace_label = str(workspace_root)
|
|
474
|
+
repo_label = repo_id.strip() if isinstance(repo_id, str) else ""
|
|
475
|
+
if self._hub_root and self._manifest_path and self._manifest_path.exists():
|
|
476
|
+
try:
|
|
477
|
+
manifest = load_manifest(self._manifest_path, self._hub_root)
|
|
478
|
+
if workspace_root:
|
|
479
|
+
entry = manifest.get_by_path(self._hub_root, workspace_root)
|
|
480
|
+
else:
|
|
481
|
+
entry = None
|
|
482
|
+
if entry:
|
|
483
|
+
repo_label = entry.id or repo_label
|
|
484
|
+
if entry.display_name and entry.display_name != repo_label:
|
|
485
|
+
repo_label = f"{repo_label} ({entry.display_name})"
|
|
486
|
+
if entry.kind == "worktree" and entry.worktree_of:
|
|
487
|
+
repo_label = f"{repo_label} [worktree of {entry.worktree_of}]"
|
|
488
|
+
except Exception as exc:
|
|
489
|
+
self._logger.debug(
|
|
490
|
+
"telegram.ticket_flow.manifest_label_failed", exc_info=exc
|
|
491
|
+
)
|
|
492
|
+
if repo_label and workspace_label:
|
|
493
|
+
return f"{repo_label} @ {workspace_label}"
|
|
494
|
+
if repo_label:
|
|
495
|
+
return repo_label
|
|
496
|
+
if workspace_label:
|
|
497
|
+
return workspace_label
|
|
498
|
+
return "unknown workspace"
|
|
499
|
+
|
|
449
500
|
async def _send_dispatch_attachments(
|
|
450
501
|
self,
|
|
451
502
|
chat_id: int,
|
|
@@ -309,6 +309,8 @@ export function createDocChat(config) {
|
|
|
309
309
|
parts.push(`${msg.meta.steps} steps`);
|
|
310
310
|
if (msg.meta.duration)
|
|
311
311
|
parts.push(`${msg.meta.duration.toFixed(1)}s`);
|
|
312
|
+
if (msg.meta.tag)
|
|
313
|
+
parts.push(String(msg.meta.tag));
|
|
312
314
|
if (state.contextUsagePercent !== null && msg.isFinal) {
|
|
313
315
|
parts.push(`ctx left ${state.contextUsagePercent}%`);
|
|
314
316
|
}
|
codex_autorunner/static/hub.js
CHANGED
|
@@ -3,8 +3,10 @@ import { api, flash, statusPill, resolvePath, escapeHtml, confirmModal, inputMod
|
|
|
3
3
|
import { registerAutoRefresh } from "./autoRefresh.js";
|
|
4
4
|
import { HUB_BASE } from "./env.js";
|
|
5
5
|
import { preserveScroll } from "./preserve.js";
|
|
6
|
+
import { initNotificationBell } from "./notificationBell.js";
|
|
6
7
|
let hubData = { repos: [], last_scan_at: null };
|
|
7
8
|
const prefetchedUrls = new Set();
|
|
9
|
+
let hubInboxHydrated = false;
|
|
8
10
|
const HUB_CACHE_TTL_MS = 30000;
|
|
9
11
|
const HUB_CACHE_KEY = `car:hub:${HUB_BASE || "/"}`;
|
|
10
12
|
const HUB_USAGE_CACHE_KEY = `car:hub-usage:${HUB_BASE || "/"}`;
|
|
@@ -24,6 +26,8 @@ const hubUsageChartRange = document.getElementById("hub-usage-chart-range");
|
|
|
24
26
|
const hubUsageChartSegment = document.getElementById("hub-usage-chart-segment");
|
|
25
27
|
const hubVersionEl = document.getElementById("hub-version");
|
|
26
28
|
const pmaVersionEl = document.getElementById("pma-version");
|
|
29
|
+
const hubInboxList = document.getElementById("hub-inbox-list");
|
|
30
|
+
const hubInboxRefresh = document.getElementById("hub-inbox-refresh");
|
|
27
31
|
const UPDATE_STATUS_SEEN_KEY = "car_update_status_seen";
|
|
28
32
|
const HUB_JOB_POLL_INTERVAL_MS = 1200;
|
|
29
33
|
const HUB_JOB_TIMEOUT_MS = 180000;
|
|
@@ -83,7 +87,9 @@ function formatLastActivity(repo) {
|
|
|
83
87
|
}
|
|
84
88
|
function setButtonLoading(scanning) {
|
|
85
89
|
const buttons = [
|
|
90
|
+
document.getElementById("hub-scan"),
|
|
86
91
|
document.getElementById("hub-quick-scan"),
|
|
92
|
+
document.getElementById("hub-refresh"),
|
|
87
93
|
];
|
|
88
94
|
buttons.forEach((btn) => {
|
|
89
95
|
if (!btn)
|
|
@@ -915,6 +921,7 @@ async function refreshHub() {
|
|
|
915
921
|
saveSessionCache(HUB_CACHE_KEY, hubData);
|
|
916
922
|
renderSummary(data.repos || []);
|
|
917
923
|
renderReposWithScroll(data.repos || []);
|
|
924
|
+
await loadHubInbox().catch(() => { });
|
|
918
925
|
loadHubUsage({ silent: true }).catch(() => { });
|
|
919
926
|
}
|
|
920
927
|
catch (err) {
|
|
@@ -924,6 +931,46 @@ async function refreshHub() {
|
|
|
924
931
|
setButtonLoading(false);
|
|
925
932
|
}
|
|
926
933
|
}
|
|
934
|
+
async function loadHubInbox(ctx) {
|
|
935
|
+
if (!hubInboxList)
|
|
936
|
+
return;
|
|
937
|
+
if (!hubInboxHydrated || ctx?.reason === "manual") {
|
|
938
|
+
hubInboxList.textContent = "Loading…";
|
|
939
|
+
}
|
|
940
|
+
try {
|
|
941
|
+
const payload = (await api("/hub/messages", { method: "GET" }));
|
|
942
|
+
const items = payload?.items || [];
|
|
943
|
+
const html = !items.length
|
|
944
|
+
? '<div class="muted">No paused runs</div>'
|
|
945
|
+
: items
|
|
946
|
+
.map((item) => {
|
|
947
|
+
const title = item.message?.title || item.message?.mode || "Message";
|
|
948
|
+
const excerpt = item.message?.body ? item.message.body.slice(0, 160) : "";
|
|
949
|
+
const repoLabel = item.repo_display_name || item.repo_id;
|
|
950
|
+
const href = item.open_url || `/repos/${item.repo_id}/?tab=messages&run_id=${item.run_id}`;
|
|
951
|
+
return `
|
|
952
|
+
<a class="hub-inbox-item" href="${escapeHtml(resolvePath(href))}">
|
|
953
|
+
<div class="hub-inbox-item-header">
|
|
954
|
+
<span class="hub-inbox-repo">${escapeHtml(repoLabel)}</span>
|
|
955
|
+
<span class="pill pill-small pill-warn">paused</span>
|
|
956
|
+
</div>
|
|
957
|
+
<div class="hub-inbox-title">${escapeHtml(title)}</div>
|
|
958
|
+
<div class="hub-inbox-excerpt muted small">${escapeHtml(excerpt)}</div>
|
|
959
|
+
</a>
|
|
960
|
+
`;
|
|
961
|
+
})
|
|
962
|
+
.join("");
|
|
963
|
+
preserveScroll(hubInboxList, () => {
|
|
964
|
+
hubInboxList.innerHTML = html;
|
|
965
|
+
}, { restoreOnNextFrame: true });
|
|
966
|
+
hubInboxHydrated = true;
|
|
967
|
+
}
|
|
968
|
+
catch (_err) {
|
|
969
|
+
preserveScroll(hubInboxList, () => {
|
|
970
|
+
hubInboxList.innerHTML = "";
|
|
971
|
+
}, { restoreOnNextFrame: true });
|
|
972
|
+
}
|
|
973
|
+
}
|
|
927
974
|
async function triggerHubScan() {
|
|
928
975
|
setButtonLoading(true);
|
|
929
976
|
try {
|
|
@@ -1138,14 +1185,22 @@ async function handleRepoAction(repoId, action) {
|
|
|
1138
1185
|
}
|
|
1139
1186
|
function attachHubHandlers() {
|
|
1140
1187
|
initHubSettings();
|
|
1188
|
+
const scanBtn = document.getElementById("hub-scan");
|
|
1189
|
+
const refreshBtn = document.getElementById("hub-refresh");
|
|
1141
1190
|
const quickScanBtn = document.getElementById("hub-quick-scan");
|
|
1142
1191
|
const newRepoBtn = document.getElementById("hub-new-repo");
|
|
1143
1192
|
const createCancelBtn = document.getElementById("create-repo-cancel");
|
|
1144
1193
|
const createSubmitBtn = document.getElementById("create-repo-submit");
|
|
1145
1194
|
const createRepoId = document.getElementById("create-repo-id");
|
|
1195
|
+
if (scanBtn) {
|
|
1196
|
+
scanBtn.addEventListener("click", () => triggerHubScan());
|
|
1197
|
+
}
|
|
1146
1198
|
if (quickScanBtn) {
|
|
1147
1199
|
quickScanBtn.addEventListener("click", () => triggerHubScan());
|
|
1148
1200
|
}
|
|
1201
|
+
if (refreshBtn) {
|
|
1202
|
+
refreshBtn.addEventListener("click", () => refreshHub());
|
|
1203
|
+
}
|
|
1149
1204
|
if (hubUsageRefresh) {
|
|
1150
1205
|
hubUsageRefresh.addEventListener("click", () => loadHubUsage());
|
|
1151
1206
|
}
|
|
@@ -1290,6 +1345,10 @@ export function initHub() {
|
|
|
1290
1345
|
return;
|
|
1291
1346
|
attachHubHandlers();
|
|
1292
1347
|
initHubUsageChartControls();
|
|
1348
|
+
initNotificationBell();
|
|
1349
|
+
hubInboxRefresh?.addEventListener("click", () => {
|
|
1350
|
+
void loadHubInbox({ reason: "manual" });
|
|
1351
|
+
});
|
|
1293
1352
|
const cachedHub = loadSessionCache(HUB_CACHE_KEY, HUB_CACHE_TTL_MS);
|
|
1294
1353
|
if (cachedHub) {
|
|
1295
1354
|
hubData = cachedHub;
|
|
@@ -23,15 +23,15 @@
|
|
|
23
23
|
</div>
|
|
24
24
|
<div class="hub-hero-actions">
|
|
25
25
|
<button class="primary sm" id="hub-new-repo">+ New</button>
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
</
|
|
26
|
+
<button class="sm" id="hub-scan">Scan</button>
|
|
27
|
+
<button class="ghost sm" id="hub-refresh">Refresh</button>
|
|
28
|
+
<button class="ghost sm icon-btn notification-bell" id="hub-notification-bell" title="Dispatches">
|
|
29
|
+
<svg class="notification-bell-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
30
|
+
<path d="M6 9.5a6 6 0 0 1 12 0v3.5l1.6 2.2H4.4L6 13z" />
|
|
31
|
+
<path d="M9.8 18.2a2.2 2.2 0 0 0 4.4 0" />
|
|
32
|
+
</svg>
|
|
33
|
+
<span class="notification-badge hidden"></span>
|
|
34
|
+
</button>
|
|
35
35
|
<button class="ghost sm icon-btn" id="hub-settings" title="Settings">⚙</button>
|
|
36
36
|
</div>
|
|
37
37
|
<div class="hub-mode-toggle" role="tablist" aria-label="Hub mode">
|
|
@@ -57,6 +57,15 @@
|
|
|
57
57
|
<p class="muted small">missing</p>
|
|
58
58
|
</div>
|
|
59
59
|
</section>
|
|
60
|
+
<section class="hub-inbox">
|
|
61
|
+
<div class="hub-panel-header">
|
|
62
|
+
<span class="label">Inbox</span>
|
|
63
|
+
<div class="hub-panel-actions">
|
|
64
|
+
<button class="ghost sm" id="hub-inbox-refresh">Refresh</button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="hub-inbox-list" id="hub-inbox-list">Loading…</div>
|
|
68
|
+
</section>
|
|
60
69
|
<section class="hub-usage-chart">
|
|
61
70
|
<div class="hub-usage-chart-header">
|
|
62
71
|
<span class="label">Usage Trend</span>
|
|
@@ -89,7 +98,7 @@
|
|
|
89
98
|
</div>
|
|
90
99
|
</section>
|
|
91
100
|
</div>
|
|
92
|
-
<div class="hub-shell hidden" id="pma-shell">
|
|
101
|
+
<div class="hub-shell hidden" id="pma-shell" data-pma-view="chat">
|
|
93
102
|
<header class="hub-hero pma-hero">
|
|
94
103
|
<div class="hub-hero-text pma-hero-text">
|
|
95
104
|
<h1>Project Manager</h1>
|
|
@@ -97,15 +106,13 @@
|
|
|
97
106
|
<span class="hub-version" id="pma-version">v–</span>
|
|
98
107
|
</div>
|
|
99
108
|
<div class="hub-hero-actions pma-hero-actions">
|
|
100
|
-
<
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
aria-label="Pending dispatches"></div>
|
|
108
|
-
</div>
|
|
109
|
+
<button class="ghost sm icon-btn notification-bell" id="pma-notification-bell" title="Dispatches">
|
|
110
|
+
<svg class="notification-bell-icon" viewBox="0 0 24 24" aria-hidden="true">
|
|
111
|
+
<path d="M6 9.5a6 6 0 0 1 12 0v3.5l1.6 2.2H4.4L6 13z" />
|
|
112
|
+
<path d="M9.8 18.2a2.2 2.2 0 0 0 4.4 0" />
|
|
113
|
+
</svg>
|
|
114
|
+
<span class="notification-badge hidden"></span>
|
|
115
|
+
</button>
|
|
109
116
|
<button class="ghost sm icon-btn" id="pma-settings" title="Settings">⚙</button>
|
|
110
117
|
</div>
|
|
111
118
|
<div class="hub-mode-toggle" role="tablist" aria-label="Hub mode">
|
|
@@ -123,42 +130,44 @@
|
|
|
123
130
|
<select id="pma-chat-agent-select" title="Agent"></select>
|
|
124
131
|
<select id="pma-chat-model-select" title="Model"></select>
|
|
125
132
|
<select id="pma-chat-reasoning-select" title="Reasoning"></select>
|
|
133
|
+
<div class="pma-view-toggle" role="tablist" aria-label="PMA view">
|
|
134
|
+
<button class="pma-view-btn active" data-view="chat" role="tab" aria-selected="true" type="button">Chat</button>
|
|
135
|
+
<button class="pma-view-btn" data-view="memory" role="tab" aria-selected="false" type="button">Memory</button>
|
|
136
|
+
</div>
|
|
126
137
|
</div>
|
|
127
138
|
<div class="pma-chat-actions">
|
|
139
|
+
<button class="ghost sm icon-btn" id="pma-scan-repos-btn" title="Rescan hub repositories">↻</button>
|
|
128
140
|
<button class="ghost sm hidden" id="pma-chat-cancel" title="Cancel">✕</button>
|
|
129
141
|
<button class="pma-new-thread-btn" id="pma-chat-new-thread" title="Start a new chat thread">New
|
|
130
142
|
thread</button>
|
|
131
143
|
<span class="pma-status-pill" id="pma-chat-status">idle</span>
|
|
132
144
|
</div>
|
|
133
145
|
</div>
|
|
134
|
-
<div class="pma-thread-info hidden" id="pma-thread-info">
|
|
135
|
-
<div class="pma-thread-info-header">
|
|
136
|
-
<span class="pma-thread-info-label">Thread</span>
|
|
137
|
-
<span class="pill pill-small pill-idle" id="pma-thread-info-status">idle</span>
|
|
138
|
-
</div>
|
|
139
|
-
<div class="pma-thread-info-details">
|
|
140
|
-
<div class="pma-thread-info-row">
|
|
141
|
-
<span class="pma-thread-info-key muted">Agent</span>
|
|
142
|
-
<span class="pma-thread-info-value" id="pma-thread-info-agent">–</span>
|
|
143
|
-
</div>
|
|
144
|
-
<div class="pma-thread-info-row">
|
|
145
|
-
<span class="pma-thread-info-key muted">Thread ID</span>
|
|
146
|
-
<span class="pma-thread-info-value" id="pma-thread-info-thread-id" title="Click to copy">–</span>
|
|
147
|
-
</div>
|
|
148
|
-
<div class="pma-thread-info-row">
|
|
149
|
-
<span class="pma-thread-info-key muted">Turn ID</span>
|
|
150
|
-
<span class="pma-thread-info-value" id="pma-thread-info-turn-id" title="Click to copy">–</span>
|
|
151
|
-
</div>
|
|
152
|
-
</div>
|
|
153
|
-
</div>
|
|
154
|
-
<div class="pma-repo-actions" id="pma-repo-actions">
|
|
155
|
-
<button class="ghost sm" id="pma-scan-repos-btn">Scan repos</button>
|
|
156
|
-
</div>
|
|
157
146
|
</section>
|
|
158
|
-
<section class="pma-chat-section">
|
|
147
|
+
<section class="pma-chat-section" id="pma-chat-section">
|
|
159
148
|
<div class="pma-chat-main">
|
|
160
149
|
<div class="pma-chat-stream" id="pma-chat-stream">
|
|
161
150
|
<div class="pma-chat-error hidden" id="pma-chat-error"></div>
|
|
151
|
+
<div class="pma-thread-info hidden" id="pma-thread-info">
|
|
152
|
+
<div class="pma-thread-info-header">
|
|
153
|
+
<span class="pma-thread-info-label">Thread</span>
|
|
154
|
+
<span class="pill pill-small pill-idle" id="pma-thread-info-status">idle</span>
|
|
155
|
+
</div>
|
|
156
|
+
<div class="pma-thread-info-details">
|
|
157
|
+
<div class="pma-thread-info-row">
|
|
158
|
+
<span class="pma-thread-info-key muted">Agent</span>
|
|
159
|
+
<span class="pma-thread-info-value" id="pma-thread-info-agent">–</span>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="pma-thread-info-row">
|
|
162
|
+
<span class="pma-thread-info-key muted">Thread ID</span>
|
|
163
|
+
<span class="pma-thread-info-value" id="pma-thread-info-thread-id" title="Click to copy">–</span>
|
|
164
|
+
</div>
|
|
165
|
+
<div class="pma-thread-info-row">
|
|
166
|
+
<span class="pma-thread-info-key muted">Turn ID</span>
|
|
167
|
+
<span class="pma-thread-info-value" id="pma-thread-info-turn-id" title="Click to copy">–</span>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
162
171
|
<div class="pma-history-header" id="pma-chat-history-header">
|
|
163
172
|
<span class="pma-history-label">History</span>
|
|
164
173
|
</div>
|
|
@@ -176,10 +185,12 @@
|
|
|
176
185
|
<div class="pma-attachments-area" id="pma-attachments-area">
|
|
177
186
|
<div class="pma-files-row">
|
|
178
187
|
<span class="pma-files-label">In</span>
|
|
188
|
+
<button class="pma-icon-btn-small hidden pma-clear-btn" id="pma-inbox-clear" title="Clear inbox">🗑</button>
|
|
179
189
|
<div class="pma-file-list filebox-list" id="pma-inbox-files"></div>
|
|
180
190
|
</div>
|
|
181
191
|
<div class="pma-files-row">
|
|
182
192
|
<span class="pma-files-label">Out</span>
|
|
193
|
+
<button class="pma-icon-btn-small hidden pma-clear-btn" id="pma-outbox-clear" title="Clear outbox">🗑</button>
|
|
183
194
|
<div class="pma-file-list filebox-list" id="pma-outbox-files"></div>
|
|
184
195
|
<button class="pma-icon-btn-small" id="pma-outbox-refresh" title="Refresh files">↻</button>
|
|
185
196
|
</div>
|
|
@@ -1042,6 +1053,22 @@
|
|
|
1042
1053
|
</div>
|
|
1043
1054
|
</div>
|
|
1044
1055
|
</div>
|
|
1056
|
+
<!-- Dispatch Notifications Modal -->
|
|
1057
|
+
<div class="modal-overlay" hidden="" id="notification-modal">
|
|
1058
|
+
<div aria-describedby="notification-modal-body" aria-labelledby="notification-modal-title" aria-modal="true"
|
|
1059
|
+
class="modal-dialog notification-dialog" role="dialog" tabindex="-1">
|
|
1060
|
+
<div class="modal-header notification-modal-header">
|
|
1061
|
+
<span class="label" id="notification-modal-title">Dispatches</span>
|
|
1062
|
+
<div class="notification-modal-actions">
|
|
1063
|
+
<button class="ghost sm" id="notification-refresh">Refresh</button>
|
|
1064
|
+
<button class="ghost sm icon-btn" id="notification-close" title="Close">✕</button>
|
|
1065
|
+
</div>
|
|
1066
|
+
</div>
|
|
1067
|
+
<div class="modal-body" id="notification-modal-body">
|
|
1068
|
+
<div class="notification-list" id="notification-list">Loading…</div>
|
|
1069
|
+
</div>
|
|
1070
|
+
</div>
|
|
1071
|
+
</div>
|
|
1045
1072
|
<!-- Repo Settings Modal -->
|
|
1046
1073
|
<div class="modal-overlay" hidden="" id="repo-settings-modal">
|
|
1047
1074
|
<div aria-describedby="repo-settings-modal-body" aria-labelledby="repo-settings-modal-title" aria-modal="true"
|
|
@@ -1153,17 +1180,6 @@
|
|
|
1153
1180
|
</div>
|
|
1154
1181
|
</div>
|
|
1155
1182
|
</div>
|
|
1156
|
-
<!-- Notifications Modal -->
|
|
1157
|
-
<div class="modal-overlay hidden" id="notifications-modal">
|
|
1158
|
-
<div aria-labelledby="notifications-modal-title" aria-modal="true" class="modal-dialog notifications-modal-dialog"
|
|
1159
|
-
role="dialog" tabindex="-1">
|
|
1160
|
-
<div class="notifications-modal-header">
|
|
1161
|
-
<span class="label" id="notifications-modal-title">Pending dispatch</span>
|
|
1162
|
-
<button class="ghost sm icon-btn" id="notifications-modal-close" title="Close">×</button>
|
|
1163
|
-
</div>
|
|
1164
|
-
<div class="notifications-modal-body-wrapper" id="notifications-modal-body"></div>
|
|
1165
|
-
</div>
|
|
1166
|
-
</div>
|
|
1167
1183
|
<!-- Reason Details Modal -->
|
|
1168
1184
|
<div class="modal-overlay" hidden="" id="reason-modal">
|
|
1169
1185
|
<div aria-describedby="reason-modal-content" aria-labelledby="reason-modal-title" aria-modal="true"
|