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
|
@@ -1,271 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
"""Backward-compatible workspace routes."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import zipfile
|
|
5
|
-
from dataclasses import asdict
|
|
6
|
-
|
|
7
|
-
from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile
|
|
8
|
-
from fastapi.responses import FileResponse, PlainTextResponse, StreamingResponse
|
|
9
|
-
|
|
10
|
-
from ..core import drafts as draft_utils
|
|
11
|
-
from ..tickets.spec_ingest import (
|
|
12
|
-
SpecIngestTicketsError,
|
|
13
|
-
ingest_workspace_spec_to_tickets,
|
|
14
|
-
)
|
|
15
|
-
from ..web.schemas import (
|
|
16
|
-
SpecIngestTicketsResponse,
|
|
17
|
-
WorkspaceFileListResponse,
|
|
18
|
-
WorkspaceResponse,
|
|
19
|
-
WorkspaceTreeResponse,
|
|
20
|
-
WorkspaceUploadResponse,
|
|
21
|
-
WorkspaceWriteRequest,
|
|
22
|
-
)
|
|
23
|
-
from ..workspace.paths import (
|
|
24
|
-
PINNED_DOC_FILENAMES,
|
|
25
|
-
WORKSPACE_DOC_KINDS,
|
|
26
|
-
list_workspace_files,
|
|
27
|
-
list_workspace_tree,
|
|
28
|
-
normalize_workspace_rel_path,
|
|
29
|
-
read_workspace_doc,
|
|
30
|
-
read_workspace_file,
|
|
31
|
-
sanitize_workspace_filename,
|
|
32
|
-
workspace_dir,
|
|
33
|
-
workspace_doc_path,
|
|
34
|
-
write_workspace_doc,
|
|
35
|
-
write_workspace_file,
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def build_workspace_routes() -> APIRouter:
|
|
40
|
-
router = APIRouter(prefix="/api", tags=["workspace"])
|
|
41
|
-
|
|
42
|
-
@router.get("/workspace", response_model=WorkspaceResponse)
|
|
43
|
-
def get_workspace(request: Request):
|
|
44
|
-
repo_root = request.app.state.engine.repo_root
|
|
45
|
-
return {
|
|
46
|
-
"active_context": read_workspace_doc(repo_root, "active_context"),
|
|
47
|
-
"decisions": read_workspace_doc(repo_root, "decisions"),
|
|
48
|
-
"spec": read_workspace_doc(repo_root, "spec"),
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
@router.get("/workspace/file", response_class=PlainTextResponse)
|
|
52
|
-
def read_workspace(request: Request, path: str):
|
|
53
|
-
repo_root = request.app.state.engine.repo_root
|
|
54
|
-
try:
|
|
55
|
-
content = read_workspace_file(repo_root, path)
|
|
56
|
-
except ValueError as exc: # invalid path
|
|
57
|
-
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
58
|
-
return PlainTextResponse(content)
|
|
59
|
-
|
|
60
|
-
@router.put("/workspace/file", response_class=PlainTextResponse)
|
|
61
|
-
def write_workspace(request: Request, payload: WorkspaceWriteRequest, path: str):
|
|
62
|
-
repo_root = request.app.state.engine.repo_root
|
|
63
|
-
try:
|
|
64
|
-
# Normalize path the same way workspace helpers do to avoid traversal
|
|
65
|
-
safe_path, rel_posix = normalize_workspace_rel_path(repo_root, path)
|
|
66
|
-
content = write_workspace_file(repo_root, path, payload.content)
|
|
67
|
-
try:
|
|
68
|
-
rel_repo_path = safe_path.relative_to(repo_root).as_posix()
|
|
69
|
-
draft_utils.invalidate_drafts_for_path(repo_root, rel_repo_path)
|
|
70
|
-
state_key = f"workspace_{rel_posix.replace('/', '_')}"
|
|
71
|
-
draft_utils.remove_draft(repo_root, state_key)
|
|
72
|
-
except Exception:
|
|
73
|
-
pass
|
|
74
|
-
except ValueError as exc:
|
|
75
|
-
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
76
|
-
return PlainTextResponse(content)
|
|
77
|
-
|
|
78
|
-
@router.put("/workspace/{kind}", response_model=WorkspaceResponse)
|
|
79
|
-
def put_workspace(kind: str, payload: WorkspaceWriteRequest, request: Request):
|
|
80
|
-
key = (kind or "").strip().lower()
|
|
81
|
-
if key not in WORKSPACE_DOC_KINDS:
|
|
82
|
-
raise HTTPException(status_code=400, detail="invalid workspace doc kind")
|
|
83
|
-
repo_root = request.app.state.engine.repo_root
|
|
84
|
-
write_workspace_doc(repo_root, key, payload.content)
|
|
85
|
-
try:
|
|
86
|
-
rel_path = workspace_doc_path(repo_root, key).relative_to(repo_root)
|
|
87
|
-
draft_utils.invalidate_drafts_for_path(repo_root, rel_path.as_posix())
|
|
88
|
-
state_key = f"workspace_{rel_path.name}"
|
|
89
|
-
draft_utils.remove_draft(repo_root, state_key)
|
|
90
|
-
except Exception:
|
|
91
|
-
# best-effort invalidation; avoid blocking writes
|
|
92
|
-
pass
|
|
93
|
-
return {
|
|
94
|
-
"active_context": read_workspace_doc(repo_root, "active_context"),
|
|
95
|
-
"decisions": read_workspace_doc(repo_root, "decisions"),
|
|
96
|
-
"spec": read_workspace_doc(repo_root, "spec"),
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
@router.get("/workspace/files", response_model=WorkspaceFileListResponse)
|
|
100
|
-
def list_files(request: Request):
|
|
101
|
-
repo_root = request.app.state.engine.repo_root
|
|
102
|
-
files = [asdict(item) for item in list_workspace_files(repo_root)]
|
|
103
|
-
return {"files": files}
|
|
104
|
-
|
|
105
|
-
@router.get("/workspace/tree", response_model=WorkspaceTreeResponse)
|
|
106
|
-
def get_workspace_tree(request: Request):
|
|
107
|
-
repo_root = request.app.state.engine.repo_root
|
|
108
|
-
tree = [asdict(item) for item in list_workspace_tree(repo_root)]
|
|
109
|
-
return {"tree": tree}
|
|
110
|
-
|
|
111
|
-
@router.post("/workspace/upload", response_model=WorkspaceUploadResponse)
|
|
112
|
-
async def upload_workspace_files(
|
|
113
|
-
request: Request,
|
|
114
|
-
files: list[UploadFile] = File(...), # noqa: B008
|
|
115
|
-
subdir: str = Form(""),
|
|
116
|
-
):
|
|
117
|
-
if not files:
|
|
118
|
-
raise HTTPException(status_code=400, detail="no files provided")
|
|
119
|
-
|
|
120
|
-
repo_root = request.app.state.engine.repo_root
|
|
121
|
-
base = workspace_dir(repo_root)
|
|
122
|
-
target_dir = base
|
|
123
|
-
if subdir:
|
|
124
|
-
try:
|
|
125
|
-
target_dir, _ = normalize_workspace_rel_path(repo_root, subdir)
|
|
126
|
-
except ValueError as exc:
|
|
127
|
-
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
128
|
-
|
|
129
|
-
target_dir.mkdir(parents=True, exist_ok=True)
|
|
130
|
-
|
|
131
|
-
uploaded: list[dict[str, str | int]] = []
|
|
132
|
-
for upload in files:
|
|
133
|
-
filename = sanitize_workspace_filename(upload.filename or "")
|
|
134
|
-
try:
|
|
135
|
-
data = await upload.read()
|
|
136
|
-
except (
|
|
137
|
-
Exception
|
|
138
|
-
) as exc: # pragma: no cover - handled by FastAPI for most cases
|
|
139
|
-
raise HTTPException(
|
|
140
|
-
status_code=400, detail="failed to read upload"
|
|
141
|
-
) from exc
|
|
142
|
-
|
|
143
|
-
dest = target_dir / filename
|
|
144
|
-
dest.write_bytes(
|
|
145
|
-
data
|
|
146
|
-
) # codeql[py/path-injection] dest sits under normalized workspace dir
|
|
147
|
-
rel_path = dest.relative_to(base).as_posix()
|
|
148
|
-
uploaded.append({"filename": filename, "path": rel_path, "size": len(data)})
|
|
149
|
-
|
|
150
|
-
return {"status": "ok", "uploaded": uploaded}
|
|
151
|
-
|
|
152
|
-
@router.get("/workspace/download")
|
|
153
|
-
async def download_workspace_file(request: Request, path: str):
|
|
154
|
-
repo_root = request.app.state.engine.repo_root
|
|
155
|
-
try:
|
|
156
|
-
safe_path, _ = normalize_workspace_rel_path(repo_root, path)
|
|
157
|
-
except ValueError as exc:
|
|
158
|
-
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
159
|
-
|
|
160
|
-
if not safe_path.exists() or safe_path.is_dir():
|
|
161
|
-
raise HTTPException(status_code=404, detail="file not found")
|
|
162
|
-
|
|
163
|
-
return FileResponse(
|
|
164
|
-
path=safe_path, filename=safe_path.name
|
|
165
|
-
) # codeql[py/path-injection] safe_path validated by normalize_workspace_rel_path
|
|
166
|
-
|
|
167
|
-
@router.get("/workspace/download-zip")
|
|
168
|
-
async def download_workspace_zip(request: Request, path: str = ""):
|
|
169
|
-
repo_root = request.app.state.engine.repo_root
|
|
170
|
-
base = workspace_dir(repo_root)
|
|
171
|
-
base.mkdir(parents=True, exist_ok=True)
|
|
172
|
-
|
|
173
|
-
target_dir = base
|
|
174
|
-
zip_name = "workspace.zip"
|
|
175
|
-
if path:
|
|
176
|
-
try:
|
|
177
|
-
target_dir, _ = normalize_workspace_rel_path(repo_root, path)
|
|
178
|
-
except ValueError as exc:
|
|
179
|
-
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
180
|
-
if not target_dir.exists() or not target_dir.is_dir():
|
|
181
|
-
raise HTTPException(status_code=404, detail="folder not found")
|
|
182
|
-
zip_name = f"{target_dir.name}.zip"
|
|
183
|
-
|
|
184
|
-
buffer = io.BytesIO()
|
|
185
|
-
base_real = base.resolve()
|
|
186
|
-
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
187
|
-
for file_path in target_dir.rglob("*"):
|
|
188
|
-
if file_path.is_dir():
|
|
189
|
-
continue
|
|
190
|
-
if file_path.is_symlink():
|
|
191
|
-
try:
|
|
192
|
-
file_path.resolve().relative_to(base_real)
|
|
193
|
-
except Exception:
|
|
194
|
-
continue
|
|
195
|
-
arc_name = file_path.relative_to(target_dir).as_posix()
|
|
196
|
-
zf.write(
|
|
197
|
-
file_path, arc_name
|
|
198
|
-
) # codeql[py/path-injection] file_path constrained to workspace dir
|
|
199
|
-
|
|
200
|
-
buffer.seek(0)
|
|
201
|
-
return StreamingResponse(
|
|
202
|
-
buffer,
|
|
203
|
-
media_type="application/zip",
|
|
204
|
-
headers={"Content-Disposition": f'attachment; filename="{zip_name}"'},
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
@router.post("/workspace/folder")
|
|
208
|
-
async def create_workspace_folder(request: Request, path: str):
|
|
209
|
-
repo_root = request.app.state.engine.repo_root
|
|
210
|
-
try:
|
|
211
|
-
safe_path, rel_posix = normalize_workspace_rel_path(repo_root, path)
|
|
212
|
-
except ValueError as exc:
|
|
213
|
-
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
214
|
-
|
|
215
|
-
if safe_path.exists():
|
|
216
|
-
raise HTTPException(status_code=400, detail="path already exists")
|
|
217
|
-
|
|
218
|
-
safe_path.mkdir(parents=True, exist_ok=True)
|
|
219
|
-
return {"status": "created", "path": rel_posix}
|
|
220
|
-
|
|
221
|
-
@router.delete("/workspace/folder")
|
|
222
|
-
async def delete_workspace_folder(request: Request, path: str):
|
|
223
|
-
repo_root = request.app.state.engine.repo_root
|
|
224
|
-
try:
|
|
225
|
-
safe_path, rel_posix = normalize_workspace_rel_path(repo_root, path)
|
|
226
|
-
except ValueError as exc:
|
|
227
|
-
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
228
|
-
|
|
229
|
-
if not safe_path.exists():
|
|
230
|
-
raise HTTPException(status_code=404, detail="folder not found")
|
|
231
|
-
if not safe_path.is_dir():
|
|
232
|
-
raise HTTPException(status_code=400, detail="not a folder")
|
|
233
|
-
if any(safe_path.iterdir()):
|
|
234
|
-
raise HTTPException(status_code=400, detail="folder not empty")
|
|
235
|
-
|
|
236
|
-
safe_path.rmdir()
|
|
237
|
-
return {"status": "deleted", "path": rel_posix}
|
|
238
|
-
|
|
239
|
-
@router.delete("/workspace/file")
|
|
240
|
-
async def delete_workspace_file(request: Request, path: str):
|
|
241
|
-
repo_root = request.app.state.engine.repo_root
|
|
242
|
-
base = workspace_dir(repo_root)
|
|
243
|
-
try:
|
|
244
|
-
safe_path, rel_posix = normalize_workspace_rel_path(repo_root, path)
|
|
245
|
-
except ValueError as exc:
|
|
246
|
-
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
247
|
-
|
|
248
|
-
if safe_path.parent == base and safe_path.name in PINNED_DOC_FILENAMES:
|
|
249
|
-
raise HTTPException(status_code=400, detail="cannot delete pinned docs")
|
|
250
|
-
if not safe_path.exists():
|
|
251
|
-
raise HTTPException(status_code=404, detail="file not found")
|
|
252
|
-
if safe_path.is_dir():
|
|
253
|
-
raise HTTPException(status_code=400, detail="use folder delete endpoint")
|
|
254
|
-
|
|
255
|
-
safe_path.unlink() # codeql[py/path-injection] safe_path validated by normalize_workspace_rel_path
|
|
256
|
-
return {"status": "deleted", "path": rel_posix}
|
|
257
|
-
|
|
258
|
-
@router.post("/workspace/spec/ingest", response_model=SpecIngestTicketsResponse)
|
|
259
|
-
def ingest_workspace_spec(request: Request):
|
|
260
|
-
repo_root = request.app.state.engine.repo_root
|
|
261
|
-
try:
|
|
262
|
-
result = ingest_workspace_spec_to_tickets(repo_root)
|
|
263
|
-
except SpecIngestTicketsError as exc:
|
|
264
|
-
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
265
|
-
return {
|
|
266
|
-
"status": "ok",
|
|
267
|
-
"created": result.created,
|
|
268
|
-
"first_ticket_path": result.first_ticket_path,
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return router
|
|
3
|
+
from ..surfaces.web.routes.workspace import * # noqa: F401,F403
|
codex_autorunner/server.py
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from importlib import resources
|
|
2
2
|
|
|
3
|
-
from .core.
|
|
4
|
-
from .web.app import create_app, create_hub_app, create_repo_app
|
|
5
|
-
from .web.middleware import BasePathRouterMiddleware
|
|
3
|
+
from .core.runtime import LockError, RuntimeContext, clear_stale_lock, doctor
|
|
4
|
+
from .surfaces.web.app import create_app, create_hub_app, create_repo_app
|
|
5
|
+
from .surfaces.web.middleware import BasePathRouterMiddleware
|
|
6
6
|
|
|
7
7
|
__all__ = [
|
|
8
|
-
"Engine",
|
|
9
8
|
"LockError",
|
|
9
|
+
"RuntimeContext",
|
|
10
10
|
"BasePathRouterMiddleware",
|
|
11
11
|
"clear_stale_lock",
|
|
12
12
|
"create_app",
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// GENERATED FILE - do not edit directly. Source: static_src/
|
|
2
2
|
import { api, flash } from "./utils.js";
|
|
3
|
+
import { createSmartRefresh } from "./smartRefresh.js";
|
|
4
|
+
import { REPO_ID } from "./env.js";
|
|
5
|
+
const API_PREFIX = REPO_ID ? "/api" : "/hub/pma";
|
|
6
|
+
const STORAGE_PREFIX = REPO_ID ? "car.agent" : "car.pma.agent";
|
|
3
7
|
const STORAGE_KEYS = {
|
|
4
|
-
selected:
|
|
5
|
-
model: (agent) =>
|
|
6
|
-
reasoning: (agent) =>
|
|
8
|
+
selected: `${STORAGE_PREFIX}.selected`,
|
|
9
|
+
model: (agent) => `${STORAGE_PREFIX}.${agent}.model`,
|
|
10
|
+
reasoning: (agent) => `${STORAGE_PREFIX}.${agent}.reasoning`,
|
|
7
11
|
};
|
|
8
12
|
const FALLBACK_AGENTS = [
|
|
9
13
|
{ id: "codex", name: "Codex" },
|
|
@@ -15,6 +19,22 @@ let agentList = [...FALLBACK_AGENTS];
|
|
|
15
19
|
let defaultAgent = "codex";
|
|
16
20
|
const modelCatalogs = new Map();
|
|
17
21
|
const modelCatalogPromises = new Map();
|
|
22
|
+
const agentControlsRefresh = createSmartRefresh({
|
|
23
|
+
getSignature: (payload) => {
|
|
24
|
+
const agentsSig = payload.agents
|
|
25
|
+
.map((agent) => `${agent.id}:${agent.name || ""}:${agent.version || ""}:${agent.protocol_version || ""}`)
|
|
26
|
+
.join("|");
|
|
27
|
+
const catalogSig = payload.catalog
|
|
28
|
+
? `${payload.catalog.default_model || ""}:${payload.catalog.models
|
|
29
|
+
.map((model) => `${model.id}:${model.display_name || ""}:${model.supports_reasoning ? "1" : "0"}:${model.reasoning_options.join(",")}`)
|
|
30
|
+
.join("|")}`
|
|
31
|
+
: "none";
|
|
32
|
+
return `${agentsSig}::${payload.defaultAgent}::${catalogSig}`;
|
|
33
|
+
},
|
|
34
|
+
render: (payload) => {
|
|
35
|
+
renderAgentControls(payload);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
18
38
|
function safeGetStorage(key) {
|
|
19
39
|
try {
|
|
20
40
|
return localStorage.getItem(key);
|
|
@@ -75,7 +95,7 @@ async function loadAgents() {
|
|
|
75
95
|
}
|
|
76
96
|
agentsLoadPromise = (async () => {
|
|
77
97
|
try {
|
|
78
|
-
const data = await api(
|
|
98
|
+
const data = await api(`${API_PREFIX}/agents`, { method: "GET" });
|
|
79
99
|
const agents = Array.isArray(data?.agents) ? data.agents : [];
|
|
80
100
|
// Only use API response if it contains valid agents
|
|
81
101
|
if (agents.length > 0 && agents.every((a) => a && typeof a.id === "string")) {
|
|
@@ -135,7 +155,7 @@ async function loadModelCatalog(agent) {
|
|
|
135
155
|
if (modelCatalogPromises.has(agent)) {
|
|
136
156
|
return await modelCatalogPromises.get(agent) || null;
|
|
137
157
|
}
|
|
138
|
-
const promise = api(
|
|
158
|
+
const promise = api(`${API_PREFIX}/agents/${encodeURIComponent(agent)}/models`, {
|
|
139
159
|
method: "GET",
|
|
140
160
|
})
|
|
141
161
|
.then((data) => {
|
|
@@ -242,7 +262,7 @@ function resolveSelectedReasoning(agent, model) {
|
|
|
242
262
|
}
|
|
243
263
|
return model.reasoning_options[0] || "";
|
|
244
264
|
}
|
|
245
|
-
async function
|
|
265
|
+
async function loadAgentControlsPayload() {
|
|
246
266
|
try {
|
|
247
267
|
await loadAgents();
|
|
248
268
|
}
|
|
@@ -251,11 +271,6 @@ async function refreshControls() {
|
|
|
251
271
|
ensureFallbackAgents();
|
|
252
272
|
}
|
|
253
273
|
const selectedAgent = getSelectedAgent();
|
|
254
|
-
// Always update agent options first (uses in-memory agentList)
|
|
255
|
-
controls.forEach((control) => {
|
|
256
|
-
ensureAgentOptions(control.agentSelect);
|
|
257
|
-
});
|
|
258
|
-
// Then try to load model catalog
|
|
259
274
|
let catalog = modelCatalogs.get(selectedAgent);
|
|
260
275
|
if (!catalog) {
|
|
261
276
|
try {
|
|
@@ -266,6 +281,20 @@ async function refreshControls() {
|
|
|
266
281
|
catalog = null;
|
|
267
282
|
}
|
|
268
283
|
}
|
|
284
|
+
return {
|
|
285
|
+
agents: [...agentList],
|
|
286
|
+
defaultAgent,
|
|
287
|
+
selectedAgent,
|
|
288
|
+
catalog: catalog || null,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function renderAgentControls(payload) {
|
|
292
|
+
const selectedAgent = payload.selectedAgent;
|
|
293
|
+
// Always update agent options first (uses in-memory agentList)
|
|
294
|
+
controls.forEach((control) => {
|
|
295
|
+
ensureAgentOptions(control.agentSelect);
|
|
296
|
+
});
|
|
297
|
+
const catalog = payload.catalog;
|
|
269
298
|
// Update model and reasoning options
|
|
270
299
|
controls.forEach((control) => {
|
|
271
300
|
ensureModelOptions(control.modelSelect, catalog);
|
|
@@ -288,6 +317,9 @@ async function refreshControls() {
|
|
|
288
317
|
}
|
|
289
318
|
});
|
|
290
319
|
}
|
|
320
|
+
export async function refreshAgentControls(request = {}) {
|
|
321
|
+
await agentControlsRefresh.refresh(loadAgentControlsPayload, request);
|
|
322
|
+
}
|
|
291
323
|
async function handleAgentChange(nextAgent) {
|
|
292
324
|
const previous = getSelectedAgent();
|
|
293
325
|
setSelectedAgent(nextAgent);
|
|
@@ -298,17 +330,17 @@ async function handleAgentChange(nextAgent) {
|
|
|
298
330
|
setSelectedAgent(previous);
|
|
299
331
|
flash(`Failed to load ${getLabelText(nextAgent)} models; staying on ${getLabelText(previous)}.`, "error");
|
|
300
332
|
}
|
|
301
|
-
await
|
|
333
|
+
await refreshAgentControls({ force: true, reason: "manual" });
|
|
302
334
|
}
|
|
303
335
|
async function handleModelChange(nextModel) {
|
|
304
336
|
const agent = getSelectedAgent();
|
|
305
337
|
setSelectedModel(agent, nextModel);
|
|
306
|
-
await
|
|
338
|
+
await refreshAgentControls({ force: true, reason: "manual" });
|
|
307
339
|
}
|
|
308
340
|
async function handleReasoningChange(nextReasoning) {
|
|
309
341
|
const agent = getSelectedAgent();
|
|
310
342
|
setSelectedReasoning(agent, nextReasoning);
|
|
311
|
-
await
|
|
343
|
+
await refreshAgentControls({ force: true, reason: "manual" });
|
|
312
344
|
}
|
|
313
345
|
/**
|
|
314
346
|
* @param {AgentControlConfig} [config]
|
|
@@ -343,10 +375,23 @@ export function initAgentControls(config = {}) {
|
|
|
343
375
|
});
|
|
344
376
|
}
|
|
345
377
|
// Async refresh to load from API (will update if API returns different data)
|
|
346
|
-
|
|
378
|
+
refreshAgentControls({ force: true, reason: "initial" }).catch((err) => {
|
|
347
379
|
console.warn("Failed to refresh agent controls", err);
|
|
348
380
|
});
|
|
349
381
|
}
|
|
350
382
|
export async function ensureAgentCatalog() {
|
|
351
|
-
await
|
|
383
|
+
await refreshAgentControls({ force: true, reason: "manual" });
|
|
384
|
+
}
|
|
385
|
+
export function clearAgentSelectionStorage() {
|
|
386
|
+
if (REPO_ID)
|
|
387
|
+
return;
|
|
388
|
+
safeSetStorage(STORAGE_KEYS.selected, "");
|
|
389
|
+
const candidates = new Set([
|
|
390
|
+
...agentList.map((agent) => agent.id),
|
|
391
|
+
...FALLBACK_AGENTS.map((agent) => agent.id),
|
|
392
|
+
]);
|
|
393
|
+
candidates.forEach((agentId) => {
|
|
394
|
+
safeSetStorage(STORAGE_KEYS.model(agentId), "");
|
|
395
|
+
safeSetStorage(STORAGE_KEYS.reasoning(agentId), "");
|
|
396
|
+
});
|
|
352
397
|
}
|
codex_autorunner/static/app.js
CHANGED
|
@@ -1,18 +1,122 @@
|
|
|
1
1
|
// GENERATED FILE - do not edit directly. Source: static_src/
|
|
2
2
|
import { REPO_ID, HUB_BASE } from "./env.js";
|
|
3
3
|
import { initHub } from "./hub.js";
|
|
4
|
-
import { initTabs, registerTab } from "./tabs.js";
|
|
4
|
+
import { initTabs, registerTab, registerHamburgerAction } from "./tabs.js";
|
|
5
5
|
import { initTerminal } from "./terminal.js";
|
|
6
6
|
import { initTicketFlow } from "./tickets.js";
|
|
7
7
|
import { initMessages, initMessageBell } from "./messages.js";
|
|
8
8
|
import { initMobileCompact } from "./mobileCompact.js";
|
|
9
9
|
import { subscribe } from "./bus.js";
|
|
10
|
-
import { initRepoSettingsPanel } from "./settings.js";
|
|
11
|
-
import { flash } from "./utils.js";
|
|
10
|
+
import { initRepoSettingsPanel, openRepoSettings } from "./settings.js";
|
|
11
|
+
import { flash, getAuthToken, repairModalBackgroundIfStuck, resolvePath, updateUrlParams, } from "./utils.js";
|
|
12
12
|
import { initLiveUpdates } from "./liveUpdates.js";
|
|
13
13
|
import { initHealthGate } from "./health.js";
|
|
14
14
|
import { initWorkspace } from "./workspace.js";
|
|
15
15
|
import { initDashboard } from "./dashboard.js";
|
|
16
|
+
import { initArchive } from "./archive.js";
|
|
17
|
+
import { initPMA } from "./pma.js";
|
|
18
|
+
import { initNotifications } from "./notifications.js";
|
|
19
|
+
let pmaInitialized = false;
|
|
20
|
+
async function initPMAView() {
|
|
21
|
+
if (!pmaInitialized) {
|
|
22
|
+
await initPMA();
|
|
23
|
+
pmaInitialized = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function showHubView() {
|
|
27
|
+
const hubShell = document.getElementById("hub-shell");
|
|
28
|
+
const pmaShell = document.getElementById("pma-shell");
|
|
29
|
+
if (hubShell)
|
|
30
|
+
hubShell.classList.remove("hidden");
|
|
31
|
+
if (pmaShell)
|
|
32
|
+
pmaShell.classList.add("hidden");
|
|
33
|
+
updateModeToggle("manual");
|
|
34
|
+
updateUrlParams({ view: null });
|
|
35
|
+
}
|
|
36
|
+
function showPMAView() {
|
|
37
|
+
const hubShell = document.getElementById("hub-shell");
|
|
38
|
+
const pmaShell = document.getElementById("pma-shell");
|
|
39
|
+
if (hubShell)
|
|
40
|
+
hubShell.classList.add("hidden");
|
|
41
|
+
if (pmaShell)
|
|
42
|
+
pmaShell.classList.remove("hidden");
|
|
43
|
+
updateModeToggle("pma");
|
|
44
|
+
void initPMAView();
|
|
45
|
+
updateUrlParams({ view: "pma" });
|
|
46
|
+
}
|
|
47
|
+
function updateModeToggle(mode) {
|
|
48
|
+
const manualBtns = document.querySelectorAll('[data-hub-mode="manual"]');
|
|
49
|
+
const pmaBtns = document.querySelectorAll('[data-hub-mode="pma"]');
|
|
50
|
+
manualBtns.forEach((btn) => {
|
|
51
|
+
const active = mode === "manual";
|
|
52
|
+
btn.classList.toggle("active", active);
|
|
53
|
+
btn.setAttribute("aria-selected", active ? "true" : "false");
|
|
54
|
+
});
|
|
55
|
+
pmaBtns.forEach((btn) => {
|
|
56
|
+
const active = mode === "pma";
|
|
57
|
+
btn.classList.toggle("active", active);
|
|
58
|
+
btn.setAttribute("aria-selected", active ? "true" : "false");
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
async function probePMAEnabled() {
|
|
62
|
+
const headers = {};
|
|
63
|
+
const token = getAuthToken();
|
|
64
|
+
if (token) {
|
|
65
|
+
headers.Authorization = `Bearer ${token}`;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const res = await fetch(resolvePath("/hub/pma/agents"), {
|
|
69
|
+
method: "GET",
|
|
70
|
+
headers,
|
|
71
|
+
});
|
|
72
|
+
return res.ok;
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async function initHubShell() {
|
|
79
|
+
const hubShell = document.getElementById("hub-shell");
|
|
80
|
+
const repoShell = document.getElementById("repo-shell");
|
|
81
|
+
const manualBtns = Array.from(document.querySelectorAll('[data-hub-mode="manual"]'));
|
|
82
|
+
const pmaBtns = Array.from(document.querySelectorAll('[data-hub-mode="pma"]'));
|
|
83
|
+
if (hubShell)
|
|
84
|
+
hubShell.classList.remove("hidden");
|
|
85
|
+
if (repoShell)
|
|
86
|
+
repoShell.classList.add("hidden");
|
|
87
|
+
initHub();
|
|
88
|
+
initNotifications();
|
|
89
|
+
manualBtns.forEach((btn) => {
|
|
90
|
+
btn.addEventListener("click", () => {
|
|
91
|
+
showHubView();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
pmaBtns.forEach((btn) => {
|
|
95
|
+
btn.addEventListener("click", () => {
|
|
96
|
+
showPMAView();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
100
|
+
const requestedPMA = urlParams.get("view") === "pma";
|
|
101
|
+
const pmaEnabled = await probePMAEnabled();
|
|
102
|
+
if (!pmaEnabled) {
|
|
103
|
+
pmaBtns.forEach((btn) => {
|
|
104
|
+
btn.disabled = true;
|
|
105
|
+
btn.setAttribute("aria-disabled", "true");
|
|
106
|
+
btn.title = "Enable PMA in config to use Project Manager";
|
|
107
|
+
btn.classList.add("hidden");
|
|
108
|
+
btn.classList.remove("active");
|
|
109
|
+
btn.setAttribute("aria-selected", "false");
|
|
110
|
+
});
|
|
111
|
+
if (requestedPMA) {
|
|
112
|
+
showHubView();
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (requestedPMA) {
|
|
117
|
+
showPMAView();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
16
120
|
async function initRepoShell() {
|
|
17
121
|
await initHealthGate();
|
|
18
122
|
if (REPO_ID) {
|
|
@@ -36,9 +140,13 @@ async function initRepoShell() {
|
|
|
36
140
|
const defaultTab = REPO_ID ? "tickets" : "analytics";
|
|
37
141
|
registerTab("tickets", "Tickets");
|
|
38
142
|
registerTab("inbox", "Inbox");
|
|
39
|
-
registerTab("analytics", "Analytics");
|
|
40
143
|
registerTab("workspace", "Workspace");
|
|
41
144
|
registerTab("terminal", "Terminal");
|
|
145
|
+
// Menu tabs (shown in hamburger menu)
|
|
146
|
+
registerTab("analytics", "Analytics", { menuTab: true, icon: "📊" });
|
|
147
|
+
registerTab("archive", "Archive", { menuTab: true, icon: "📦" });
|
|
148
|
+
// Settings action in hamburger menu
|
|
149
|
+
registerHamburgerAction("settings", "Settings", "⚙", () => openRepoSettings());
|
|
42
150
|
const initializedTabs = new Set();
|
|
43
151
|
const lazyInit = (tabId) => {
|
|
44
152
|
if (initializedTabs.has(tabId))
|
|
@@ -52,6 +160,9 @@ async function initRepoShell() {
|
|
|
52
160
|
else if (tabId === "analytics") {
|
|
53
161
|
initDashboard();
|
|
54
162
|
}
|
|
163
|
+
else if (tabId === "archive") {
|
|
164
|
+
initArchive();
|
|
165
|
+
}
|
|
55
166
|
else if (tabId === "tickets") {
|
|
56
167
|
initTicketFlow();
|
|
57
168
|
}
|
|
@@ -80,22 +191,23 @@ async function initRepoShell() {
|
|
|
80
191
|
if (repoShell?.hasAttribute("inert")) {
|
|
81
192
|
const openModals = document.querySelectorAll(".modal-overlay:not([hidden])");
|
|
82
193
|
const count = openModals.length;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
194
|
+
if (!count && repairModalBackgroundIfStuck()) {
|
|
195
|
+
flash("Recovered from stuck modal state (UI was inert).", "info");
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
flash(count
|
|
199
|
+
? `UI inert: ${count} modal${count === 1 ? "" : "s"} open`
|
|
200
|
+
: "UI inert but no modal is visible", "error");
|
|
201
|
+
}
|
|
86
202
|
}
|
|
87
203
|
}
|
|
88
204
|
function bootstrap() {
|
|
89
|
-
const hubShell = document.getElementById("hub-shell");
|
|
90
|
-
const repoShell = document.getElementById("repo-shell");
|
|
91
205
|
if (!REPO_ID) {
|
|
92
|
-
|
|
93
|
-
hubShell.classList.remove("hidden");
|
|
94
|
-
if (repoShell)
|
|
95
|
-
repoShell.classList.add("hidden");
|
|
96
|
-
initHub();
|
|
206
|
+
void initHubShell();
|
|
97
207
|
return;
|
|
98
208
|
}
|
|
209
|
+
const hubShell = document.getElementById("hub-shell");
|
|
210
|
+
const repoShell = document.getElementById("repo-shell");
|
|
99
211
|
if (repoShell)
|
|
100
212
|
repoShell.classList.remove("hidden");
|
|
101
213
|
if (hubShell)
|