agentbridge-cli 0.1.14__tar.gz → 0.1.16__tar.gz
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.
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/PKG-INFO +2 -1
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/README.md +1 -0
- agentbridge_cli-0.1.16/agentbridge/_build_info.py +1 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/dashboard.py +12 -7
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/server.py +43 -70
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/templates/dashboard/chat.html +138 -40
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/templates/dashboard/page.html +75 -19
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/templates/dashboard/requests.html +12 -11
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/macos/Sources/AgentBridgeMenuBar/MenuBarStatusPresenter.swift +2 -17
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/macos/Tests/AgentBridgeMenuBarTests/StatusModelsTests.swift +20 -1
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/pyproject.toml +1 -1
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/tests/test_dashboard.py +134 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/tests/test_server.py +21 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/uv.lock +1 -1
- agentbridge_cli-0.1.14/agentbridge/_build_info.py +0 -1
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/.codex/config.toml +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/.codex/skills/build-release/SKILL.md +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/.codex/skills/build-release/agents/openai.yaml +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/.codex/skills/build-release/scripts/release_build.py +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/.env.example +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/.github/dependabot.yml +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/.github/workflows/ci.yml +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/.github/workflows/dependency-review.yml +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/.github/workflows/release.yml +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/.gitignore +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/AGENTS.md +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/CLAUDE.md +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/LICENSE +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/Makefile +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/TODO.txt +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/__init__.py +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/config.py +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/models.py +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/pool.py +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/static/brand/android-chrome-192.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/static/brand/android-chrome-512.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/static/brand/apple-touch-icon.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/static/brand/favicon-16.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/static/brand/favicon-32.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/static/brand/favicon-48.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/static/brand/favicon.ico +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/static/brand/favicon.svg +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/static/brand/site.webmanifest +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/templates/dashboard/base.html +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/templates/dashboard/detail.html +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/agentbridge/templates/dashboard/pool.html +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/architecture.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/agentbridge-popover-final.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/agentbridge-popover-normalized.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/agentbridge-popover-pass-1.jpeg +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/implementation-header-activity.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/reference-header-activity.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/reference-inactive-menu-icons.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/reference-inactive-telegram-badge-normalized.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/reference-inactive-telegram-badge.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/reference-low-contrast-arrows-normalized.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/reference-low-contrast-arrows.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/reference-panel.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/reference-telegram-badge-normalized.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/reference-telegram-badge.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/status-item-active-workers.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/status-item-arrow-contrast-comparison.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/status-item-badge-comparison.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/status-item-inactive-comparison.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa/status-item-no-workers.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/design-qa.md +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/docs/plans/2026-02-14-dashboard-design.md +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/docs/plans/2026-02-14-dashboard-implementation.md +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/hatch_build.py +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/apple-store/app-store-icon-1024.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/icon/icon-1024.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/logo/logo-1024.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/manifest.json +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/sources/icon-source-keyed.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/sources/icon-source.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/sources/logo-source-keyed.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/sources/logo-source.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/sources/social-background-source.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/sources/social-source.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/web-seo/android-chrome-192.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/web-seo/android-chrome-512.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/web-seo/apple-touch-icon.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/web-seo/favicon/favicon-16.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/web-seo/favicon/favicon-32.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/web-seo/favicon/favicon-48.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/web-seo/favicon/favicon.ico +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/web-seo/favicon/favicon.svg +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/web-seo/og-image-1200x630.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/image-assets/web-seo/site.webmanifest +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/logo.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/macos/Package.swift +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/macos/README.md +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/macos/Sources/AgentBridgeMenuBar/AgentBridgeMenuBarApp.swift +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/macos/Sources/AgentBridgeMenuBar/BrandMark.swift +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/macos/Sources/AgentBridgeMenuBar/HealthClient.swift +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/macos/Sources/AgentBridgeMenuBar/ServerController.swift +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/macos/Sources/AgentBridgeMenuBar/ServerLauncher.swift +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/macos/Sources/AgentBridgeMenuBar/SettingsView.swift +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/macos/Sources/AgentBridgeMenuBar/StatusModels.swift +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/macos/Sources/AgentBridgeMenuBar/StatusPopover.swift +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/scripts/build_macos_app.sh +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/scripts/dashboard_load_test.py +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/scripts/verify_macos_app.sh +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/tests/fixtures/ocr_test_document.png +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/tests/test_config.py +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/tests/test_dependency_security.py +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/tests/test_image_utils.py +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/tests/test_model_mapping.py +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/tests/test_models.py +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/tests/test_pool.py +0 -0
- {agentbridge_cli-0.1.14 → agentbridge_cli-0.1.16}/tests/test_session_logger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentbridge-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.16
|
|
4
4
|
Summary: Bridge OpenAI tools to Claude Code SDK, Codex CLI, and OpenRouter
|
|
5
5
|
Project-URL: Homepage, https://github.com/tsilva/agentbridge
|
|
6
6
|
Project-URL: Repository, https://github.com/tsilva/agentbridge
|
|
@@ -177,6 +177,7 @@ uv build # build wheel and source d
|
|
|
177
177
|
- Claude clients are created lazily, reused by model, and capped by the worker count. Claude sessions do not load filesystem settings and run with built-in tools disabled.
|
|
178
178
|
- Codex runs one ephemeral `codex exec` process per request in a temporary directory with read-only sandboxing, no approvals, and project rules ignored. Multimodal structured-output calls also ignore user config and disable execution and image-generation tools. Native image calls use the same strict profile, keep execution disabled, and enable the image-generation capability needed for the edit.
|
|
179
179
|
- Claude and Codex function calls are represented through prompted JSON; OpenRouter tool calls pass through its SDK. Session logs and extracted image or PDF attachments are saved under `~/.config/agentbridge/logs/sessions` by default.
|
|
180
|
+
- Streaming provider failures emit an OpenAI-shaped `error` object as an SSE `data` event before `[DONE]`; they are not returned as assistant message text.
|
|
180
181
|
- Set `store: false` on chat requests to suppress session-log artifacts. The native image route requires `store: false`, accepts data URLs only, validates both rasters, locates the result from the structured Codex thread ID, and removes that thread's generated-image directory after the request.
|
|
181
182
|
|
|
182
183
|
## Publishing
|
|
@@ -145,6 +145,7 @@ uv build # build wheel and source d
|
|
|
145
145
|
- Claude clients are created lazily, reused by model, and capped by the worker count. Claude sessions do not load filesystem settings and run with built-in tools disabled.
|
|
146
146
|
- Codex runs one ephemeral `codex exec` process per request in a temporary directory with read-only sandboxing, no approvals, and project rules ignored. Multimodal structured-output calls also ignore user config and disable execution and image-generation tools. Native image calls use the same strict profile, keep execution disabled, and enable the image-generation capability needed for the edit.
|
|
147
147
|
- Claude and Codex function calls are represented through prompted JSON; OpenRouter tool calls pass through its SDK. Session logs and extracted image or PDF attachments are saved under `~/.config/agentbridge/logs/sessions` by default.
|
|
148
|
+
- Streaming provider failures emit an OpenAI-shaped `error` object as an SSE `data` event before `[DONE]`; they are not returned as assistant message text.
|
|
148
149
|
- Set `store: false` on chat requests to suppress session-log artifacts. The native image route requires `store: false`, accepts data URLs only, validates both rasters, locates the result from the structured Codex thread ID, and removes that thread's generated-image directory after the request.
|
|
149
150
|
|
|
150
151
|
## Publishing
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
GIT_HASH = "4d5ec6b"
|
|
@@ -225,6 +225,8 @@ def _parse_log_file(path: Path) -> dict | None:
|
|
|
225
225
|
|
|
226
226
|
def _get_recent_logs(limit: int = 20) -> list[dict]:
|
|
227
227
|
"""Read recent session log files, newest first."""
|
|
228
|
+
if limit <= 0:
|
|
229
|
+
return []
|
|
228
230
|
log_dir = session_log_dir()
|
|
229
231
|
if not log_dir.exists():
|
|
230
232
|
return []
|
|
@@ -242,10 +244,12 @@ def _get_recent_logs(limit: int = 20) -> list[dict]:
|
|
|
242
244
|
)
|
|
243
245
|
|
|
244
246
|
results = []
|
|
245
|
-
for path in log_files
|
|
247
|
+
for path in log_files:
|
|
246
248
|
parsed = _parse_log_file(path)
|
|
247
249
|
if parsed is not None:
|
|
248
250
|
results.append(parsed)
|
|
251
|
+
if len(results) >= limit:
|
|
252
|
+
break
|
|
249
253
|
|
|
250
254
|
return results
|
|
251
255
|
|
|
@@ -366,10 +370,11 @@ def create_dashboard_router(
|
|
|
366
370
|
if log.get("duration_ms") is None:
|
|
367
371
|
log["duration_ms"] = timing.get("duration_ms", 0)
|
|
368
372
|
if log.get("input_tokens") is None:
|
|
369
|
-
usage_data =
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
+
usage_data = (
|
|
374
|
+
log.get("usage") if isinstance(log.get("usage"), dict) else {}
|
|
375
|
+
)
|
|
376
|
+
log["input_tokens"] = usage_data.get("input_tokens")
|
|
377
|
+
log["output_tokens"] = usage_data.get("output_tokens")
|
|
373
378
|
|
|
374
379
|
merged = active + completed
|
|
375
380
|
return merged[:limit]
|
|
@@ -441,8 +446,8 @@ def create_dashboard_router(
|
|
|
441
446
|
if parsed is None:
|
|
442
447
|
raise HTTPException(status_code=404, detail="Request not found")
|
|
443
448
|
|
|
444
|
-
timing = parsed.get("timing", {}
|
|
445
|
-
usage = parsed.get("usage", {}
|
|
449
|
+
timing = parsed.get("timing") if isinstance(parsed.get("timing"), dict) else {}
|
|
450
|
+
usage = parsed.get("usage") if isinstance(parsed.get("usage"), dict) else {}
|
|
446
451
|
return templates.TemplateResponse(
|
|
447
452
|
request,
|
|
448
453
|
"detail.html",
|
|
@@ -686,6 +686,23 @@ OPENROUTER_TIMEOUT = int(
|
|
|
686
686
|
)
|
|
687
687
|
|
|
688
688
|
|
|
689
|
+
def _stream_error_event(
|
|
690
|
+
message: str,
|
|
691
|
+
*,
|
|
692
|
+
error_type: str = "server_error",
|
|
693
|
+
code: str | None = None,
|
|
694
|
+
) -> str:
|
|
695
|
+
"""Return a machine-readable OpenAI-shaped error in an SSE data event."""
|
|
696
|
+
payload = ErrorResponse(
|
|
697
|
+
error=ErrorDetail(
|
|
698
|
+
message=message,
|
|
699
|
+
type=error_type,
|
|
700
|
+
code=code,
|
|
701
|
+
)
|
|
702
|
+
)
|
|
703
|
+
return f"data: {payload.model_dump_json()}\n\n"
|
|
704
|
+
|
|
705
|
+
|
|
689
706
|
class BridgeHTTPException(HTTPException):
|
|
690
707
|
"""HTTPException with request_id for error tracing."""
|
|
691
708
|
|
|
@@ -1863,18 +1880,13 @@ async def stream_openrouter_api(
|
|
|
1863
1880
|
)
|
|
1864
1881
|
dashboard_state.request_errored(request_id, f"{type(e).__name__}: {error}")
|
|
1865
1882
|
_dashboard_handled = True
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
delta=DeltaMessage(content="\n\n[Error: OpenRouter request failed.]"),
|
|
1873
|
-
finish_reason=None,
|
|
1874
|
-
)
|
|
1875
|
-
],
|
|
1883
|
+
yield _stream_error_event(
|
|
1884
|
+
"OpenRouter request timed out."
|
|
1885
|
+
if isinstance(e, asyncio.TimeoutError)
|
|
1886
|
+
else "OpenRouter request failed.",
|
|
1887
|
+
error_type="timeout_error" if isinstance(e, asyncio.TimeoutError) else "server_error",
|
|
1888
|
+
code="timeout_error" if isinstance(e, asyncio.TimeoutError) else "provider_error",
|
|
1876
1889
|
)
|
|
1877
|
-
yield f"data: {error_chunk.model_dump_json()}\n\n"
|
|
1878
1890
|
yield "data: [DONE]\n\n"
|
|
1879
1891
|
finally:
|
|
1880
1892
|
if not _dashboard_handled:
|
|
@@ -2186,23 +2198,11 @@ async def stream_claude_sdk(
|
|
|
2186
2198
|
)
|
|
2187
2199
|
dashboard_state.request_errored(request_id, f"Timeout after {CLAUDE_TIMEOUT}s")
|
|
2188
2200
|
_dashboard_handled = True
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
choices=[
|
|
2194
|
-
StreamChoice(
|
|
2195
|
-
delta=DeltaMessage(
|
|
2196
|
-
content=(
|
|
2197
|
-
"\n\n[Error: Request timed out. Increase CLAUDE_TIMEOUT "
|
|
2198
|
-
"env var for longer requests.]"
|
|
2199
|
-
)
|
|
2200
|
-
),
|
|
2201
|
-
finish_reason=None,
|
|
2202
|
-
)
|
|
2203
|
-
],
|
|
2201
|
+
yield _stream_error_event(
|
|
2202
|
+
"Request timed out. Increase CLAUDE_TIMEOUT env var for longer requests.",
|
|
2203
|
+
error_type="timeout_error",
|
|
2204
|
+
code="timeout_error",
|
|
2204
2205
|
)
|
|
2205
|
-
yield f"data: {error_chunk.model_dump_json()}\n\n"
|
|
2206
2206
|
yield "data: [DONE]\n\n"
|
|
2207
2207
|
return
|
|
2208
2208
|
except HTTPException as he:
|
|
@@ -2213,16 +2213,11 @@ async def stream_claude_sdk(
|
|
|
2213
2213
|
)
|
|
2214
2214
|
dashboard_state.request_errored(request_id, f"HTTP {he.status_code}: {he.detail}")
|
|
2215
2215
|
_dashboard_handled = True
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
choices=[StreamChoice(
|
|
2221
|
-
delta=DeltaMessage(content=f"\n\n[Error: {he.detail}]"),
|
|
2222
|
-
finish_reason=None,
|
|
2223
|
-
)],
|
|
2216
|
+
yield _stream_error_event(
|
|
2217
|
+
str(he.detail),
|
|
2218
|
+
error_type="invalid_request_error" if he.status_code < 500 else "server_error",
|
|
2219
|
+
code="invalid_request_error" if he.status_code < 500 else "server_error",
|
|
2224
2220
|
)
|
|
2225
|
-
yield f"data: {error_chunk.model_dump_json()}\n\n"
|
|
2226
2221
|
yield "data: [DONE]\n\n"
|
|
2227
2222
|
return
|
|
2228
2223
|
except Exception as e:
|
|
@@ -2237,16 +2232,11 @@ async def stream_claude_sdk(
|
|
|
2237
2232
|
)
|
|
2238
2233
|
dashboard_state.request_errored(request_id, f"{type(e).__name__}: {e}")
|
|
2239
2234
|
_dashboard_handled = True
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
choices=[StreamChoice(
|
|
2245
|
-
delta=DeltaMessage(content="\n\n[Error: An internal error occurred.]"),
|
|
2246
|
-
finish_reason=None,
|
|
2247
|
-
)],
|
|
2235
|
+
yield _stream_error_event(
|
|
2236
|
+
"An internal error occurred.",
|
|
2237
|
+
error_type="server_error",
|
|
2238
|
+
code="server_error",
|
|
2248
2239
|
)
|
|
2249
|
-
yield f"data: {error_chunk.model_dump_json()}\n\n"
|
|
2250
2240
|
yield "data: [DONE]\n\n"
|
|
2251
2241
|
return
|
|
2252
2242
|
finally:
|
|
@@ -2482,23 +2472,11 @@ async def stream_codex_cli(
|
|
|
2482
2472
|
)
|
|
2483
2473
|
dashboard_state.request_errored(request_id, f"Timeout after {CODEX_TIMEOUT}s")
|
|
2484
2474
|
_dashboard_handled = True
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
choices=[
|
|
2490
|
-
StreamChoice(
|
|
2491
|
-
delta=DeltaMessage(
|
|
2492
|
-
content=(
|
|
2493
|
-
"\n\n[Error: Request timed out. Increase CODEX_TIMEOUT "
|
|
2494
|
-
"env var for longer requests.]"
|
|
2495
|
-
)
|
|
2496
|
-
),
|
|
2497
|
-
finish_reason=None,
|
|
2498
|
-
)
|
|
2499
|
-
],
|
|
2475
|
+
yield _stream_error_event(
|
|
2476
|
+
"Request timed out. Increase CODEX_TIMEOUT env var for longer requests.",
|
|
2477
|
+
error_type="timeout_error",
|
|
2478
|
+
code="timeout_error",
|
|
2500
2479
|
)
|
|
2501
|
-
yield f"data: {error_chunk.model_dump_json()}\n\n"
|
|
2502
2480
|
yield "data: [DONE]\n\n"
|
|
2503
2481
|
return
|
|
2504
2482
|
except Exception as e:
|
|
@@ -2514,16 +2492,11 @@ async def stream_codex_cli(
|
|
|
2514
2492
|
)
|
|
2515
2493
|
dashboard_state.request_errored(request_id, f"{type(e).__name__}: {e}")
|
|
2516
2494
|
_dashboard_handled = True
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
choices=[StreamChoice(
|
|
2522
|
-
delta=DeltaMessage(content="\n\n[Error: An internal error occurred.]"),
|
|
2523
|
-
finish_reason=None,
|
|
2524
|
-
)],
|
|
2495
|
+
yield _stream_error_event(
|
|
2496
|
+
"An internal error occurred.",
|
|
2497
|
+
error_type="server_error",
|
|
2498
|
+
code="server_error",
|
|
2525
2499
|
)
|
|
2526
|
-
yield f"data: {error_chunk.model_dump_json()}\n\n"
|
|
2527
2500
|
yield "data: [DONE]\n\n"
|
|
2528
2501
|
return
|
|
2529
2502
|
finally:
|
|
@@ -642,14 +642,14 @@
|
|
|
642
642
|
</svg>
|
|
643
643
|
</button>
|
|
644
644
|
<textarea id="prompt" placeholder="Send a test message..." rows="1"></textarea>
|
|
645
|
-
<button id="send" class="primary" type="button" aria-label="Send message" title="Send message">
|
|
645
|
+
<button id="send" class="primary" type="button" disabled aria-label="Send message" title="Send message">
|
|
646
646
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
647
647
|
<path d="m5 12 7-7 7 7"></path>
|
|
648
648
|
<path d="M12 19V5"></path>
|
|
649
649
|
</svg>
|
|
650
650
|
</button>
|
|
651
651
|
</div>
|
|
652
|
-
<input id="file-input" class="hidden-input" type="file" multiple accept="image/*,application/pdf,text/plain,.txt">
|
|
652
|
+
<input id="file-input" class="hidden-input" type="file" tabindex="-1" aria-hidden="true" multiple accept="image/*,application/pdf,text/plain,.txt">
|
|
653
653
|
</section>
|
|
654
654
|
</main>
|
|
655
655
|
{% endblock %}
|
|
@@ -670,6 +670,7 @@
|
|
|
670
670
|
var attachedFiles = [];
|
|
671
671
|
var conversationMessages = [];
|
|
672
672
|
var renderedMessages = [];
|
|
673
|
+
var isSending = false;
|
|
673
674
|
var chatStateKey = "agentbridge.chat.state.v1";
|
|
674
675
|
|
|
675
676
|
function endpoint(path) {
|
|
@@ -690,6 +691,10 @@
|
|
|
690
691
|
promptEl.style.overflowY = promptEl.scrollHeight > maxHeight ? "auto" : "hidden";
|
|
691
692
|
}
|
|
692
693
|
|
|
694
|
+
function updateSendAvailability() {
|
|
695
|
+
sendEl.disabled = isSending || (!promptEl.value.trim() && !attachedFiles.length);
|
|
696
|
+
}
|
|
697
|
+
|
|
693
698
|
function clearEmptyState() {
|
|
694
699
|
var empty = messagesEl.querySelector(".empty");
|
|
695
700
|
if (empty) empty.remove();
|
|
@@ -839,9 +844,26 @@
|
|
|
839
844
|
target.innerHTML = '<span class="typing-indicator" aria-hidden="true"><span></span><span></span><span></span></span>';
|
|
840
845
|
}
|
|
841
846
|
|
|
847
|
+
function removeRenderedMessage(article) {
|
|
848
|
+
if (!article) return;
|
|
849
|
+
if (article.dataset.chatIndex === undefined) {
|
|
850
|
+
article.remove();
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
var index = Number(article.dataset.chatIndex);
|
|
854
|
+
if (Number.isInteger(index) && index >= 0 && index < renderedMessages.length) {
|
|
855
|
+
renderedMessages.splice(index, 1);
|
|
856
|
+
}
|
|
857
|
+
article.remove();
|
|
858
|
+
messagesEl.querySelectorAll(".message[data-chat-index]").forEach(function(message, nextIndex) {
|
|
859
|
+
message.dataset.chatIndex = String(nextIndex);
|
|
860
|
+
});
|
|
861
|
+
saveChatState();
|
|
862
|
+
}
|
|
863
|
+
|
|
842
864
|
function removeMessageBody(target) {
|
|
843
865
|
var article = target && target.closest ? target.closest(".message") : null;
|
|
844
|
-
|
|
866
|
+
removeRenderedMessage(article);
|
|
845
867
|
}
|
|
846
868
|
|
|
847
869
|
function messageFileNames(files) {
|
|
@@ -993,6 +1015,25 @@
|
|
|
993
1015
|
if (hasSavedModel) modelEl.value = state.model;
|
|
994
1016
|
}
|
|
995
1017
|
state.renderedMessages.forEach(function(message) {
|
|
1018
|
+
if (message.role === "error") {
|
|
1019
|
+
var errorArticle = appendError(
|
|
1020
|
+
message.status || 0,
|
|
1021
|
+
message.statusText || "Request failed",
|
|
1022
|
+
message.body || null,
|
|
1023
|
+
message.requestId || null,
|
|
1024
|
+
null,
|
|
1025
|
+
{ persist: false }
|
|
1026
|
+
);
|
|
1027
|
+
renderedMessages.push({
|
|
1028
|
+
role: "error",
|
|
1029
|
+
status: message.status || 0,
|
|
1030
|
+
statusText: message.statusText || "Request failed",
|
|
1031
|
+
body: message.body || null,
|
|
1032
|
+
requestId: message.requestId || null
|
|
1033
|
+
});
|
|
1034
|
+
errorArticle.dataset.chatIndex = String(renderedMessages.length - 1);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
996
1037
|
var body = appendMessage(
|
|
997
1038
|
message.role,
|
|
998
1039
|
message.text || "",
|
|
@@ -1032,7 +1073,7 @@
|
|
|
1032
1073
|
function summarizeError(status, statusText, body, requestId) {
|
|
1033
1074
|
var err = body && body.error ? body.error : null;
|
|
1034
1075
|
var title = "Server error";
|
|
1035
|
-
var summary = status + (statusText ? " " + statusText : "");
|
|
1076
|
+
var summary = status ? String(status) + (statusText ? " " + statusText : "") : (statusText || "Request failed");
|
|
1036
1077
|
if (err && err.message) summary = summary + ": " + err.message;
|
|
1037
1078
|
return {
|
|
1038
1079
|
title: title,
|
|
@@ -1046,7 +1087,8 @@
|
|
|
1046
1087
|
};
|
|
1047
1088
|
}
|
|
1048
1089
|
|
|
1049
|
-
function appendError(status, statusText, body, requestId) {
|
|
1090
|
+
function appendError(status, statusText, body, requestId, retryAction, options) {
|
|
1091
|
+
options = options || {};
|
|
1050
1092
|
clearEmptyState();
|
|
1051
1093
|
var info = summarizeError(status, statusText, body, requestId);
|
|
1052
1094
|
var article = document.createElement("article");
|
|
@@ -1073,12 +1115,6 @@
|
|
|
1073
1115
|
var actions = document.createElement("div");
|
|
1074
1116
|
actions.className = "error-actions";
|
|
1075
1117
|
|
|
1076
|
-
var retry = document.createElement("button");
|
|
1077
|
-
retry.className = "secondary";
|
|
1078
|
-
retry.type = "button";
|
|
1079
|
-
retry.textContent = "Retry";
|
|
1080
|
-
retry.addEventListener("click", sendMessage);
|
|
1081
|
-
|
|
1082
1118
|
var copy = document.createElement("button");
|
|
1083
1119
|
copy.className = "secondary";
|
|
1084
1120
|
copy.type = "button";
|
|
@@ -1087,16 +1123,38 @@
|
|
|
1087
1123
|
navigator.clipboard.writeText(compactJson(info.details));
|
|
1088
1124
|
});
|
|
1089
1125
|
|
|
1090
|
-
|
|
1126
|
+
if (retryAction) {
|
|
1127
|
+
var retry = document.createElement("button");
|
|
1128
|
+
retry.className = "secondary";
|
|
1129
|
+
retry.type = "button";
|
|
1130
|
+
retry.textContent = "Retry";
|
|
1131
|
+
retry.addEventListener("click", function() {
|
|
1132
|
+
removeRenderedMessage(article);
|
|
1133
|
+
retryAction();
|
|
1134
|
+
});
|
|
1135
|
+
actions.appendChild(retry);
|
|
1136
|
+
}
|
|
1091
1137
|
actions.appendChild(copy);
|
|
1092
1138
|
|
|
1093
1139
|
article.appendChild(title);
|
|
1094
1140
|
article.appendChild(summary);
|
|
1095
1141
|
article.appendChild(details);
|
|
1096
1142
|
article.appendChild(actions);
|
|
1143
|
+
if (options.persist !== false) {
|
|
1144
|
+
article.dataset.chatIndex = String(renderedMessages.length);
|
|
1145
|
+
renderedMessages.push({
|
|
1146
|
+
role: "error",
|
|
1147
|
+
status: status,
|
|
1148
|
+
statusText: statusText || null,
|
|
1149
|
+
body: body || null,
|
|
1150
|
+
requestId: requestId || null
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1097
1153
|
messagesEl.appendChild(article);
|
|
1098
1154
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1099
1155
|
setStatus("error", "Error");
|
|
1156
|
+
if (options.persist !== false) saveChatState();
|
|
1157
|
+
return article;
|
|
1100
1158
|
}
|
|
1101
1159
|
|
|
1102
1160
|
function filePill(fileName, removable, index) {
|
|
@@ -1111,8 +1169,9 @@
|
|
|
1111
1169
|
var remove = document.createElement("button");
|
|
1112
1170
|
remove.className = "remove-file";
|
|
1113
1171
|
remove.type = "button";
|
|
1114
|
-
remove.textContent = "
|
|
1172
|
+
remove.textContent = "×";
|
|
1115
1173
|
remove.title = "Remove file";
|
|
1174
|
+
remove.setAttribute("aria-label", "Remove " + fileName);
|
|
1116
1175
|
remove.addEventListener("click", function() {
|
|
1117
1176
|
attachedFiles.splice(index, 1);
|
|
1118
1177
|
renderAttachedFiles();
|
|
@@ -1127,6 +1186,7 @@
|
|
|
1127
1186
|
attachedFiles.forEach(function(file, index) {
|
|
1128
1187
|
attachedFilesEl.appendChild(filePill(file.name, true, index));
|
|
1129
1188
|
});
|
|
1189
|
+
updateSendAvailability();
|
|
1130
1190
|
}
|
|
1131
1191
|
|
|
1132
1192
|
function isTextFile(file) {
|
|
@@ -1237,12 +1297,19 @@
|
|
|
1237
1297
|
if (data === "[DONE]") return;
|
|
1238
1298
|
try {
|
|
1239
1299
|
var chunk = JSON.parse(data);
|
|
1300
|
+
if (chunk.error) {
|
|
1301
|
+
var error = new Error(chunk.error.message || "Streaming request failed");
|
|
1302
|
+
error.type = "stream_error";
|
|
1303
|
+
error.details = chunk;
|
|
1304
|
+
throw error;
|
|
1305
|
+
}
|
|
1240
1306
|
var choice = chunk.choices && chunk.choices[0];
|
|
1241
1307
|
var delta = choice && choice.delta && choice.delta.content;
|
|
1242
1308
|
if (delta) {
|
|
1243
1309
|
appendAssistantDelta(target, delta);
|
|
1244
1310
|
}
|
|
1245
1311
|
} catch (e) {
|
|
1312
|
+
if (e.type === "stream_error") throw e;
|
|
1246
1313
|
appendAssistantDelta(target, data);
|
|
1247
1314
|
}
|
|
1248
1315
|
});
|
|
@@ -1251,31 +1318,13 @@
|
|
|
1251
1318
|
return target.dataset.rawMarkdown || "";
|
|
1252
1319
|
}
|
|
1253
1320
|
|
|
1254
|
-
async function
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
var assistantTarget = null;
|
|
1258
|
-
|
|
1259
|
-
sendEl.disabled = true;
|
|
1321
|
+
async function submitPreparedMessage(payload, userMessage, userTarget) {
|
|
1322
|
+
isSending = true;
|
|
1323
|
+
updateSendAvailability();
|
|
1260
1324
|
setStatus(null, "Sending");
|
|
1325
|
+
var assistantTarget = appendMessage("assistant", "", null, { model: payload.model, loading: true });
|
|
1261
1326
|
|
|
1262
1327
|
try {
|
|
1263
|
-
var content = await buildUserContent(text);
|
|
1264
|
-
var filesForMessage = attachedFiles.slice();
|
|
1265
|
-
var userMessage = { role: "user", content: content };
|
|
1266
|
-
var payload = {
|
|
1267
|
-
model: modelEl.value,
|
|
1268
|
-
messages: conversationMessages.concat([userMessage]),
|
|
1269
|
-
stream: true
|
|
1270
|
-
};
|
|
1271
|
-
|
|
1272
|
-
var userTarget = appendMessage("user", displayTextForMessage(text, filesForMessage), filesForMessage);
|
|
1273
|
-
assistantTarget = appendMessage("assistant", "", null, { model: payload.model, loading: true });
|
|
1274
|
-
promptEl.value = "";
|
|
1275
|
-
autosizePrompt();
|
|
1276
|
-
attachedFiles = [];
|
|
1277
|
-
renderAttachedFiles();
|
|
1278
|
-
|
|
1279
1328
|
var response = await fetch(endpoint("/chat/completions"), {
|
|
1280
1329
|
method: "POST",
|
|
1281
1330
|
headers: { "Content-Type": "application/json" },
|
|
@@ -1287,7 +1336,13 @@
|
|
|
1287
1336
|
|
|
1288
1337
|
if (!response.ok) {
|
|
1289
1338
|
removeMessageBody(assistantTarget);
|
|
1290
|
-
appendError(
|
|
1339
|
+
appendError(
|
|
1340
|
+
response.status,
|
|
1341
|
+
response.statusText,
|
|
1342
|
+
await parseResponseBody(response),
|
|
1343
|
+
requestId,
|
|
1344
|
+
function() { submitPreparedMessage(payload, userMessage, userTarget); }
|
|
1345
|
+
);
|
|
1291
1346
|
return;
|
|
1292
1347
|
}
|
|
1293
1348
|
|
|
@@ -1299,15 +1354,54 @@
|
|
|
1299
1354
|
saveChatState();
|
|
1300
1355
|
} catch (error) {
|
|
1301
1356
|
removeMessageBody(assistantTarget);
|
|
1302
|
-
appendError(0, "Request failed", {
|
|
1357
|
+
appendError(0, error.type === "stream_error" ? "Stream failed" : "Request failed", error.details || {
|
|
1303
1358
|
error: {
|
|
1304
1359
|
message: error && error.message ? error.message : String(error),
|
|
1305
1360
|
type: "network_error",
|
|
1306
1361
|
code: "request_failed"
|
|
1307
1362
|
}
|
|
1308
|
-
}, null);
|
|
1363
|
+
}, null, function() { submitPreparedMessage(payload, userMessage, userTarget); });
|
|
1309
1364
|
} finally {
|
|
1310
|
-
|
|
1365
|
+
isSending = false;
|
|
1366
|
+
updateSendAvailability();
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
async function sendMessage() {
|
|
1371
|
+
var text = promptEl.value.trim();
|
|
1372
|
+
if (!text && !attachedFiles.length) return;
|
|
1373
|
+
|
|
1374
|
+
isSending = true;
|
|
1375
|
+
updateSendAvailability();
|
|
1376
|
+
setStatus(null, "Sending");
|
|
1377
|
+
|
|
1378
|
+
try {
|
|
1379
|
+
var content = await buildUserContent(text);
|
|
1380
|
+
var filesForMessage = attachedFiles.slice();
|
|
1381
|
+
var userMessage = { role: "user", content: content };
|
|
1382
|
+
var payload = {
|
|
1383
|
+
model: modelEl.value,
|
|
1384
|
+
messages: conversationMessages.concat([userMessage]),
|
|
1385
|
+
stream: true
|
|
1386
|
+
};
|
|
1387
|
+
var userTarget = appendMessage("user", displayTextForMessage(text, filesForMessage), filesForMessage);
|
|
1388
|
+
|
|
1389
|
+
promptEl.value = "";
|
|
1390
|
+
autosizePrompt();
|
|
1391
|
+
attachedFiles = [];
|
|
1392
|
+
renderAttachedFiles();
|
|
1393
|
+
|
|
1394
|
+
await submitPreparedMessage(payload, userMessage, userTarget);
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
appendError(0, "Request failed", {
|
|
1397
|
+
error: {
|
|
1398
|
+
message: error && error.message ? error.message : String(error),
|
|
1399
|
+
type: "request_preparation_error",
|
|
1400
|
+
code: "request_preparation_failed"
|
|
1401
|
+
}
|
|
1402
|
+
}, null, sendMessage);
|
|
1403
|
+
isSending = false;
|
|
1404
|
+
updateSendAvailability();
|
|
1311
1405
|
}
|
|
1312
1406
|
}
|
|
1313
1407
|
|
|
@@ -1329,7 +1423,10 @@
|
|
|
1329
1423
|
});
|
|
1330
1424
|
sendEl.addEventListener("click", sendMessage);
|
|
1331
1425
|
modelEl.addEventListener("change", saveChatState);
|
|
1332
|
-
promptEl.addEventListener("input",
|
|
1426
|
+
promptEl.addEventListener("input", function() {
|
|
1427
|
+
autosizePrompt();
|
|
1428
|
+
updateSendAvailability();
|
|
1429
|
+
});
|
|
1333
1430
|
promptEl.addEventListener("keydown", function(e) {
|
|
1334
1431
|
if (e.key !== "Enter" || e.isComposing) return;
|
|
1335
1432
|
if (e.altKey) return;
|
|
@@ -1356,6 +1453,7 @@
|
|
|
1356
1453
|
restoreChatState();
|
|
1357
1454
|
checkHealth();
|
|
1358
1455
|
autosizePrompt();
|
|
1456
|
+
updateSendAvailability();
|
|
1359
1457
|
})();
|
|
1360
1458
|
</script>
|
|
1361
1459
|
{% endblock %}
|