@oneciel-ai/ciel-runtime 0.1.1 → 0.2.0
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.
- package/README.md +132 -0
- package/ciel-runtime-menu.py +56 -6
- package/ciel_runtime.py +9379 -35909
- package/ciel_runtime_support/advisor_client.py +193 -0
- package/ciel_runtime_support/advisor_policy.py +320 -0
- package/ciel_runtime_support/advisor_refinement.py +160 -0
- package/ciel_runtime_support/advisor_request_builder.py +261 -0
- package/ciel_runtime_support/agy_installer.py +169 -0
- package/ciel_runtime_support/agy_mcp_restore.py +182 -0
- package/ciel_runtime_support/anthropic_model_policy.py +186 -0
- package/ciel_runtime_support/anthropic_response_writer.py +255 -0
- package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
- package/ciel_runtime_support/api_key_cooldown.py +159 -0
- package/ciel_runtime_support/architecture.py +488 -1
- package/ciel_runtime_support/architecture_budget.py +42 -0
- package/ciel_runtime_support/channel_backlog.py +90 -0
- package/ciel_runtime_support/channel_cli.py +119 -0
- package/ciel_runtime_support/channel_compact_injection.py +82 -0
- package/ciel_runtime_support/channel_compact_poll.py +67 -0
- package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
- package/ciel_runtime_support/channel_config_service.py +281 -0
- package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
- package/ciel_runtime_support/channel_connection_registry.py +128 -0
- package/ciel_runtime_support/channel_connection_worker.py +284 -0
- package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
- package/ciel_runtime_support/channel_cursor_repository.py +89 -0
- package/ciel_runtime_support/channel_cursor_service.py +178 -0
- package/ciel_runtime_support/channel_event_identity.py +212 -0
- package/ciel_runtime_support/channel_event_projection.py +315 -0
- package/ciel_runtime_support/channel_inflight.py +127 -0
- package/ciel_runtime_support/channel_injection.py +115 -0
- package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
- package/ciel_runtime_support/channel_launch_policy.py +180 -0
- package/ciel_runtime_support/channel_llm_context.py +156 -0
- package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
- package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
- package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
- package/ciel_runtime_support/channel_mcp_tools.py +240 -0
- package/ciel_runtime_support/channel_mcp_transport.py +394 -0
- package/ciel_runtime_support/channel_message_dedupe.py +65 -0
- package/ciel_runtime_support/channel_message_policy.py +256 -0
- package/ciel_runtime_support/channel_message_prompt.py +305 -0
- package/ciel_runtime_support/channel_message_repository.py +234 -0
- package/ciel_runtime_support/channel_notification_projection.py +217 -0
- package/ciel_runtime_support/channel_panel.py +162 -0
- package/ciel_runtime_support/channel_pending_injection.py +209 -0
- package/ciel_runtime_support/channel_pending_poll.py +109 -0
- package/ciel_runtime_support/channel_probe_cache.py +433 -0
- package/ciel_runtime_support/channel_probe_report.py +101 -0
- package/ciel_runtime_support/channel_runtime_environment.py +181 -0
- package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
- package/ciel_runtime_support/channel_session_repository.py +90 -0
- package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
- package/ciel_runtime_support/channel_terminal_input.py +277 -0
- package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
- package/ciel_runtime_support/channel_tool_context.py +166 -0
- package/ciel_runtime_support/channel_transcript.py +414 -0
- package/ciel_runtime_support/channel_transcript_repository.py +96 -0
- package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
- package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
- package/ciel_runtime_support/chat_files.py +138 -0
- package/ciel_runtime_support/chat_http_controller.py +235 -0
- package/ciel_runtime_support/claude_environment.py +375 -0
- package/ciel_runtime_support/claude_router.py +247 -193
- package/ciel_runtime_support/cli_dispatch.py +792 -0
- package/ciel_runtime_support/cli_parser.py +165 -0
- package/ciel_runtime_support/cli_usage.py +100 -0
- package/ciel_runtime_support/codex_app_server.py +20 -5
- package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
- package/ciel_runtime_support/codex_cli.py +42 -6
- package/ciel_runtime_support/codex_config.py +323 -0
- package/ciel_runtime_support/codex_launch_configuration.py +240 -0
- package/ciel_runtime_support/codex_launch_policy.py +66 -0
- package/ciel_runtime_support/codex_mcp_integration.py +195 -0
- package/ciel_runtime_support/codex_mcp_restore.py +304 -0
- package/ciel_runtime_support/codex_model_catalog.py +133 -0
- package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
- package/ciel_runtime_support/codex_router.py +147 -1
- package/ciel_runtime_support/codex_session_repository.py +115 -0
- package/ciel_runtime_support/codex_session_selection.py +114 -0
- package/ciel_runtime_support/command_asset_installer.py +103 -0
- package/ciel_runtime_support/compatibility_probe.py +295 -0
- package/ciel_runtime_support/compatibility_protocol.py +251 -0
- package/ciel_runtime_support/compatibility_runtime.py +166 -0
- package/ciel_runtime_support/compatibility_test.py +370 -0
- package/ciel_runtime_support/config_migrations.py +307 -0
- package/ciel_runtime_support/config_repository.py +175 -0
- package/ciel_runtime_support/config_value_codec.py +64 -0
- package/ciel_runtime_support/configuration_cli.py +374 -0
- package/ciel_runtime_support/context_compaction.py +280 -0
- package/ciel_runtime_support/context_setup.py +208 -0
- package/ciel_runtime_support/context_summary_policy.py +392 -0
- package/ciel_runtime_support/credential_cli.py +104 -0
- package/ciel_runtime_support/credential_management.py +261 -0
- package/ciel_runtime_support/credentials.py +269 -0
- package/ciel_runtime_support/executable_discovery.py +141 -0
- package/ciel_runtime_support/github_copilot_oauth.py +335 -0
- package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
- package/ciel_runtime_support/header_forwarding.py +73 -0
- package/ciel_runtime_support/headless_config.py +221 -0
- package/ciel_runtime_support/http_response.py +129 -0
- package/ciel_runtime_support/install_diagnostics.py +149 -0
- package/ciel_runtime_support/kimi_identity.py +123 -0
- package/ciel_runtime_support/launch_diagnostics.py +204 -0
- package/ciel_runtime_support/launch_state.py +127 -0
- package/ciel_runtime_support/live_api_key_controller.py +58 -0
- package/ciel_runtime_support/llm_config_http.py +148 -0
- package/ciel_runtime_support/llm_option_config.py +259 -0
- package/ciel_runtime_support/llm_presentation_data.py +447 -0
- package/ciel_runtime_support/llm_presets.py +773 -0
- package/ciel_runtime_support/lm_studio_runtime.py +401 -0
- package/ciel_runtime_support/managed_mcp_config.py +144 -0
- package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
- package/ciel_runtime_support/managed_service_cleanup.py +89 -0
- package/ciel_runtime_support/mcp_config_reader.py +230 -0
- package/ciel_runtime_support/mcp_http_proxy.py +607 -0
- package/ciel_runtime_support/mcp_inventory.py +59 -0
- package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
- package/ciel_runtime_support/mcp_probe_codec.py +136 -0
- package/ciel_runtime_support/mcp_probe_transport.py +328 -0
- package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
- package/ciel_runtime_support/mcp_proxy_config.py +107 -0
- package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
- package/ciel_runtime_support/mcp_proxy_process.py +560 -0
- package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
- package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
- package/ciel_runtime_support/mcp_transport.py +146 -0
- package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
- package/ciel_runtime_support/model_catalog_projection.py +61 -0
- package/ciel_runtime_support/model_context_hints.py +109 -0
- package/ciel_runtime_support/model_panel.py +147 -0
- package/ciel_runtime_support/model_registry_repository.py +231 -0
- package/ciel_runtime_support/npm_runtime.py +191 -0
- package/ciel_runtime_support/ollama_catalog.py +462 -0
- package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
- package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
- package/ciel_runtime_support/ollama_context_sync.py +87 -0
- package/ciel_runtime_support/ollama_forwarding.py +449 -0
- package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
- package/ciel_runtime_support/openai_chat_router.py +64 -0
- package/ciel_runtime_support/openai_forwarding.py +194 -0
- package/ciel_runtime_support/openai_responses_router.py +291 -0
- package/ciel_runtime_support/openai_responses_stream.py +135 -0
- package/ciel_runtime_support/output_budget.py +89 -0
- package/ciel_runtime_support/package_lifecycle.py +217 -0
- package/ciel_runtime_support/plan_artifact_controller.py +104 -0
- package/ciel_runtime_support/prelaunch.py +959 -0
- package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
- package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
- package/ciel_runtime_support/prelaunch_terminal.py +764 -0
- package/ciel_runtime_support/process_control.py +708 -0
- package/ciel_runtime_support/prompt_compaction.py +322 -0
- package/ciel_runtime_support/prompt_injection.py +176 -0
- package/ciel_runtime_support/protocols/__init__.py +24 -0
- package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
- package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
- package/ciel_runtime_support/protocols/chat_projection.py +315 -0
- package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
- package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
- package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
- package/ciel_runtime_support/protocols/ollama_response.py +231 -0
- package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
- package/ciel_runtime_support/protocols/openai_responses.py +271 -0
- package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
- package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
- package/ciel_runtime_support/provider_adapters.py +160 -0
- package/ciel_runtime_support/provider_catalog_sources.py +316 -0
- package/ciel_runtime_support/provider_choice.py +201 -0
- package/ciel_runtime_support/provider_compatibility.py +165 -0
- package/ciel_runtime_support/provider_config_mutations.py +361 -0
- package/ciel_runtime_support/provider_configuration_service.py +162 -0
- package/ciel_runtime_support/provider_context.py +308 -0
- package/ciel_runtime_support/provider_contract_projection.py +73 -0
- package/ciel_runtime_support/provider_descriptor.py +82 -0
- package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
- package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
- package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
- package/ciel_runtime_support/provider_limits.py +457 -0
- package/ciel_runtime_support/provider_model_identity.py +139 -0
- package/ciel_runtime_support/provider_model_selection.py +431 -0
- package/ciel_runtime_support/provider_model_specs.py +142 -0
- package/ciel_runtime_support/provider_models.py +263 -0
- package/ciel_runtime_support/provider_network.py +176 -0
- package/ciel_runtime_support/provider_option_cli.py +238 -0
- package/ciel_runtime_support/provider_option_panel.py +275 -0
- package/ciel_runtime_support/provider_option_status.py +192 -0
- package/ciel_runtime_support/provider_policy.py +101 -0
- package/ciel_runtime_support/provider_query_policy.py +67 -0
- package/ciel_runtime_support/provider_readiness.py +112 -0
- package/ciel_runtime_support/provider_request_access.py +131 -0
- package/ciel_runtime_support/provider_request_builder.py +250 -0
- package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
- package/ciel_runtime_support/provider_runtime_info.py +113 -0
- package/ciel_runtime_support/provider_runtime_modes.py +150 -0
- package/ciel_runtime_support/provider_sampling_policy.py +46 -0
- package/ciel_runtime_support/provider_status.py +145 -0
- package/ciel_runtime_support/provider_timeout_policy.py +184 -0
- package/ciel_runtime_support/provider_tool_policy.py +145 -0
- package/ciel_runtime_support/providers/__init__.py +65 -0
- package/ciel_runtime_support/providers/anthropic.py +160 -0
- package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
- package/ciel_runtime_support/providers/base.py +232 -0
- package/ciel_runtime_support/providers/catalog.py +326 -0
- package/ciel_runtime_support/providers/cloud.py +194 -0
- package/ciel_runtime_support/providers/constants.py +54 -0
- package/ciel_runtime_support/providers/deepseek.py +115 -0
- package/ciel_runtime_support/providers/fireworks.py +158 -0
- package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
- package/ciel_runtime_support/providers/kimi.py +297 -0
- package/ciel_runtime_support/providers/lm_studio.py +76 -0
- package/ciel_runtime_support/providers/meta.py +257 -0
- package/ciel_runtime_support/providers/native.py +194 -0
- package/ciel_runtime_support/providers/nim.py +69 -0
- package/ciel_runtime_support/providers/nvidia.py +158 -0
- package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
- package/ciel_runtime_support/providers/ollama.py +181 -0
- package/ciel_runtime_support/providers/ollama_context.py +195 -0
- package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
- package/ciel_runtime_support/providers/opencode.py +220 -0
- package/ciel_runtime_support/providers/opencode_go.py +37 -0
- package/ciel_runtime_support/providers/openrouter.py +57 -0
- package/ciel_runtime_support/providers/vllm.py +65 -0
- package/ciel_runtime_support/providers/zai.py +119 -0
- package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
- package/ciel_runtime_support/rate_limit_policy.py +117 -0
- package/ciel_runtime_support/rate_limit_repository.py +154 -0
- package/ciel_runtime_support/registry.py +46 -0
- package/ciel_runtime_support/request_shortcuts.py +253 -0
- package/ciel_runtime_support/request_trace.py +323 -0
- package/ciel_runtime_support/response_collection.py +209 -0
- package/ciel_runtime_support/router_access.py +238 -0
- package/ciel_runtime_support/router_client_lifecycle.py +366 -0
- package/ciel_runtime_support/router_health_policy.py +101 -0
- package/ciel_runtime_support/router_http.py +513 -0
- package/ciel_runtime_support/router_process_lifecycle.py +401 -0
- package/ciel_runtime_support/router_rate_limit_service.py +285 -0
- package/ciel_runtime_support/router_server_runtime.py +103 -0
- package/ciel_runtime_support/router_shortcuts.py +201 -0
- package/ciel_runtime_support/routing_fallback.py +73 -0
- package/ciel_runtime_support/runtime_activity_repository.py +143 -0
- package/ciel_runtime_support/runtime_adapters.py +104 -0
- package/ciel_runtime_support/runtime_command_factory.py +73 -0
- package/ciel_runtime_support/runtime_compatibility.py +50 -0
- package/ciel_runtime_support/runtime_constants.py +178 -0
- package/ciel_runtime_support/runtime_launch.py +1602 -0
- package/ciel_runtime_support/runtime_llm_options.py +312 -0
- package/ciel_runtime_support/runtime_logging.py +161 -0
- package/ciel_runtime_support/runtime_paths.py +157 -0
- package/ciel_runtime_support/runtime_restart.py +84 -0
- package/ciel_runtime_support/runtime_upgrade.py +149 -0
- package/ciel_runtime_support/secure_json_repository.py +55 -0
- package/ciel_runtime_support/session_import.py +356 -0
- package/ciel_runtime_support/settings_repository.py +8 -0
- package/ciel_runtime_support/slash_command_assets.py +211 -0
- package/ciel_runtime_support/sse_stream.py +57 -0
- package/ciel_runtime_support/sse_trace.py +225 -0
- package/ciel_runtime_support/statusline_script.py +593 -0
- package/ciel_runtime_support/statusline_settings.py +53 -0
- package/ciel_runtime_support/stream_chunk_policy.py +18 -0
- package/ciel_runtime_support/streaming_anthropic.py +1955 -0
- package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
- package/ciel_runtime_support/terminal_platform_io.py +127 -0
- package/ciel_runtime_support/timeout_profile.py +196 -0
- package/ciel_runtime_support/tool_dialects.py +85 -0
- package/ciel_runtime_support/tool_exposure_policy.py +63 -0
- package/ciel_runtime_support/tool_guard_hooks.py +218 -0
- package/ciel_runtime_support/tool_request_projection.py +96 -0
- package/ciel_runtime_support/tool_schema.py +483 -0
- package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
- package/ciel_runtime_support/ui_text.py +266 -0
- package/ciel_runtime_support/upstream_error_policy.py +104 -0
- package/ciel_runtime_support/upstream_retry.py +419 -0
- package/ciel_runtime_support/upstream_stream_io.py +106 -0
- package/ciel_runtime_support/usage_events.py +96 -0
- package/ciel_runtime_support/visible_stream_filters.py +130 -0
- package/ciel_runtime_support/web_endpoints.py +447 -0
- package/ciel_runtime_support/web_ui.py +915 -0
- package/ciel_runtime_support/web_ui_controller.py +189 -0
- package/ciel_runtime_support/windows_console_input.py +137 -0
- package/ciel_runtime_support/windows_console_mode.py +112 -0
- package/docs/Architecture.md +54 -0
- package/docs/Configuration.md +17 -0
- package/docs/Module-Map.md +1093 -26
- package/docs/Providers.md +46 -1
- package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
- package/npm-bin/run-ciel-runtime.js +22 -2
- package/package.json +9 -2
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
"""Pure HTML renderers for the router web interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import html as html_lib
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
def render_web_chat_page(
|
|
9
|
+
*,
|
|
10
|
+
model: str,
|
|
11
|
+
provider: str,
|
|
12
|
+
mode: str,
|
|
13
|
+
api_status: str,
|
|
14
|
+
timeout_ms: int,
|
|
15
|
+
) -> str:
|
|
16
|
+
escaped_model = html_lib.escape(model)
|
|
17
|
+
escaped_provider = html_lib.escape(provider)
|
|
18
|
+
escaped_mode = html_lib.escape(mode)
|
|
19
|
+
escaped_api_status = html_lib.escape(api_status)
|
|
20
|
+
return f"""<!doctype html>
|
|
21
|
+
<html lang="en">
|
|
22
|
+
<head>
|
|
23
|
+
<meta charset="utf-8">
|
|
24
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
25
|
+
<title>Ciel Runtime Web Chat</title>
|
|
26
|
+
<style>
|
|
27
|
+
:root {{
|
|
28
|
+
color-scheme: dark;
|
|
29
|
+
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif;
|
|
30
|
+
--bg: #0a0d12;
|
|
31
|
+
--panel: #111827;
|
|
32
|
+
--panel-2: #162033;
|
|
33
|
+
--line: #283548;
|
|
34
|
+
--text: #eef2f8;
|
|
35
|
+
--muted: #a9b4c6;
|
|
36
|
+
--user: #174c6b;
|
|
37
|
+
--assistant: #243447;
|
|
38
|
+
--accent: #2f9e8f;
|
|
39
|
+
--danger: #fca5a5;
|
|
40
|
+
--ok: #86efac;
|
|
41
|
+
}}
|
|
42
|
+
* {{ box-sizing: border-box; }}
|
|
43
|
+
body {{ margin: 0; min-height: 100vh; background: var(--bg); color: var(--text); }}
|
|
44
|
+
.shell {{ display: grid; grid-template-columns: 280px minmax(0, 1fr); min-height: 100vh; }}
|
|
45
|
+
aside {{ border-right: 1px solid var(--line); background: #0e1521; padding: 18px; }}
|
|
46
|
+
.brand {{ font-size: 19px; font-weight: 700; letter-spacing: 0; margin: 0 0 12px; }}
|
|
47
|
+
.status-card {{ border: 1px solid var(--line); border-radius: 8px; background: var(--panel); padding: 12px; display: grid; gap: 10px; }}
|
|
48
|
+
.meta-label {{ color: var(--muted); font-size: 11px; text-transform: uppercase; }}
|
|
49
|
+
.meta-value {{ margin-top: 3px; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 12px; word-break: break-word; }}
|
|
50
|
+
.nav {{ margin-top: 14px; display: grid; gap: 8px; }}
|
|
51
|
+
.nav a, .ghost {{
|
|
52
|
+
display: flex; align-items: center; justify-content: center;
|
|
53
|
+
min-height: 36px; border-radius: 6px; border: 1px solid var(--line);
|
|
54
|
+
background: #0b111b; color: var(--text); text-decoration: none; cursor: pointer;
|
|
55
|
+
}}
|
|
56
|
+
.nav a:hover, .ghost:hover {{ border-color: var(--accent); }}
|
|
57
|
+
main {{ display: grid; grid-template-rows: auto minmax(0, 1fr) auto; min-width: 0; }}
|
|
58
|
+
header {{ min-height: 66px; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 18px; border-bottom: 1px solid var(--line); background: #0d1420; }}
|
|
59
|
+
h1 {{ margin: 0; font-size: 18px; letter-spacing: 0; }}
|
|
60
|
+
.sub {{ color: var(--muted); font-size: 12px; margin-top: 4px; }}
|
|
61
|
+
.pill {{ border: 1px solid var(--line); border-radius: 999px; padding: 5px 9px; color: var(--muted); font-size: 12px; white-space: nowrap; }}
|
|
62
|
+
#transcript {{ overflow-y: auto; padding: 18px; display: flex; flex-direction: column; gap: 12px; }}
|
|
63
|
+
.row {{ display: flex; width: 100%; }}
|
|
64
|
+
.row.user {{ justify-content: flex-end; }}
|
|
65
|
+
.bubble {{
|
|
66
|
+
max-width: min(760px, 86%);
|
|
67
|
+
border: 1px solid var(--line);
|
|
68
|
+
border-radius: 8px;
|
|
69
|
+
padding: 10px 12px;
|
|
70
|
+
line-height: 1.45;
|
|
71
|
+
white-space: normal;
|
|
72
|
+
word-break: break-word;
|
|
73
|
+
box-shadow: 0 1px 0 rgba(255,255,255,.03) inset;
|
|
74
|
+
}}
|
|
75
|
+
.row.user .bubble {{ background: var(--user); border-color: #276a8d; }}
|
|
76
|
+
.row.assistant .bubble {{ background: var(--assistant); }}
|
|
77
|
+
.row.system .bubble {{ background: #191f2b; color: var(--muted); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 12px; white-space: pre-wrap; }}
|
|
78
|
+
.markdown > :first-child {{ margin-top: 0; }}
|
|
79
|
+
.markdown > :last-child {{ margin-bottom: 0; }}
|
|
80
|
+
.markdown p {{ margin: 0 0 10px; }}
|
|
81
|
+
.markdown h1, .markdown h2, .markdown h3, .markdown h4 {{ margin: 12px 0 8px; line-height: 1.2; }}
|
|
82
|
+
.markdown h1 {{ font-size: 1.35rem; }}
|
|
83
|
+
.markdown h2 {{ font-size: 1.2rem; }}
|
|
84
|
+
.markdown h3 {{ font-size: 1.08rem; }}
|
|
85
|
+
.markdown ul, .markdown ol {{ margin: 0 0 10px 20px; padding: 0; }}
|
|
86
|
+
.markdown li {{ margin: 3px 0; }}
|
|
87
|
+
.markdown blockquote {{ margin: 0 0 10px; padding-left: 12px; border-left: 3px solid #4b6585; color: var(--muted); }}
|
|
88
|
+
.markdown pre {{ margin: 0 0 10px; padding: 10px; overflow-x: auto; border: 1px solid #33445b; border-radius: 6px; background: #0b111b; white-space: pre; }}
|
|
89
|
+
.markdown code {{ font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: .92em; }}
|
|
90
|
+
.markdown :not(pre) > code {{ padding: 1px 4px; border-radius: 4px; background: rgba(191, 219, 254, .12); }}
|
|
91
|
+
.markdown a {{ color: #8bd7ff; text-decoration: underline; text-underline-offset: 2px; }}
|
|
92
|
+
.markdown table {{ width: 100%; border-collapse: collapse; margin: 0 0 10px; display: block; overflow-x: auto; }}
|
|
93
|
+
.markdown th, .markdown td {{ border: 1px solid #3a4b63; padding: 6px 8px; text-align: left; vertical-align: top; }}
|
|
94
|
+
.markdown th {{ background: rgba(255,255,255,.06); font-weight: 700; }}
|
|
95
|
+
.markdown hr {{ border: 0; border-top: 1px solid var(--line); margin: 12px 0; }}
|
|
96
|
+
.composer {{ border-top: 1px solid var(--line); padding: 12px 18px; background: #0d1420; }}
|
|
97
|
+
.composer-inner {{ display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; align-items: end; }}
|
|
98
|
+
textarea {{
|
|
99
|
+
width: 100%; min-height: 54px; max-height: 180px; resize: vertical;
|
|
100
|
+
border: 1px solid var(--line); border-radius: 8px; background: #080d14; color: var(--text);
|
|
101
|
+
padding: 10px 12px; line-height: 1.4; font: inherit;
|
|
102
|
+
}}
|
|
103
|
+
button.primary {{
|
|
104
|
+
width: 86px; min-height: 54px; border: 1px solid #37b7a4; border-radius: 8px;
|
|
105
|
+
background: #127668; color: white; font-weight: 700; cursor: pointer;
|
|
106
|
+
}}
|
|
107
|
+
button.primary:disabled {{ opacity: .55; cursor: not-allowed; }}
|
|
108
|
+
.composer-actions {{ display: flex; gap: 8px; align-items: center; margin-top: 8px; flex-wrap: wrap; }}
|
|
109
|
+
.attach-button {{
|
|
110
|
+
min-height: 34px; border: 1px solid var(--line); border-radius: 6px;
|
|
111
|
+
background: #0b111b; color: var(--text); padding: 0 12px; cursor: pointer;
|
|
112
|
+
}}
|
|
113
|
+
.attach-button:hover {{ border-color: var(--accent); }}
|
|
114
|
+
.attach-button:disabled {{ opacity: .55; cursor: not-allowed; }}
|
|
115
|
+
#fileInput {{ display: none; }}
|
|
116
|
+
.attachment-tray {{ display: flex; gap: 7px; flex-wrap: wrap; min-height: 0; }}
|
|
117
|
+
.attachment-chip {{
|
|
118
|
+
display: inline-flex; align-items: center; gap: 7px; max-width: min(360px, 100%);
|
|
119
|
+
border: 1px solid #33445b; border-radius: 999px; background: #121b2a;
|
|
120
|
+
padding: 5px 8px; color: var(--muted); font-size: 12px;
|
|
121
|
+
}}
|
|
122
|
+
.attachment-chip span {{ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }}
|
|
123
|
+
.attachment-chip button {{
|
|
124
|
+
width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center;
|
|
125
|
+
border: 0; border-radius: 999px; background: #243447; color: var(--text); cursor: pointer;
|
|
126
|
+
}}
|
|
127
|
+
.drop-active textarea {{ border-color: var(--accent); box-shadow: 0 0 0 2px rgba(47, 158, 143, .18); }}
|
|
128
|
+
.hint {{ margin-top: 7px; color: var(--muted); font-size: 12px; }}
|
|
129
|
+
.error {{ color: var(--danger); }}
|
|
130
|
+
.ok {{ color: var(--ok); }}
|
|
131
|
+
code {{ color: #bfdbfe; }}
|
|
132
|
+
@media (max-width: 820px) {{
|
|
133
|
+
.shell {{ grid-template-columns: 1fr; }}
|
|
134
|
+
aside {{ display: none; }}
|
|
135
|
+
.bubble {{ max-width: 94%; }}
|
|
136
|
+
header {{ align-items: flex-start; flex-direction: column; }}
|
|
137
|
+
.pill {{ white-space: normal; }}
|
|
138
|
+
}}
|
|
139
|
+
</style>
|
|
140
|
+
</head>
|
|
141
|
+
<body>
|
|
142
|
+
<div class="shell">
|
|
143
|
+
<aside>
|
|
144
|
+
<div class="brand">Ciel Runtime</div>
|
|
145
|
+
<div class="status-card">
|
|
146
|
+
<div><div class="meta-label">Provider</div><div class="meta-value">{escaped_provider}</div></div>
|
|
147
|
+
<div><div class="meta-label">Mode</div><div class="meta-value">{escaped_mode}</div></div>
|
|
148
|
+
<div><div class="meta-label">Model</div><div class="meta-value">{escaped_model}</div></div>
|
|
149
|
+
<div><div class="meta-label">API</div><div class="meta-value">{escaped_api_status}</div></div>
|
|
150
|
+
<div><div class="meta-label">Timeout</div><div class="meta-value">{timeout_ms:,} ms</div></div>
|
|
151
|
+
<div><div class="meta-label">Bridge</div><div class="meta-value">active session channel</div></div>
|
|
152
|
+
</div>
|
|
153
|
+
<div class="nav">
|
|
154
|
+
<a href="/">Router Home</a>
|
|
155
|
+
<a href="/ca/events">Events</a>
|
|
156
|
+
<a href="/health">Health JSON</a>
|
|
157
|
+
<button class="ghost" id="shareButton" type="button">Copy Chat Link</button>
|
|
158
|
+
<button class="ghost" id="clearButton" type="button">Clear Chat</button>
|
|
159
|
+
</div>
|
|
160
|
+
</aside>
|
|
161
|
+
<main>
|
|
162
|
+
<header>
|
|
163
|
+
<div>
|
|
164
|
+
<h1>Session Web Chat</h1>
|
|
165
|
+
<div class="sub">Send messages into the active coding-agent session through the Ciel Runtime channel bridge and stream replies from the same channel.</div>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="pill" id="statePill">ready</div>
|
|
168
|
+
</header>
|
|
169
|
+
<section id="transcript" aria-live="polite"></section>
|
|
170
|
+
<form class="composer" id="composer">
|
|
171
|
+
<div class="composer-inner">
|
|
172
|
+
<textarea id="prompt" placeholder="Type a message..." autocomplete="off"></textarea>
|
|
173
|
+
<button class="primary" id="sendButton" type="submit">Send</button>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="composer-actions">
|
|
176
|
+
<button class="attach-button" id="attachButton" type="button">Attach files</button>
|
|
177
|
+
<input id="fileInput" type="file" multiple>
|
|
178
|
+
<div class="attachment-tray" id="attachmentTray" aria-live="polite"></div>
|
|
179
|
+
</div>
|
|
180
|
+
<div class="hint">Enter sends. Shift+Enter inserts a new line. The active coding-agent session handles the message, so its configured tools and MCP servers remain available. If replies stay queued, restart Ciel Runtime so the session wake bridge wraps the terminal.</div>
|
|
181
|
+
</form>
|
|
182
|
+
</main>
|
|
183
|
+
</div>
|
|
184
|
+
<script>
|
|
185
|
+
const MODEL = {json.dumps(model)};
|
|
186
|
+
const transcript = document.getElementById('transcript');
|
|
187
|
+
const composer = document.getElementById('composer');
|
|
188
|
+
const prompt = document.getElementById('prompt');
|
|
189
|
+
const sendButton = document.getElementById('sendButton');
|
|
190
|
+
const attachButton = document.getElementById('attachButton');
|
|
191
|
+
const fileInput = document.getElementById('fileInput');
|
|
192
|
+
const attachmentTray = document.getElementById('attachmentTray');
|
|
193
|
+
const shareButton = document.getElementById('shareButton');
|
|
194
|
+
const clearButton = document.getElementById('clearButton');
|
|
195
|
+
const statePill = document.getElementById('statePill');
|
|
196
|
+
const SESSION_KEY = 'ciel-runtime-web-chat-session';
|
|
197
|
+
const LAST_ID_KEY = 'ciel-runtime-web-chat-last-id';
|
|
198
|
+
const HISTORY_PAGE_SIZE = 80;
|
|
199
|
+
const renderedIds = new Set();
|
|
200
|
+
let oldestId = 0;
|
|
201
|
+
let historyLoading = false;
|
|
202
|
+
let historyExhausted = false;
|
|
203
|
+
function cleanSessionId(value) {{
|
|
204
|
+
return String(value || '').replace(/[^a-zA-Z0-9_.:-]/g, '').slice(0, 128);
|
|
205
|
+
}}
|
|
206
|
+
const urlParams = new URLSearchParams(location.search);
|
|
207
|
+
const urlSessionId = cleanSessionId(urlParams.get('session') || urlParams.get('s') || '');
|
|
208
|
+
const storedSessionId = cleanSessionId(localStorage.getItem(SESSION_KEY) || '');
|
|
209
|
+
const sessionId = urlSessionId || storedSessionId || (crypto.randomUUID ? crypto.randomUUID() : String(Date.now()) + '-' + Math.random().toString(16).slice(2));
|
|
210
|
+
localStorage.setItem(SESSION_KEY, sessionId);
|
|
211
|
+
if (!urlSessionId) {{
|
|
212
|
+
urlParams.set('session', sessionId);
|
|
213
|
+
const nextUrl = location.pathname + '?' + urlParams.toString() + location.hash;
|
|
214
|
+
history.replaceState(null, '', nextUrl);
|
|
215
|
+
}}
|
|
216
|
+
const channel = 'web-chat-' + sessionId;
|
|
217
|
+
const scopedLastIdKey = LAST_ID_KEY + ':' + sessionId;
|
|
218
|
+
let lastId = Number(localStorage.getItem(scopedLastIdKey) || '0') || 0;
|
|
219
|
+
let eventSource = null;
|
|
220
|
+
let selectedFiles = [];
|
|
221
|
+
function setState(text, cls = '') {{
|
|
222
|
+
statePill.textContent = text;
|
|
223
|
+
statePill.className = 'pill ' + cls;
|
|
224
|
+
}}
|
|
225
|
+
function escapeHtml(value) {{
|
|
226
|
+
return String(value ?? '').replace(/[&<>"']/g, ch => ({{
|
|
227
|
+
'&': '&',
|
|
228
|
+
'<': '<',
|
|
229
|
+
'>': '>',
|
|
230
|
+
'"': '"',
|
|
231
|
+
"'": '''
|
|
232
|
+
}}[ch]));
|
|
233
|
+
}}
|
|
234
|
+
function safeHref(value) {{
|
|
235
|
+
const href = String(value || '').trim();
|
|
236
|
+
if (/^(https?:|mailto:)/i.test(href)) return escapeHtml(href);
|
|
237
|
+
return '#';
|
|
238
|
+
}}
|
|
239
|
+
function renderInlineMarkdown(value) {{
|
|
240
|
+
const codeBlocks = [];
|
|
241
|
+
const linkBlocks = [];
|
|
242
|
+
let raw = String(value ?? '').replace(/`([^`\\n]+)`/g, (_match, code) => {{
|
|
243
|
+
const token = '\\u0000CODE' + codeBlocks.length + '\\u0000';
|
|
244
|
+
codeBlocks.push('<code>' + escapeHtml(code) + '</code>');
|
|
245
|
+
return token;
|
|
246
|
+
}});
|
|
247
|
+
raw = raw.replace(/\\[([^\\]]+)\\]\\(([^)\\s]+)\\)/g, (_match, label, href) => {{
|
|
248
|
+
const token = '\\u0000LINK' + linkBlocks.length + '\\u0000';
|
|
249
|
+
linkBlocks.push('<a href="' + safeHref(href) + '" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a>');
|
|
250
|
+
return token;
|
|
251
|
+
}});
|
|
252
|
+
let html = escapeHtml(raw);
|
|
253
|
+
html = html.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
|
|
254
|
+
html = html.replace(/(^|[\\s(])\\*([^*\\n]+)\\*/g, '$1<em>$2</em>');
|
|
255
|
+
html = html.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
|
256
|
+
linkBlocks.forEach((link, index) => {{
|
|
257
|
+
html = html.replace('\\u0000LINK' + index + '\\u0000', link);
|
|
258
|
+
}});
|
|
259
|
+
codeBlocks.forEach((code, index) => {{
|
|
260
|
+
html = html.replace('\\u0000CODE' + index + '\\u0000', code);
|
|
261
|
+
}});
|
|
262
|
+
return html;
|
|
263
|
+
}}
|
|
264
|
+
function splitMarkdownTableRow(line) {{
|
|
265
|
+
let row = String(line || '').trim();
|
|
266
|
+
if (row.startsWith('|')) row = row.slice(1);
|
|
267
|
+
if (row.endsWith('|')) row = row.slice(0, -1);
|
|
268
|
+
return row.split('|').map(cell => cell.trim());
|
|
269
|
+
}}
|
|
270
|
+
function isMarkdownDelimiterCell(cell) {{
|
|
271
|
+
const compact = String(cell || '').replace(/\\s+/g, '');
|
|
272
|
+
const core = compact.replace(/^:/, '').replace(/:$/, '');
|
|
273
|
+
return core.length >= 3 && /^-+$/.test(core);
|
|
274
|
+
}}
|
|
275
|
+
function isMarkdownTableDelimiter(line) {{
|
|
276
|
+
const cells = splitMarkdownTableRow(line);
|
|
277
|
+
return cells.length > 1 && cells.every(isMarkdownDelimiterCell);
|
|
278
|
+
}}
|
|
279
|
+
function isMarkdownTableStart(lines, index) {{
|
|
280
|
+
return index + 1 < lines.length
|
|
281
|
+
&& String(lines[index] || '').includes('|')
|
|
282
|
+
&& splitMarkdownTableRow(lines[index]).length > 1
|
|
283
|
+
&& isMarkdownTableDelimiter(lines[index + 1]);
|
|
284
|
+
}}
|
|
285
|
+
function isMarkdownBlockStart(lines, index) {{
|
|
286
|
+
const line = String(lines[index] || '');
|
|
287
|
+
const trimmed = line.trim();
|
|
288
|
+
if (!trimmed) return true;
|
|
289
|
+
return trimmed.startsWith('```')
|
|
290
|
+
|| isMarkdownTableStart(lines, index)
|
|
291
|
+
|| /^(####|###|##|#)\\s+/.test(trimmed)
|
|
292
|
+
|| /^([-*+]\\s+|\\d+[.)]\\s+|>\\s?)/.test(trimmed)
|
|
293
|
+
|| /^(---+|\\*\\*\\*+|___+)$/.test(trimmed);
|
|
294
|
+
}}
|
|
295
|
+
function renderMarkdownTable(lines, startIndex) {{
|
|
296
|
+
const headers = splitMarkdownTableRow(lines[startIndex]);
|
|
297
|
+
const rows = [];
|
|
298
|
+
let index = startIndex + 2;
|
|
299
|
+
while (index < lines.length && String(lines[index] || '').trim() && String(lines[index] || '').includes('|')) {{
|
|
300
|
+
rows.push(splitMarkdownTableRow(lines[index]));
|
|
301
|
+
index += 1;
|
|
302
|
+
}}
|
|
303
|
+
const head = '<thead><tr>' + headers.map(cell => '<th>' + renderInlineMarkdown(cell) + '</th>').join('') + '</tr></thead>';
|
|
304
|
+
const body = '<tbody>' + rows.map(row => {{
|
|
305
|
+
const cells = headers.map((_header, cellIndex) => '<td>' + renderInlineMarkdown(row[cellIndex] || '') + '</td>').join('');
|
|
306
|
+
return '<tr>' + cells + '</tr>';
|
|
307
|
+
}}).join('') + '</tbody>';
|
|
308
|
+
return {{ html: '<table>' + head + body + '</table>', nextIndex: index }};
|
|
309
|
+
}}
|
|
310
|
+
function renderMarkdown(text) {{
|
|
311
|
+
const lines = String(text ?? '').replace(/\\r\\n?/g, '\\n').split('\\n');
|
|
312
|
+
const blocks = [];
|
|
313
|
+
let index = 0;
|
|
314
|
+
while (index < lines.length) {{
|
|
315
|
+
const line = lines[index];
|
|
316
|
+
const trimmed = String(line || '').trim();
|
|
317
|
+
if (!trimmed) {{
|
|
318
|
+
index += 1;
|
|
319
|
+
continue;
|
|
320
|
+
}}
|
|
321
|
+
if (trimmed.startsWith('```')) {{
|
|
322
|
+
const code = [];
|
|
323
|
+
index += 1;
|
|
324
|
+
while (index < lines.length && !String(lines[index] || '').trim().startsWith('```')) {{
|
|
325
|
+
code.push(lines[index]);
|
|
326
|
+
index += 1;
|
|
327
|
+
}}
|
|
328
|
+
if (index < lines.length) index += 1;
|
|
329
|
+
blocks.push('<pre><code>' + escapeHtml(code.join('\\n')) + '</code></pre>');
|
|
330
|
+
continue;
|
|
331
|
+
}}
|
|
332
|
+
if (isMarkdownTableStart(lines, index)) {{
|
|
333
|
+
const table = renderMarkdownTable(lines, index);
|
|
334
|
+
blocks.push(table.html);
|
|
335
|
+
index = table.nextIndex;
|
|
336
|
+
continue;
|
|
337
|
+
}}
|
|
338
|
+
const heading = trimmed.match(/^(####|###|##|#)\\s+(.+)$/);
|
|
339
|
+
if (heading) {{
|
|
340
|
+
const level = Math.min(4, heading[1].length);
|
|
341
|
+
blocks.push('<h' + level + '>' + renderInlineMarkdown(heading[2]) + '</h' + level + '>');
|
|
342
|
+
index += 1;
|
|
343
|
+
continue;
|
|
344
|
+
}}
|
|
345
|
+
if (/^(---+|\\*\\*\\*+|___+)$/.test(trimmed)) {{
|
|
346
|
+
blocks.push('<hr>');
|
|
347
|
+
index += 1;
|
|
348
|
+
continue;
|
|
349
|
+
}}
|
|
350
|
+
if (/^[-*+]\\s+/.test(trimmed)) {{
|
|
351
|
+
const items = [];
|
|
352
|
+
while (index < lines.length && /^[-*+]\\s+/.test(String(lines[index] || '').trim())) {{
|
|
353
|
+
items.push(String(lines[index] || '').trim().replace(/^[-*+]\\s+/, ''));
|
|
354
|
+
index += 1;
|
|
355
|
+
}}
|
|
356
|
+
blocks.push('<ul>' + items.map(item => '<li>' + renderInlineMarkdown(item) + '</li>').join('') + '</ul>');
|
|
357
|
+
continue;
|
|
358
|
+
}}
|
|
359
|
+
if (/^\\d+[.)]\\s+/.test(trimmed)) {{
|
|
360
|
+
const items = [];
|
|
361
|
+
while (index < lines.length && /^\\d+[.)]\\s+/.test(String(lines[index] || '').trim())) {{
|
|
362
|
+
items.push(String(lines[index] || '').trim().replace(/^\\d+[.)]\\s+/, ''));
|
|
363
|
+
index += 1;
|
|
364
|
+
}}
|
|
365
|
+
blocks.push('<ol>' + items.map(item => '<li>' + renderInlineMarkdown(item) + '</li>').join('') + '</ol>');
|
|
366
|
+
continue;
|
|
367
|
+
}}
|
|
368
|
+
if (/^>\\s?/.test(trimmed)) {{
|
|
369
|
+
const quotes = [];
|
|
370
|
+
while (index < lines.length && /^>\\s?/.test(String(lines[index] || '').trim())) {{
|
|
371
|
+
quotes.push(String(lines[index] || '').trim().replace(/^>\\s?/, ''));
|
|
372
|
+
index += 1;
|
|
373
|
+
}}
|
|
374
|
+
blocks.push('<blockquote>' + renderInlineMarkdown(quotes.join('\\n')) + '</blockquote>');
|
|
375
|
+
continue;
|
|
376
|
+
}}
|
|
377
|
+
const paragraph = [trimmed];
|
|
378
|
+
index += 1;
|
|
379
|
+
while (index < lines.length && !isMarkdownBlockStart(lines, index)) {{
|
|
380
|
+
paragraph.push(String(lines[index] || '').trim());
|
|
381
|
+
index += 1;
|
|
382
|
+
}}
|
|
383
|
+
blocks.push('<p>' + renderInlineMarkdown(paragraph.join(' ')) + '</p>');
|
|
384
|
+
}}
|
|
385
|
+
return blocks.join('');
|
|
386
|
+
}}
|
|
387
|
+
function addBubble(role, text, mode = 'append', id = null) {{
|
|
388
|
+
if (id !== null && id !== undefined) {{
|
|
389
|
+
const key = String(id);
|
|
390
|
+
if (renderedIds.has(key)) return null;
|
|
391
|
+
renderedIds.add(key);
|
|
392
|
+
}}
|
|
393
|
+
const row = document.createElement('div');
|
|
394
|
+
row.className = 'row ' + role;
|
|
395
|
+
const bubble = document.createElement('div');
|
|
396
|
+
bubble.className = 'bubble';
|
|
397
|
+
if (role === 'system') {{
|
|
398
|
+
bubble.textContent = text;
|
|
399
|
+
}} else {{
|
|
400
|
+
bubble.classList.add('markdown');
|
|
401
|
+
bubble.innerHTML = renderMarkdown(text);
|
|
402
|
+
}}
|
|
403
|
+
row.appendChild(bubble);
|
|
404
|
+
if (mode === 'prepend') {{
|
|
405
|
+
transcript.insertBefore(row, transcript.firstChild);
|
|
406
|
+
}} else {{
|
|
407
|
+
transcript.appendChild(row);
|
|
408
|
+
transcript.scrollTop = transcript.scrollHeight;
|
|
409
|
+
}}
|
|
410
|
+
return bubble;
|
|
411
|
+
}}
|
|
412
|
+
function rememberLastId(id) {{
|
|
413
|
+
const numeric = Number(id || 0) || 0;
|
|
414
|
+
if (numeric > lastId) {{
|
|
415
|
+
lastId = numeric;
|
|
416
|
+
localStorage.setItem(scopedLastIdKey, String(lastId));
|
|
417
|
+
}}
|
|
418
|
+
}}
|
|
419
|
+
function roleForMessage(message) {{
|
|
420
|
+
return message.sender_id === 'web-user' ? 'user' : 'assistant';
|
|
421
|
+
}}
|
|
422
|
+
function renderIncomingMessage(message, mode = 'append') {{
|
|
423
|
+
if (mode !== 'prepend') rememberLastId(message.id);
|
|
424
|
+
const text = message.message || '';
|
|
425
|
+
if (!text.trim()) return;
|
|
426
|
+
addBubble(roleForMessage(message), text, mode, message.id);
|
|
427
|
+
if (mode !== 'prepend' && message.sender_id !== 'web-user') setState('reply received', 'ok');
|
|
428
|
+
}}
|
|
429
|
+
function formatBytes(bytes) {{
|
|
430
|
+
const value = Number(bytes || 0);
|
|
431
|
+
if (value < 1024) return value + ' B';
|
|
432
|
+
if (value < 1024 * 1024) return (value / 1024).toFixed(1).replace(/\\.0$/, '') + ' KB';
|
|
433
|
+
return (value / (1024 * 1024)).toFixed(1).replace(/\\.0$/, '') + ' MB';
|
|
434
|
+
}}
|
|
435
|
+
function renderAttachmentTray() {{
|
|
436
|
+
attachmentTray.innerHTML = '';
|
|
437
|
+
selectedFiles.forEach((file, index) => {{
|
|
438
|
+
const chip = document.createElement('div');
|
|
439
|
+
chip.className = 'attachment-chip';
|
|
440
|
+
const label = document.createElement('span');
|
|
441
|
+
label.textContent = file.name + ' (' + formatBytes(file.size) + ')';
|
|
442
|
+
const remove = document.createElement('button');
|
|
443
|
+
remove.type = 'button';
|
|
444
|
+
remove.setAttribute('aria-label', 'Remove ' + file.name);
|
|
445
|
+
remove.textContent = 'x';
|
|
446
|
+
remove.addEventListener('click', () => {{
|
|
447
|
+
selectedFiles.splice(index, 1);
|
|
448
|
+
renderAttachmentTray();
|
|
449
|
+
}});
|
|
450
|
+
chip.appendChild(label);
|
|
451
|
+
chip.appendChild(remove);
|
|
452
|
+
attachmentTray.appendChild(chip);
|
|
453
|
+
}});
|
|
454
|
+
}}
|
|
455
|
+
function addSelectedFiles(fileList) {{
|
|
456
|
+
const incoming = Array.from(fileList || []);
|
|
457
|
+
if (!incoming.length) return;
|
|
458
|
+
selectedFiles = selectedFiles.concat(incoming);
|
|
459
|
+
renderAttachmentTray();
|
|
460
|
+
setState(selectedFiles.length + ' file(s) ready', 'ok');
|
|
461
|
+
}}
|
|
462
|
+
function fileToBase64(file) {{
|
|
463
|
+
return new Promise((resolve, reject) => {{
|
|
464
|
+
const reader = new FileReader();
|
|
465
|
+
reader.onload = () => {{
|
|
466
|
+
const dataUrl = String(reader.result || '');
|
|
467
|
+
const comma = dataUrl.indexOf(',');
|
|
468
|
+
resolve(comma >= 0 ? dataUrl.slice(comma + 1) : dataUrl);
|
|
469
|
+
}};
|
|
470
|
+
reader.onerror = () => reject(reader.error || new Error('Could not read file'));
|
|
471
|
+
reader.readAsDataURL(file);
|
|
472
|
+
}});
|
|
473
|
+
}}
|
|
474
|
+
async function uploadAttachment(file) {{
|
|
475
|
+
const content = await fileToBase64(file);
|
|
476
|
+
const response = await fetch('/ca/channel/files', {{
|
|
477
|
+
method: 'POST',
|
|
478
|
+
headers: {{'content-type': 'application/json', 'accept': 'application/json'}},
|
|
479
|
+
body: JSON.stringify({{
|
|
480
|
+
channel,
|
|
481
|
+
sender_id: 'web-user',
|
|
482
|
+
recipients: ['all'],
|
|
483
|
+
thread_id: sessionId,
|
|
484
|
+
announce: false,
|
|
485
|
+
name: file.name,
|
|
486
|
+
content_type: file.type || 'application/octet-stream',
|
|
487
|
+
encoding: 'base64',
|
|
488
|
+
content
|
|
489
|
+
}})
|
|
490
|
+
}});
|
|
491
|
+
const text = await response.text();
|
|
492
|
+
let json = {{}};
|
|
493
|
+
try {{ json = text ? JSON.parse(text) : {{}}; }} catch {{}}
|
|
494
|
+
if (!response.ok || !json.ok) {{
|
|
495
|
+
throw new Error(json.error || text || `Upload failed with HTTP ${{response.status}}`);
|
|
496
|
+
}}
|
|
497
|
+
return {{
|
|
498
|
+
name: json.name,
|
|
499
|
+
original_name: json.original_name || file.name,
|
|
500
|
+
url: json.url,
|
|
501
|
+
path: json.path,
|
|
502
|
+
bytes: json.bytes,
|
|
503
|
+
content_type: json.content_type || file.type || 'application/octet-stream'
|
|
504
|
+
}};
|
|
505
|
+
}}
|
|
506
|
+
async function uploadAttachments(files) {{
|
|
507
|
+
const uploads = [];
|
|
508
|
+
for (const file of files) {{
|
|
509
|
+
setState('uploading ' + file.name);
|
|
510
|
+
uploads.push(await uploadAttachment(file));
|
|
511
|
+
}}
|
|
512
|
+
return uploads;
|
|
513
|
+
}}
|
|
514
|
+
function attachmentSummary(uploads) {{
|
|
515
|
+
if (!uploads.length) return '';
|
|
516
|
+
const lines = uploads.map(file => {{
|
|
517
|
+
const label = file.original_name || file.name || 'file';
|
|
518
|
+
const size = formatBytes(file.bytes);
|
|
519
|
+
const type = file.content_type || 'application/octet-stream';
|
|
520
|
+
const url = file.url || file.path || '';
|
|
521
|
+
return '- [' + label + '](' + url + ') (' + size + ', ' + type + ') - router URL: ' + url;
|
|
522
|
+
}});
|
|
523
|
+
return 'Attached files:\\n' + lines.join('\\n');
|
|
524
|
+
}}
|
|
525
|
+
function buildOutboundText(text, uploads) {{
|
|
526
|
+
const trimmed = String(text || '').trim();
|
|
527
|
+
const summary = attachmentSummary(uploads);
|
|
528
|
+
if (trimmed && summary) return trimmed + '\\n\\n' + summary;
|
|
529
|
+
return trimmed || summary;
|
|
530
|
+
}}
|
|
531
|
+
function updateHistoryBounds(messages) {{
|
|
532
|
+
if (!Array.isArray(messages) || messages.length === 0) return;
|
|
533
|
+
const ids = messages.map(message => Number(message.id || 0)).filter(id => id > 0);
|
|
534
|
+
if (!ids.length) return;
|
|
535
|
+
const minId = Math.min(...ids);
|
|
536
|
+
const maxId = Math.max(...ids);
|
|
537
|
+
oldestId = oldestId ? Math.min(oldestId, minId) : minId;
|
|
538
|
+
rememberLastId(maxId);
|
|
539
|
+
}}
|
|
540
|
+
async function fetchMessagePage(params) {{
|
|
541
|
+
const query = new URLSearchParams({{
|
|
542
|
+
channel,
|
|
543
|
+
recipient: 'web',
|
|
544
|
+
limit: String(HISTORY_PAGE_SIZE),
|
|
545
|
+
...params
|
|
546
|
+
}});
|
|
547
|
+
const response = await fetch('/ca/channel/messages?' + query.toString(), {{headers: {{'accept': 'application/json'}}}});
|
|
548
|
+
if (!response.ok) throw new Error(await response.text() || `HTTP ${{response.status}}`);
|
|
549
|
+
return await response.json();
|
|
550
|
+
}}
|
|
551
|
+
async function loadInitialHistory() {{
|
|
552
|
+
try {{
|
|
553
|
+
const json = await fetchMessagePage({{latest: '1'}});
|
|
554
|
+
const messages = Array.isArray(json.messages) ? json.messages : [];
|
|
555
|
+
messages.forEach(message => renderIncomingMessage(message, 'append'));
|
|
556
|
+
updateHistoryBounds(messages);
|
|
557
|
+
historyExhausted = messages.length < HISTORY_PAGE_SIZE;
|
|
558
|
+
}} catch (err) {{
|
|
559
|
+
addBubble('system', 'Could not load chat history: ' + String(err && err.message ? err.message : err));
|
|
560
|
+
}}
|
|
561
|
+
}}
|
|
562
|
+
async function loadOlderHistory() {{
|
|
563
|
+
if (historyLoading || historyExhausted || !oldestId) return;
|
|
564
|
+
historyLoading = true;
|
|
565
|
+
const previousHeight = transcript.scrollHeight;
|
|
566
|
+
try {{
|
|
567
|
+
const json = await fetchMessagePage({{before: String(oldestId)}});
|
|
568
|
+
const messages = Array.isArray(json.messages) ? json.messages : [];
|
|
569
|
+
if (!messages.length) {{
|
|
570
|
+
historyExhausted = true;
|
|
571
|
+
return;
|
|
572
|
+
}}
|
|
573
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {{
|
|
574
|
+
renderIncomingMessage(messages[i], 'prepend');
|
|
575
|
+
}}
|
|
576
|
+
updateHistoryBounds(messages);
|
|
577
|
+
historyExhausted = messages.length < HISTORY_PAGE_SIZE;
|
|
578
|
+
transcript.scrollTop = transcript.scrollHeight - previousHeight;
|
|
579
|
+
}} catch (err) {{
|
|
580
|
+
addBubble('system', 'Could not load older history: ' + String(err && err.message ? err.message : err));
|
|
581
|
+
}} finally {{
|
|
582
|
+
historyLoading = false;
|
|
583
|
+
}}
|
|
584
|
+
}}
|
|
585
|
+
function startChannelStream() {{
|
|
586
|
+
if (eventSource) eventSource.close();
|
|
587
|
+
const url = `/ca/channel/stream?channel=${{encodeURIComponent(channel)}}&recipient=web&after=${{lastId}}&timeout=3600`;
|
|
588
|
+
eventSource = new EventSource(url);
|
|
589
|
+
eventSource.onopen = () => setState('listening', 'ok');
|
|
590
|
+
eventSource.onmessage = ev => {{
|
|
591
|
+
try {{
|
|
592
|
+
const message = JSON.parse(ev.data);
|
|
593
|
+
renderIncomingMessage(message);
|
|
594
|
+
}} catch {{}}
|
|
595
|
+
}};
|
|
596
|
+
eventSource.onerror = () => {{
|
|
597
|
+
if (eventSource) eventSource.close();
|
|
598
|
+
setState('reconnecting');
|
|
599
|
+
setTimeout(startChannelStream, 1200);
|
|
600
|
+
}};
|
|
601
|
+
}}
|
|
602
|
+
async function sendMessage(text, files = []) {{
|
|
603
|
+
setState('queued');
|
|
604
|
+
sendButton.disabled = true;
|
|
605
|
+
attachButton.disabled = true;
|
|
606
|
+
try {{
|
|
607
|
+
const uploads = await uploadAttachments(files);
|
|
608
|
+
const outboundText = buildOutboundText(text, uploads);
|
|
609
|
+
const response = await fetch('/ca/channel/messages', {{
|
|
610
|
+
method: 'POST',
|
|
611
|
+
headers: {{'content-type': 'application/json', 'accept': 'application/json'}},
|
|
612
|
+
body: JSON.stringify({{
|
|
613
|
+
channel,
|
|
614
|
+
sender_id: 'web-user',
|
|
615
|
+
recipients: ['all'],
|
|
616
|
+
delivery: ['llm', 'native'],
|
|
617
|
+
thread_id: sessionId,
|
|
618
|
+
kind: 'web_chat',
|
|
619
|
+
message: outboundText,
|
|
620
|
+
meta: {{
|
|
621
|
+
source: 'ciel-runtime-web-chat',
|
|
622
|
+
web_chat_session: sessionId,
|
|
623
|
+
reply_channel: channel,
|
|
624
|
+
reply_recipient: 'web',
|
|
625
|
+
reply_instruction: 'Use the ciel-runtime-router send_message tool to answer this browser chat on the same channel/thread_id with recipients web and delivery web. Use send_file when returning a file attachment to this browser chat.',
|
|
626
|
+
attachments: uploads
|
|
627
|
+
}}
|
|
628
|
+
}})
|
|
629
|
+
}});
|
|
630
|
+
if (!response.ok) {{
|
|
631
|
+
const fallback = await response.text();
|
|
632
|
+
throw new Error(fallback || `HTTP ${{response.status}}`);
|
|
633
|
+
}}
|
|
634
|
+
const json = await response.json();
|
|
635
|
+
if (json.message) renderIncomingMessage(json.message);
|
|
636
|
+
else addBubble('user', outboundText);
|
|
637
|
+
addBubble('system', 'Message queued for the active coding-agent session. Waiting for a channel reply. If this never changes, restart Ciel Runtime so the session wake bridge is active.');
|
|
638
|
+
setState('waiting for session');
|
|
639
|
+
}} catch (err) {{
|
|
640
|
+
const bubble = addBubble('assistant', String(err && err.message ? err.message : err));
|
|
641
|
+
bubble.classList.add('error');
|
|
642
|
+
setState('error', 'error');
|
|
643
|
+
}} finally {{
|
|
644
|
+
sendButton.disabled = false;
|
|
645
|
+
attachButton.disabled = false;
|
|
646
|
+
prompt.focus();
|
|
647
|
+
}}
|
|
648
|
+
}}
|
|
649
|
+
composer.addEventListener('submit', ev => {{
|
|
650
|
+
ev.preventDefault();
|
|
651
|
+
const text = prompt.value.trim();
|
|
652
|
+
const files = selectedFiles.slice();
|
|
653
|
+
if (!text && !files.length) return;
|
|
654
|
+
prompt.value = '';
|
|
655
|
+
selectedFiles = [];
|
|
656
|
+
renderAttachmentTray();
|
|
657
|
+
sendMessage(text, files);
|
|
658
|
+
}});
|
|
659
|
+
prompt.addEventListener('keydown', ev => {{
|
|
660
|
+
if (ev.key === 'Enter' && !ev.shiftKey) {{
|
|
661
|
+
ev.preventDefault();
|
|
662
|
+
composer.requestSubmit();
|
|
663
|
+
}}
|
|
664
|
+
}});
|
|
665
|
+
attachButton.addEventListener('click', () => fileInput.click());
|
|
666
|
+
fileInput.addEventListener('change', () => {{
|
|
667
|
+
addSelectedFiles(fileInput.files);
|
|
668
|
+
fileInput.value = '';
|
|
669
|
+
}});
|
|
670
|
+
composer.addEventListener('dragover', ev => {{
|
|
671
|
+
if (!ev.dataTransfer || !ev.dataTransfer.files || !ev.dataTransfer.files.length) return;
|
|
672
|
+
ev.preventDefault();
|
|
673
|
+
composer.classList.add('drop-active');
|
|
674
|
+
}});
|
|
675
|
+
composer.addEventListener('dragleave', () => composer.classList.remove('drop-active'));
|
|
676
|
+
composer.addEventListener('drop', ev => {{
|
|
677
|
+
if (!ev.dataTransfer || !ev.dataTransfer.files || !ev.dataTransfer.files.length) return;
|
|
678
|
+
ev.preventDefault();
|
|
679
|
+
composer.classList.remove('drop-active');
|
|
680
|
+
addSelectedFiles(ev.dataTransfer.files);
|
|
681
|
+
}});
|
|
682
|
+
clearButton.addEventListener('click', () => {{
|
|
683
|
+
transcript.innerHTML = '';
|
|
684
|
+
renderedIds.clear();
|
|
685
|
+
oldestId = 0;
|
|
686
|
+
historyExhausted = false;
|
|
687
|
+
selectedFiles = [];
|
|
688
|
+
renderAttachmentTray();
|
|
689
|
+
addBubble('system', `Chat cleared. This browser sends to active coding-agent session channel ${{channel}}.`);
|
|
690
|
+
startChannelStream();
|
|
691
|
+
}});
|
|
692
|
+
shareButton.addEventListener('click', async () => {{
|
|
693
|
+
const url = new URL(location.href);
|
|
694
|
+
url.searchParams.set('session', sessionId);
|
|
695
|
+
try {{
|
|
696
|
+
await navigator.clipboard.writeText(url.toString());
|
|
697
|
+
setState('link copied', 'ok');
|
|
698
|
+
}} catch {{
|
|
699
|
+
prompt.value = url.toString();
|
|
700
|
+
prompt.focus();
|
|
701
|
+
prompt.select();
|
|
702
|
+
setState('copy manually');
|
|
703
|
+
}}
|
|
704
|
+
}});
|
|
705
|
+
transcript.addEventListener('scroll', () => {{
|
|
706
|
+
if (transcript.scrollTop < 48) loadOlderHistory();
|
|
707
|
+
}});
|
|
708
|
+
addBubble('system', `Connected to active session bridge for ${{MODEL}}. Messages are queued on channel ${{channel}} and replies stream back from /ca/channel/stream.`);
|
|
709
|
+
loadInitialHistory().finally(startChannelStream);
|
|
710
|
+
prompt.focus();
|
|
711
|
+
</script>
|
|
712
|
+
</body>
|
|
713
|
+
</html>"""
|
|
714
|
+
|
|
715
|
+
def render_router_home_page(
|
|
716
|
+
*,
|
|
717
|
+
version: str,
|
|
718
|
+
provider: str,
|
|
719
|
+
model: str,
|
|
720
|
+
context_text: str,
|
|
721
|
+
timeout_ms: int,
|
|
722
|
+
idle_ms: int,
|
|
723
|
+
rpm_text: str,
|
|
724
|
+
upstream_text: str,
|
|
725
|
+
) -> str:
|
|
726
|
+
links = [
|
|
727
|
+
("Events UI", "/ca/events", "Live router event stream with filters"),
|
|
728
|
+
("Session web chat", "/ca/web/chat", "Bridge messages into the active coding-agent session"),
|
|
729
|
+
("Recent events JSON", "/ca/events/recent", "Latest structured event records"),
|
|
730
|
+
("Events SSE", "/ca/events/stream", "Server-sent events stream"),
|
|
731
|
+
("Chat health", "/ca/chat/health", "Agent chat component status"),
|
|
732
|
+
("Chat messages", "/ca/chat/messages", "Stored agent chat messages"),
|
|
733
|
+
("Channel bridge", "/ca/channel/health", "External channel bridge API"),
|
|
734
|
+
("Channel messages", "/ca/channel/messages", "Messages posted through channel bridge"),
|
|
735
|
+
("Plan artifacts", "/ca/plan/artifacts", "Plan mode artifacts served by router"),
|
|
736
|
+
("Models", "/v1/models", "Claude-compatible model list"),
|
|
737
|
+
("Health", "/health", "Machine-readable health JSON"),
|
|
738
|
+
]
|
|
739
|
+
link_html = "\n".join(
|
|
740
|
+
f'<a class="link" href="{html_lib.escape(href)}"><strong>{html_lib.escape(label)}</strong><span>{html_lib.escape(desc)}</span></a>'
|
|
741
|
+
for label, href, desc in links
|
|
742
|
+
)
|
|
743
|
+
return f"""<!doctype html>
|
|
744
|
+
<html lang="en">
|
|
745
|
+
<head>
|
|
746
|
+
<meta charset="utf-8">
|
|
747
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
748
|
+
<title>Ciel Runtime Router</title>
|
|
749
|
+
<style>
|
|
750
|
+
:root {{ color-scheme: dark; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif; }}
|
|
751
|
+
body {{ margin: 0; background: #090b0f; color: #e8edf4; }}
|
|
752
|
+
header {{ padding: 22px 24px 16px; border-bottom: 1px solid #253044; background: #101722; }}
|
|
753
|
+
h1 {{ margin: 0 0 6px; font-size: 24px; letter-spacing: 0; }}
|
|
754
|
+
.sub {{ color: #a8b3c5; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }}
|
|
755
|
+
.topnav {{ position: sticky; top: 0; z-index: 10; display: flex; gap: 6px; padding: 10px 24px; background: #0b111a; border-bottom: 1px solid #253044; overflow-x: auto; }}
|
|
756
|
+
.tab, .chat-tab {{ box-sizing: border-box; min-width: 96px; min-height: 34px; border-radius: 6px; border: 1px solid #334155; background: #101722; color: #cbd5e1; cursor: pointer; }}
|
|
757
|
+
.chat-tab {{ display: inline-flex; align-items: center; justify-content: center; padding: 6px 12px; border-color: #2563eb; background: #12304f; color: #eff6ff; font-weight: 700; text-decoration: none; white-space: nowrap; }}
|
|
758
|
+
.chat-tab:hover {{ background: #17406a; border-color: #60a5fa; }}
|
|
759
|
+
.tab:hover {{ border-color: #60a5fa; color: #eff6ff; }}
|
|
760
|
+
.tab.active {{ background: #1d4ed8; border-color: #60a5fa; color: white; }}
|
|
761
|
+
main {{ max-width: 1180px; margin: 0 auto; padding: 18px; }}
|
|
762
|
+
.view {{ display: none; }}
|
|
763
|
+
.view.active {{ display: block; }}
|
|
764
|
+
.view h2 {{ margin: 0 0 12px; font-size: 20px; }}
|
|
765
|
+
.grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 10px; }}
|
|
766
|
+
.card, .link, .events {{ background: #0d131d; border: 1px solid #253044; border-radius: 8px; padding: 12px; }}
|
|
767
|
+
.label {{ color: #93a4ba; font-size: 12px; text-transform: uppercase; }}
|
|
768
|
+
.value {{ margin-top: 5px; font-size: 15px; word-break: break-word; }}
|
|
769
|
+
.overview-action {{ display: flex; align-items: center; justify-content: space-between; gap: 14px; margin-top: 14px; padding: 14px; border: 1px solid #1d4ed8; border-radius: 8px; background: #0d1b2d; }}
|
|
770
|
+
.overview-action strong {{ display: block; color: #eff6ff; }}
|
|
771
|
+
.overview-action span {{ display: block; margin-top: 4px; color: #a8b3c5; font-size: 13px; }}
|
|
772
|
+
.overview-action .chat-tab {{ flex: 0 0 auto; }}
|
|
773
|
+
.links {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 10px; margin-top: 18px; }}
|
|
774
|
+
a.link {{ display: block; color: #dbeafe; text-decoration: none; }}
|
|
775
|
+
a.link:hover {{ border-color: #60a5fa; }}
|
|
776
|
+
a.link span {{ display: block; margin-top: 4px; color: #93a4ba; font-size: 13px; }}
|
|
777
|
+
.events {{ margin-top: 18px; }}
|
|
778
|
+
.settings {{ background: #0d131d; border: 1px solid #253044; border-radius: 8px; padding: 12px; }}
|
|
779
|
+
.settings h2, .events h2 {{ margin: 0 0 10px; font-size: 18px; }}
|
|
780
|
+
.settings-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 10px; }}
|
|
781
|
+
.control {{ display: grid; gap: 6px; }}
|
|
782
|
+
.control label {{ color: #93a4ba; font-size: 12px; text-transform: uppercase; }}
|
|
783
|
+
input, select, button {{ min-height: 34px; border-radius: 6px; border: 1px solid #334155; background: #080d14; color: #e8edf4; padding: 6px 8px; }}
|
|
784
|
+
button {{ cursor: pointer; background: #12304f; border-color: #2563eb; }}
|
|
785
|
+
button:hover {{ background: #17406a; }}
|
|
786
|
+
.option-row {{ display: grid; grid-template-columns: minmax(240px, 1fr) minmax(160px, 260px) auto; gap: 8px; align-items: center; padding: 8px 0; border-top: 1px solid #1f2937; }}
|
|
787
|
+
.option-row .name {{ color: #dbeafe; word-break: break-word; }}
|
|
788
|
+
.messages {{ margin-top: 10px; color: #c4b5fd; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; white-space: pre-wrap; }}
|
|
789
|
+
.event {{ padding: 8px 0; border-top: 1px solid #1f2937; }}
|
|
790
|
+
.event:first-child {{ border-top: 0; }}
|
|
791
|
+
.meta {{ color: #93a4ba; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 12px; }}
|
|
792
|
+
.preview {{ margin-top: 4px; color: #cbd5e1; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 12px; white-space: pre-wrap; word-break: break-word; }}
|
|
793
|
+
code {{ color: #bfdbfe; }}
|
|
794
|
+
</style>
|
|
795
|
+
</head>
|
|
796
|
+
<body>
|
|
797
|
+
<header>
|
|
798
|
+
<h1>Ciel Runtime Router</h1>
|
|
799
|
+
<div class="sub">v{html_lib.escape(version)} · {html_lib.escape(provider)} · {html_lib.escape(model)}</div>
|
|
800
|
+
</header>
|
|
801
|
+
<nav class="topnav" aria-label="Router sections">
|
|
802
|
+
<button class="tab active" data-view="overview">Overview</button>
|
|
803
|
+
<a class="chat-tab" href="/ca/web/chat">Web Chat</a>
|
|
804
|
+
<button class="tab" data-view="settings">LLM Settings</button>
|
|
805
|
+
<button class="tab" data-view="events">Events</button>
|
|
806
|
+
<button class="tab" data-view="endpoints">Endpoints</button>
|
|
807
|
+
</nav>
|
|
808
|
+
<main>
|
|
809
|
+
<section id="view-overview" class="view active">
|
|
810
|
+
<h2>Overview</h2>
|
|
811
|
+
<div class="grid">
|
|
812
|
+
<div class="card"><div class="label">Provider</div><div class="value">{html_lib.escape(provider)}</div></div>
|
|
813
|
+
<div class="card"><div class="label">Model</div><div class="value">{html_lib.escape(model)}</div></div>
|
|
814
|
+
<div class="card"><div class="label">Context</div><div class="value">{html_lib.escape(context_text)}</div></div>
|
|
815
|
+
<div class="card"><div class="label">Timeout</div><div class="value">{timeout_ms:,} ms · idle {idle_ms:,} ms</div></div>
|
|
816
|
+
<div class="card"><div class="label">RPM</div><div class="value">{html_lib.escape(rpm_text)}</div></div>
|
|
817
|
+
<div class="card"><div class="label">Upstream</div><div class="value">{html_lib.escape(upstream_text)}</div></div>
|
|
818
|
+
</div>
|
|
819
|
+
<div class="overview-action">
|
|
820
|
+
<div><strong>Session Web Chat</strong><span>Send messages and files to the active coding-agent session.</span></div>
|
|
821
|
+
<a class="chat-tab" href="/ca/web/chat">Open Web Chat</a>
|
|
822
|
+
</div>
|
|
823
|
+
</section>
|
|
824
|
+
<section id="view-settings" class="view">
|
|
825
|
+
<h2>LLM Settings</h2>
|
|
826
|
+
<div class="settings">
|
|
827
|
+
<div class="settings-grid">
|
|
828
|
+
<div class="control"><label>Model</label><input id="modelInput"><button id="modelApply">Apply model</button></div>
|
|
829
|
+
<div class="control"><label>Advisor Model</label><input id="advisorInput" placeholder="off or model id"><button id="advisorApply">Apply advisor</button></div>
|
|
830
|
+
<div class="control"><label>Preset</label><select id="presetSelect"></select><button id="presetApply">Apply preset</button></div>
|
|
831
|
+
<div class="control"><label>Context Setup</label><select id="contextSelect"></select><button id="contextApply">Apply context</button></div>
|
|
832
|
+
<div class="control"><label>Timeout Profile</label><select id="timeoutSelect"></select><button id="timeoutApply">Apply timeout</button></div>
|
|
833
|
+
</div>
|
|
834
|
+
<div id="optionRows"></div>
|
|
835
|
+
<div id="settingsMessages" class="messages"></div>
|
|
836
|
+
</div>
|
|
837
|
+
</section>
|
|
838
|
+
<section id="view-events" class="view events">
|
|
839
|
+
<h2>Recent Events</h2>
|
|
840
|
+
<div id="events"><div class="meta">Loading /ca/events/recent...</div></div>
|
|
841
|
+
</section>
|
|
842
|
+
<section id="view-endpoints" class="view">
|
|
843
|
+
<h2>Endpoints</h2>
|
|
844
|
+
<div class="links">{link_html}</div>
|
|
845
|
+
</section>
|
|
846
|
+
</main>
|
|
847
|
+
<script>
|
|
848
|
+
const tabs = Array.from(document.querySelectorAll('.tab'));
|
|
849
|
+
const views = Array.from(document.querySelectorAll('.view'));
|
|
850
|
+
function showView(name) {{
|
|
851
|
+
tabs.forEach(tab => tab.classList.toggle('active', tab.dataset.view === name));
|
|
852
|
+
views.forEach(view => view.classList.toggle('active', view.id === 'view-' + name));
|
|
853
|
+
if (location.hash !== '#' + name) history.replaceState(null, '', '#' + name);
|
|
854
|
+
}}
|
|
855
|
+
tabs.forEach(tab => tab.addEventListener('click', () => showView(tab.dataset.view)));
|
|
856
|
+
const initialView = (location.hash || '#overview').slice(1);
|
|
857
|
+
if (tabs.some(tab => tab.dataset.view === initialView)) showView(initialView);
|
|
858
|
+
const el = document.getElementById('events');
|
|
859
|
+
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({{'&':'&','<':'<','>':'>','"':'"',"'":'''}}[c]));
|
|
860
|
+
const modelInput = document.getElementById('modelInput');
|
|
861
|
+
const advisorInput = document.getElementById('advisorInput');
|
|
862
|
+
const presetSelect = document.getElementById('presetSelect');
|
|
863
|
+
const contextSelect = document.getElementById('contextSelect');
|
|
864
|
+
const timeoutSelect = document.getElementById('timeoutSelect');
|
|
865
|
+
const optionRows = document.getElementById('optionRows');
|
|
866
|
+
const settingsMessages = document.getElementById('settingsMessages');
|
|
867
|
+
function fillSelect(select, rows, current) {{
|
|
868
|
+
select.innerHTML = (rows || []).map(item => `<option value="${{esc(item.value)}}">${{esc(item.label)}}</option>`).join('');
|
|
869
|
+
if (current) select.value = current;
|
|
870
|
+
}}
|
|
871
|
+
async function loadSettings(messages = []) {{
|
|
872
|
+
const res = await fetch('/ca/config/llm');
|
|
873
|
+
const data = await res.json();
|
|
874
|
+
modelInput.value = data.model || '';
|
|
875
|
+
advisorInput.value = data.advisor_model || '';
|
|
876
|
+
fillSelect(presetSelect, data.presets, data.preset);
|
|
877
|
+
fillSelect(contextSelect, data.contexts);
|
|
878
|
+
fillSelect(timeoutSelect, data.timeouts);
|
|
879
|
+
optionRows.innerHTML = (data.options || []).map(item => `<div class="option-row"><div class="name">${{esc(item.label)}}</div><input data-key="${{esc(item.key)}}" value="${{esc(item.value)}}"><button data-key="${{esc(item.key)}}">Apply</button></div>`).join('');
|
|
880
|
+
settingsMessages.textContent = (messages.length ? messages : data.messages || []).join('\\n');
|
|
881
|
+
}}
|
|
882
|
+
async function postSettings(payload) {{
|
|
883
|
+
settingsMessages.textContent = 'Saving...';
|
|
884
|
+
const res = await fetch('/ca/config/llm', {{method:'POST', headers:{{'content-type':'application/json'}}, body: JSON.stringify(payload)}});
|
|
885
|
+
const data = await res.json();
|
|
886
|
+
if (!res.ok || !data.ok) {{
|
|
887
|
+
settingsMessages.textContent = (data.messages || [data.error || 'Update failed']).join('\\n');
|
|
888
|
+
return;
|
|
889
|
+
}}
|
|
890
|
+
await loadSettings(data.messages || []);
|
|
891
|
+
}}
|
|
892
|
+
document.getElementById('modelApply').onclick = () => postSettings({{action:'model', value:modelInput.value}});
|
|
893
|
+
document.getElementById('advisorApply').onclick = () => postSettings({{action:'advisor_model', value:advisorInput.value}});
|
|
894
|
+
document.getElementById('presetApply').onclick = () => postSettings({{action:'preset', value:presetSelect.value}});
|
|
895
|
+
document.getElementById('contextApply').onclick = () => postSettings({{action:'context_setup', value:contextSelect.value}});
|
|
896
|
+
document.getElementById('timeoutApply').onclick = () => postSettings({{action:'timeout_profile', value:timeoutSelect.value}});
|
|
897
|
+
optionRows.addEventListener('click', ev => {{
|
|
898
|
+
const button = ev.target.closest('button[data-key]');
|
|
899
|
+
if (!button) return;
|
|
900
|
+
const input = optionRows.querySelector(`input[data-key="${{CSS.escape(button.dataset.key)}}"]`);
|
|
901
|
+
postSettings({{action:'option', key:button.dataset.key, value:input ? input.value : ''}});
|
|
902
|
+
}});
|
|
903
|
+
loadSettings();
|
|
904
|
+
fetch('/ca/events/recent?limit=20').then(r => r.json()).then(j => {{
|
|
905
|
+
const events = j.events || [];
|
|
906
|
+
el.innerHTML = events.length ? events.reverse().map(e => {{
|
|
907
|
+
const preview = e.data && e.data.message_preview ? `<div class="preview">${{esc(e.data.message_preview)}}${{e.data.message_preview_truncated ? '…' : ''}}</div>` : '';
|
|
908
|
+
return `<div class="event"><div class="meta">#${{e.id}} ${{esc(e.time)}} · ${{esc(e.level)}} · ${{esc(e.category)}} · ${{esc(e.provider)}} ${{esc(e.model)}}</div><div>${{esc(e.message)}}</div>${{preview}}</div>`;
|
|
909
|
+
}}).join('') : '<div class="meta">No events yet.</div>';
|
|
910
|
+
}}).catch(err => {{ el.innerHTML = '<div class="meta">Could not load events: ' + esc(err) + '</div>'; }});
|
|
911
|
+
</script>
|
|
912
|
+
</body>
|
|
913
|
+
</html>"""
|
|
914
|
+
|
|
915
|
+
__all__ = ["render_router_home_page", "render_web_chat_page"]
|