@azure-id/orc 0.55.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/CHANGELOG.md +2017 -0
- package/README-id.md +521 -0
- package/README.md +485 -0
- package/bin/build-agents.js +86 -0
- package/bin/cli.js +29050 -0
- package/bin/mockrun-catalog.js +267 -0
- package/bin/onboarding-content.js +172 -0
- package/bin/pricing.json +200 -0
- package/bin/providers.json +437 -0
- package/bin/ui.js +95 -0
- package/bin/verify-contracts.js +3277 -0
- package/bin/verify-package.js +498 -0
- package/bin/webui/api.js +1163 -0
- package/bin/webui/app.html +198 -0
- package/bin/webui/css/00-tokens.css +92 -0
- package/bin/webui/css/01-base.css +42 -0
- package/bin/webui/css/02-shell.css +103 -0
- package/bin/webui/css/03-components.css +462 -0
- package/bin/webui/css/04-motion.css +91 -0
- package/bin/webui/css/05-tour.css +100 -0
- package/bin/webui/css/06-responsive.css +108 -0
- package/bin/webui/css/panels/boundary.css +26 -0
- package/bin/webui/css/panels/challenge.css +151 -0
- package/bin/webui/css/panels/crosslink.css +194 -0
- package/bin/webui/css/panels/docs.css +254 -0
- package/bin/webui/css/panels/experiment.css +23 -0
- package/bin/webui/css/panels/extra.css +536 -0
- package/bin/webui/css/panels/flow.css +177 -0
- package/bin/webui/css/panels/handoff.css +67 -0
- package/bin/webui/css/panels/knowledge.css +86 -0
- package/bin/webui/css/panels/learn.css +154 -0
- package/bin/webui/css/panels/maintenance.css +55 -0
- package/bin/webui/css/panels/mockrun.css +268 -0
- package/bin/webui/css/panels/overview.css +100 -0
- package/bin/webui/css/panels/pact.css +43 -0
- package/bin/webui/css/panels/runs.css +105 -0
- package/bin/webui/css/panels/settings.css +227 -0
- package/bin/webui/css/panels/stats.css +87 -0
- package/bin/webui/fixtures/boundary.js +92 -0
- package/bin/webui/fixtures/challenge.js +825 -0
- package/bin/webui/fixtures/crosslink.js +31 -0
- package/bin/webui/fixtures/docs.js +982 -0
- package/bin/webui/fixtures/extra.js +1934 -0
- package/bin/webui/fixtures/flow.js +81 -0
- package/bin/webui/fixtures/handoff.js +35 -0
- package/bin/webui/fixtures/index.js +486 -0
- package/bin/webui/fixtures/knowledge.js +291 -0
- package/bin/webui/fixtures/maintenance.js +42 -0
- package/bin/webui/fixtures/mockrun.js +36 -0
- package/bin/webui/fixtures/pact.js +111 -0
- package/bin/webui/fixtures/runs.js +109 -0
- package/bin/webui/fixtures/settings.js +78 -0
- package/bin/webui/fixtures/shell.js +48 -0
- package/bin/webui/fixtures/stats.js +104 -0
- package/bin/webui/i18n/TERMS.md +134 -0
- package/bin/webui/i18n/en/banner.json +29 -0
- package/bin/webui/i18n/en/boundary.json +20 -0
- package/bin/webui/i18n/en/challenge.json +106 -0
- package/bin/webui/i18n/en/common.json +69 -0
- package/bin/webui/i18n/en/crosslink.json +69 -0
- package/bin/webui/i18n/en/docs.json +175 -0
- package/bin/webui/i18n/en/experiment.json +17 -0
- package/bin/webui/i18n/en/extra.json +313 -0
- package/bin/webui/i18n/en/flow.json +30 -0
- package/bin/webui/i18n/en/handoff.json +37 -0
- package/bin/webui/i18n/en/knowledge.json +141 -0
- package/bin/webui/i18n/en/learn.json +16 -0
- package/bin/webui/i18n/en/maintenance.json +52 -0
- package/bin/webui/i18n/en/mockrun.json +19 -0
- package/bin/webui/i18n/en/nav.json +19 -0
- package/bin/webui/i18n/en/overview.json +89 -0
- package/bin/webui/i18n/en/pact.json +36 -0
- package/bin/webui/i18n/en/runs.json +51 -0
- package/bin/webui/i18n/en/settings.json +49 -0
- package/bin/webui/i18n/en/stats.json +69 -0
- package/bin/webui/i18n/en/tour.json +33 -0
- package/bin/webui/i18n/id/banner.json +29 -0
- package/bin/webui/i18n/id/boundary.json +20 -0
- package/bin/webui/i18n/id/challenge.json +106 -0
- package/bin/webui/i18n/id/common.json +70 -0
- package/bin/webui/i18n/id/crosslink.json +69 -0
- package/bin/webui/i18n/id/docs.json +175 -0
- package/bin/webui/i18n/id/experiment.json +17 -0
- package/bin/webui/i18n/id/extra.json +313 -0
- package/bin/webui/i18n/id/flow.json +30 -0
- package/bin/webui/i18n/id/handoff.json +37 -0
- package/bin/webui/i18n/id/knowledge.json +141 -0
- package/bin/webui/i18n/id/learn.json +16 -0
- package/bin/webui/i18n/id/maintenance.json +52 -0
- package/bin/webui/i18n/id/mockrun.json +19 -0
- package/bin/webui/i18n/id/nav.json +19 -0
- package/bin/webui/i18n/id/overview.json +89 -0
- package/bin/webui/i18n/id/pact.json +36 -0
- package/bin/webui/i18n/id/runs.json +51 -0
- package/bin/webui/i18n/id/settings.json +49 -0
- package/bin/webui/i18n/id/stats.json +69 -0
- package/bin/webui/i18n/id/tour.json +33 -0
- package/bin/webui/js/00-core.js +115 -0
- package/bin/webui/js/01-i18n.js +149 -0
- package/bin/webui/js/02-ui.js +211 -0
- package/bin/webui/js/03-md.js +286 -0
- package/bin/webui/js/04-router.js +51 -0
- package/bin/webui/js/05-banners.js +172 -0
- package/bin/webui/js/06-edit.js +183 -0
- package/bin/webui/js/90-tour.js +488 -0
- package/bin/webui/js/91-shortcuts.js +126 -0
- package/bin/webui/js/99-boot.js +118 -0
- package/bin/webui/js/panels/boundary.js +104 -0
- package/bin/webui/js/panels/challenge.js +781 -0
- package/bin/webui/js/panels/crosslink.js +654 -0
- package/bin/webui/js/panels/docs.js +1338 -0
- package/bin/webui/js/panels/experiment.js +98 -0
- package/bin/webui/js/panels/extra.js +3011 -0
- package/bin/webui/js/panels/flow.js +297 -0
- package/bin/webui/js/panels/handoff.js +169 -0
- package/bin/webui/js/panels/knowledge.js +890 -0
- package/bin/webui/js/panels/learn.js +220 -0
- package/bin/webui/js/panels/maintenance.js +235 -0
- package/bin/webui/js/panels/mockrun.js +227 -0
- package/bin/webui/js/panels/overview.js +447 -0
- package/bin/webui/js/panels/pact.js +189 -0
- package/bin/webui/js/panels/runs.js +416 -0
- package/bin/webui/js/panels/settings.js +573 -0
- package/bin/webui/js/panels/stats.js +378 -0
- package/bin/webui/serve.js +557 -0
- package/mock-run/INDEX.md +106 -0
- package/mock-run/a-normal-day.md +587 -0
- package/mock-run/context-combiner.md +100 -0
- package/mock-run/extra-recovery.md +330 -0
- package/mock-run/extra-slots.md +177 -0
- package/mock-run/media/README.md +43 -0
- package/mock-run/orc-aftermath.md +392 -0
- package/mock-run/orc-boundary.md +356 -0
- package/mock-run/orc-brainstorm.md +177 -0
- package/mock-run/orc-budget.md +534 -0
- package/mock-run/orc-challenge-council.md +262 -0
- package/mock-run/orc-challenge.md +416 -0
- package/mock-run/orc-cli.md +200 -0
- package/mock-run/orc-diy.md +129 -0
- package/mock-run/orc-doc.md +448 -0
- package/mock-run/orc-explain.md +86 -0
- package/mock-run/orc-export.md +423 -0
- package/mock-run/orc-extra.md +392 -0
- package/mock-run/orc-fast.md +106 -0
- package/mock-run/orc-grill.md +151 -0
- package/mock-run/orc-handoff.md +480 -0
- package/mock-run/orc-pact.md +421 -0
- package/mock-run/orc-pattern.md +112 -0
- package/mock-run/orc-plan.md +108 -0
- package/mock-run/orc-pr-setup.md +126 -0
- package/mock-run/orc-quick.md +113 -0
- package/mock-run/orc-route.md +93 -0
- package/mock-run/orc-ui.md +125 -0
- package/mock-run/orc-ultra.md +103 -0
- package/mock-run/orc.md +157 -0
- package/mock-run/the-example-project.md +55 -0
- package/package.json +39 -0
- package/templates/agents/MODEL-MAPPING.md +168 -0
- package/templates/agents/orc-advisor-fable-5.md +50 -0
- package/templates/agents/orc-advisor-opus-5-xhigh.md +56 -0
- package/templates/agents/orc-analyst-fable-5.md +115 -0
- package/templates/agents/orc-analyze-mini-opus-5-med.md +60 -0
- package/templates/agents/orc-analyze-mini-sonnet-5-high.md +58 -0
- package/templates/agents/orc-challenge-advisor-opus-5-med.md +75 -0
- package/templates/agents/orc-challenge-contrarian-opus-5-high.md +110 -0
- package/templates/agents/orc-challenge-executor-opus-5-med.md +114 -0
- package/templates/agents/orc-challenge-expansionist-opus-5-med.md +112 -0
- package/templates/agents/orc-challenge-judge-opus-5-high.md +132 -0
- package/templates/agents/orc-challenge-outsider-opus-5-low.md +109 -0
- package/templates/agents/orc-challenge-principles-opus-5-high.md +109 -0
- package/templates/agents/orc-challenge-reader-opus-5-low.md +90 -0
- package/templates/agents/orc-claude-writer-opus-4-8-high.md +53 -0
- package/templates/agents/orc-claude-writer-opus-5-med.md +55 -0
- package/templates/agents/orc-context-combiner-opus-5-high.md +88 -0
- package/templates/agents/orc-doc-checker-opus-5-low.md +108 -0
- package/templates/agents/orc-doc-writer-opus-5-med.md +134 -0
- package/templates/agents/orc-executor-haiku-4-5.md +113 -0
- package/templates/agents/orc-executor-opus-4-7-high.md +114 -0
- package/templates/agents/orc-executor-opus-4-7-med.md +114 -0
- package/templates/agents/orc-executor-opus-4-8-high.md +114 -0
- package/templates/agents/orc-executor-opus-5-high.md +114 -0
- package/templates/agents/orc-executor-opus-5-low.md +114 -0
- package/templates/agents/orc-executor-opus-5-med.md +114 -0
- package/templates/agents/orc-executor-sonnet-4-6-high.md +114 -0
- package/templates/agents/orc-executor-sonnet-4-6-med.md +114 -0
- package/templates/agents/orc-executor-sonnet-5-high.md +114 -0
- package/templates/agents/orc-judge-fable-5.md +79 -0
- package/templates/agents/orc-judge-opus-5-xhigh.md +85 -0
- package/templates/agents/orc-learn-writer-opus-5-low.md +73 -0
- package/templates/agents/orc-pattern-codifier-opus-5-med.md +65 -0
- package/templates/agents/orc-pattern-codifier-sonnet-5-high.md +63 -0
- package/templates/agents/orc-planner-fable-5.md +152 -0
- package/templates/agents/orc-planner-mini-opus-5-med.md +69 -0
- package/templates/agents/orc-planner-mini-sonnet-5-high.md +67 -0
- package/templates/agents/orc-planner-opus-5-med.md +160 -0
- package/templates/agents/orc-retro-opus-5-med.md +73 -0
- package/templates/agents/orc-retro-sonnet-5-high.md +72 -0
- package/templates/agents/orc-reviewer-fable-5.md +57 -0
- package/templates/agents/orc-reviewer-opus-5-med.md +60 -0
- package/templates/agents/orc-scout-opus-5-low.md +40 -0
- package/templates/agents/orc-scout-sonnet-4-6-high.md +39 -0
- package/templates/agents/orc-system-analyst-opus-5-high.md +120 -0
- package/templates/agents/orc-test-author-opus-5-med.md +71 -0
- package/templates/agents/orc-trace-writer-haiku-4-5.md +107 -0
- package/templates/agents/orc-verifier-opus-5-med.md +69 -0
- package/templates/agents/orc-wiki-scanner-opus-4-8-high.md +79 -0
- package/templates/agents/orc-wiki-scanner-opus-5-med.md +81 -0
- package/templates/agents/orc-wiki-scanner-sonnet-5-high.md +106 -0
- package/templates/commands/orc-aftermath.md +49 -0
- package/templates/commands/orc-analyze.md +21 -0
- package/templates/commands/orc-boundary.md +46 -0
- package/templates/commands/orc-brainstorm.md +51 -0
- package/templates/commands/orc-budget.md +54 -0
- package/templates/commands/orc-challenge.md +58 -0
- package/templates/commands/orc-claude.md +14 -0
- package/templates/commands/orc-diy.md +13 -0
- package/templates/commands/orc-doc.md +128 -0
- package/templates/commands/orc-explain.md +20 -0
- package/templates/commands/orc-export.md +46 -0
- package/templates/commands/orc-fast.md +15 -0
- package/templates/commands/orc-grill.md +40 -0
- package/templates/commands/orc-handoff.md +52 -0
- package/templates/commands/orc-learn.md +17 -0
- package/templates/commands/orc-mini.md +12 -0
- package/templates/commands/orc-pact.md +44 -0
- package/templates/commands/orc-pattern.md +15 -0
- package/templates/commands/orc-plan.md +22 -0
- package/templates/commands/orc-poly.md +21 -0
- package/templates/commands/orc-pr-driver.md +30 -0
- package/templates/commands/orc-pr-setup.md +31 -0
- package/templates/commands/orc-quick.md +33 -0
- package/templates/commands/orc-retro.md +17 -0
- package/templates/commands/orc-route.md +41 -0
- package/templates/commands/orc-ultra.md +17 -0
- package/templates/commands/orc-verify.md +11 -0
- package/templates/commands/orc-wiki.md +42 -0
- package/templates/commands/orc.md +16 -0
- package/templates/hooks/orc-effort-guard.js +178 -0
- package/templates/hooks/orc-statusline.js +305 -0
- package/templates/hooks/orc-trace.js +471 -0
- package/templates/hooks/orc-update-lib.js +125 -0
- package/templates/skills/_shared/README.md +50 -0
- package/templates/skills/_shared/detecting-artifacts.md +90 -0
- package/templates/skills/_shared/drift-recovery.md +98 -0
- package/templates/skills/_shared/extra-dispatch.md +1051 -0
- package/templates/skills/_shared/fable5-override.md +56 -0
- package/templates/skills/_shared/fallback-handoff.md +42 -0
- package/templates/skills/_shared/gh-stack-commands.md +65 -0
- package/templates/skills/_shared/gotchas.md +177 -0
- package/templates/skills/_shared/interview.md +144 -0
- package/templates/skills/_shared/lane-suspend.md +82 -0
- package/templates/skills/_shared/opus5-only.md +131 -0
- package/templates/skills/_shared/pr-templates.md +106 -0
- package/templates/skills/_shared/read-ladder.md +54 -0
- package/templates/skills/_shared/return-validation.md +145 -0
- package/templates/skills/_shared/smoke-gate.md +28 -0
- package/templates/skills/_shared/stack-plan.md +135 -0
- package/templates/skills/_shared/untrusted-input.md +63 -0
- package/templates/skills/context-combiner/SKILL.md +187 -0
- package/templates/skills/context-combiner/schemas/combined-report.md +78 -0
- package/templates/skills/context-combiner/schemas/combined-requirement-spec.md +87 -0
- package/templates/skills/orc/README.md +150 -0
- package/templates/skills/orc/SKILL.md +526 -0
- package/templates/skills/orc/config.md +389 -0
- package/templates/skills/orc/examples/full-run-mock.md +73 -0
- package/templates/skills/orc/references/analyst-gates.md +124 -0
- package/templates/skills/orc/references/effort-and-mode.md +224 -0
- package/templates/skills/orc/references/house-rules.md +20 -0
- package/templates/skills/orc/references/intake.md +120 -0
- package/templates/skills/orc/references/log-protocol.md +39 -0
- package/templates/skills/orc/references/pattern-gate.md +89 -0
- package/templates/skills/orc/references/plan-handoff.md +84 -0
- package/templates/skills/orc/references/preflight-report.md +220 -0
- package/templates/skills/orc/references/security-checklist.md +39 -0
- package/templates/skills/orc/references/stop-and-resume.md +160 -0
- package/templates/skills/orc/references/trace-protocol.md +314 -0
- package/templates/skills/orc/references/ultra-mode.md +123 -0
- package/templates/skills/orc/references/wave-grouping.md +121 -0
- package/templates/skills/orc/references/wiki-consult.md +177 -0
- package/templates/skills/orc/schemas/checkpoint.md +122 -0
- package/templates/skills/orc/schemas/intent-spec.md +42 -0
- package/templates/skills/orc/schemas/planning-output.md +279 -0
- package/templates/skills/orc/subskills/orc-checkpoint/SKILL.md +55 -0
- package/templates/skills/orc/subskills/orc-execution/SKILL.md +72 -0
- package/templates/skills/orc/subskills/orc-execution/core.md +151 -0
- package/templates/skills/orc/subskills/orc-execution/subagent.md +13 -0
- package/templates/skills/orc/subskills/orc-planner/SKILL.md +264 -0
- package/templates/skills/orc/subskills/orc-planner-mini/SKILL.md +121 -0
- package/templates/skills/orc/subskills/orc-pr/SKILL.md +51 -0
- package/templates/skills/orc/subskills/orc-pr/pr.md +22 -0
- package/templates/skills/orc/subskills/orc-pr/stack-gate.md +108 -0
- package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +76 -0
- package/templates/skills/orc/subskills/orc-review-verify/core.md +135 -0
- package/templates/skills/orc/subskills/orc-review-verify/subagent.md +7 -0
- package/templates/skills/orc/subskills/orc-testgen/SKILL.md +45 -0
- package/templates/skills/orc/subskills/orc-testgen/core.md +59 -0
- package/templates/skills/orc/subskills/orc-testgen/subagent.md +7 -0
- package/templates/skills/orc-advisor/SKILL.md +60 -0
- package/templates/skills/orc-aftermath/SKILL.md +136 -0
- package/templates/skills/orc-aftermath/references/report.md +95 -0
- package/templates/skills/orc-analyze/SKILL.md +220 -0
- package/templates/skills/orc-analyze/examples/analyze-mock.md +42 -0
- package/templates/skills/orc-analyze/references/branching.md +51 -0
- package/templates/skills/orc-analyze/references/deep-mode.md +27 -0
- package/templates/skills/orc-analyze/references/thin-input.md +77 -0
- package/templates/skills/orc-analyze/schemas/report-audit.md +83 -0
- package/templates/skills/orc-analyze/schemas/report-prose.md +63 -0
- package/templates/skills/orc-analyze/schemas/report-requirement.md +78 -0
- package/templates/skills/orc-analyze/schemas/requirement-spec.md +77 -0
- package/templates/skills/orc-analyze-mini/SKILL.md +112 -0
- package/templates/skills/orc-analyze-mini/examples/quick-analysis-mock.md +31 -0
- package/templates/skills/orc-boundary/SKILL.md +208 -0
- package/templates/skills/orc-boundary/references/card.md +78 -0
- package/templates/skills/orc-boundary/references/gate.md +113 -0
- package/templates/skills/orc-brainstorm/SKILL.md +339 -0
- package/templates/skills/orc-brainstorm/references/brainstorm-doc.md +133 -0
- package/templates/skills/orc-brainstorm/references/lenses.md +152 -0
- package/templates/skills/orc-budget/SKILL.md +225 -0
- package/templates/skills/orc-budget/references/corpus.md +87 -0
- package/templates/skills/orc-challenge/README.md +142 -0
- package/templates/skills/orc-challenge/SKILL.md +255 -0
- package/templates/skills/orc-challenge/examples/code-module.md +110 -0
- package/templates/skills/orc-challenge/examples/council-full-roster.md +273 -0
- package/templates/skills/orc-challenge/examples/tsd-two-iterations.md +213 -0
- package/templates/skills/orc-challenge/references/conservation.md +107 -0
- package/templates/skills/orc-challenge/references/council.md +315 -0
- package/templates/skills/orc-challenge/references/cycle-state.md +185 -0
- package/templates/skills/orc-challenge/references/dimensions.md +121 -0
- package/templates/skills/orc-challenge/references/fix-brief.md +130 -0
- package/templates/skills/orc-challenge/references/intake.md +171 -0
- package/templates/skills/orc-challenge/references/kinds.md +44 -0
- package/templates/skills/orc-challenge/references/plain-english.md +98 -0
- package/templates/skills/orc-challenge/references/rubric.md +109 -0
- package/templates/skills/orc-challenge/references/sealed-slice.md +97 -0
- package/templates/skills/orc-challenge/references/verdict-doc.md +154 -0
- package/templates/skills/orc-claude/SKILL.md +199 -0
- package/templates/skills/orc-claude/examples/claude-run-mock.md +65 -0
- package/templates/skills/orc-claude/references/refresh.md +76 -0
- package/templates/skills/orc-claude/references/template.md +109 -0
- package/templates/skills/orc-diy/README.md +142 -0
- package/templates/skills/orc-diy/SKILL.md +70 -0
- package/templates/skills/orc-diy/references/blocks/analyze.md +24 -0
- package/templates/skills/orc-diy/references/blocks/execution.md +42 -0
- package/templates/skills/orc-diy/references/blocks/extra.md +55 -0
- package/templates/skills/orc-diy/references/blocks/header.md +55 -0
- package/templates/skills/orc-diy/references/blocks/mock-example.md +22 -0
- package/templates/skills/orc-diy/references/blocks/pattern.md +18 -0
- package/templates/skills/orc-diy/references/blocks/planning.md +34 -0
- package/templates/skills/orc-diy/references/blocks/review.md +18 -0
- package/templates/skills/orc-diy/references/blocks/scoring.md +16 -0
- package/templates/skills/orc-diy/references/blocks/security.md +25 -0
- package/templates/skills/orc-diy/references/blocks/ship.md +25 -0
- package/templates/skills/orc-diy/references/blocks/summary.md +18 -0
- package/templates/skills/orc-diy/references/blocks/testgen.md +17 -0
- package/templates/skills/orc-diy/references/blocks/trace.md +28 -0
- package/templates/skills/orc-diy/references/blocks/verify.md +25 -0
- package/templates/skills/orc-diy/references/blocks/wiki.md +26 -0
- package/templates/skills/orc-diy/references/compile.md +59 -0
- package/templates/skills/orc-diy/references/flow-schema.md +100 -0
- package/templates/skills/orc-diy/references/locked-blocks.md +37 -0
- package/templates/skills/orc-doc/README.md +229 -0
- package/templates/skills/orc-doc/SKILL.md +444 -0
- package/templates/skills/orc-doc/examples/orc-doc-prd-run.md +325 -0
- package/templates/skills/orc-doc/references/chunking.md +527 -0
- package/templates/skills/orc-doc/references/gates.md +311 -0
- package/templates/skills/orc-doc/references/generation-rules.md +122 -0
- package/templates/skills/orc-doc/references/house-rules.md +170 -0
- package/templates/skills/orc-doc/references/import-targets.md +40 -0
- package/templates/skills/orc-doc/references/plain-language.md +66 -0
- package/templates/skills/orc-doc/references/portable-markdown.md +74 -0
- package/templates/skills/orc-doc/references/resume-protocol.md +228 -0
- package/templates/skills/orc-doc/references/templates/collaboration.md +118 -0
- package/templates/skills/orc-doc/references/templates/prd.md +133 -0
- package/templates/skills/orc-doc/references/templates/report.md +102 -0
- package/templates/skills/orc-doc/references/templates/tsd.md +122 -0
- package/templates/skills/orc-doc/references/templates/workflow.md +112 -0
- package/templates/skills/orc-explain/SKILL.md +57 -0
- package/templates/skills/orc-export/SKILL.md +154 -0
- package/templates/skills/orc-fast/SKILL.md +188 -0
- package/templates/skills/orc-grill/SKILL.md +207 -0
- package/templates/skills/orc-grill/references/grill-doc.md +105 -0
- package/templates/skills/orc-handoff/SKILL.md +204 -0
- package/templates/skills/orc-handoff/references/handoff-log.md +67 -0
- package/templates/skills/orc-handoff/references/surfaces.md +88 -0
- package/templates/skills/orc-judge/SKILL.md +82 -0
- package/templates/skills/orc-learn/SKILL.md +156 -0
- package/templates/skills/orc-learn/examples/learn-run-mock.md +61 -0
- package/templates/skills/orc-learn/references/deepen.md +70 -0
- package/templates/skills/orc-learn/references/refresh.md +65 -0
- package/templates/skills/orc-learn/references/template-knowledge.md +83 -0
- package/templates/skills/orc-learn/references/template-learning.md +67 -0
- package/templates/skills/orc-mini/SKILL.md +226 -0
- package/templates/skills/orc-mini/examples/mini-run-mock.md +50 -0
- package/templates/skills/orc-pact/SKILL.md +218 -0
- package/templates/skills/orc-pact/references/gate.md +70 -0
- package/templates/skills/orc-pact/references/ledger.md +99 -0
- package/templates/skills/orc-pattern/SKILL.md +124 -0
- package/templates/skills/orc-pattern/references/INDEX.md +73 -0
- package/templates/skills/orc-pattern/references/be-django.md +80 -0
- package/templates/skills/orc-pattern/references/be-express.md +82 -0
- package/templates/skills/orc-pattern/references/be-fastapi.md +107 -0
- package/templates/skills/orc-pattern/references/be-go.md +96 -0
- package/templates/skills/orc-pattern/references/be-nestjs.md +86 -0
- package/templates/skills/orc-pattern/references/be-postgres.md +98 -0
- package/templates/skills/orc-pattern/references/fe-a11y.md +36 -0
- package/templates/skills/orc-pattern/references/fe-angular.md +87 -0
- package/templates/skills/orc-pattern/references/fe-nextjs.md +65 -0
- package/templates/skills/orc-pattern/references/fe-perf.md +39 -0
- package/templates/skills/orc-pattern/references/fe-react.md +83 -0
- package/templates/skills/orc-pattern/references/fe-vue.md +75 -0
- package/templates/skills/orc-pattern/schemas/pattern-doc.md +62 -0
- package/templates/skills/orc-poly/SKILL.md +216 -0
- package/templates/skills/orc-poly/examples/poly-run-mock.md +51 -0
- package/templates/skills/orc-poly/references/gather.md +70 -0
- package/templates/skills/orc-poly/references/poly-spec.md +84 -0
- package/templates/skills/orc-pr-driver/README.md +349 -0
- package/templates/skills/orc-pr-driver/SKILL.md +167 -0
- package/templates/skills/orc-pr-driver/references/conflict-playbook.md +78 -0
- package/templates/skills/orc-pr-driver/references/green-gate.md +105 -0
- package/templates/skills/orc-pr-driver/references/orc-run-split.md +99 -0
- package/templates/skills/orc-pr-setup/README.md +436 -0
- package/templates/skills/orc-pr-setup/SKILL.md +184 -0
- package/templates/skills/orc-pr-setup/references/certainty-gate.md +66 -0
- package/templates/skills/orc-pr-setup/references/layer-taxonomy.md +67 -0
- package/templates/skills/orc-quick/README.md +423 -0
- package/templates/skills/orc-quick/SKILL.md +306 -0
- package/templates/skills/orc-quick/references/context-doc.md +114 -0
- package/templates/skills/orc-quick/references/dispatch-gate.md +163 -0
- package/templates/skills/orc-quick/references/gh-mode.md +127 -0
- package/templates/skills/orc-retro/SKILL.md +222 -0
- package/templates/skills/orc-retro/examples/retro-mock.md +171 -0
- package/templates/skills/orc-route/SKILL.md +165 -0
- package/templates/skills/orc-verify/SKILL.md +86 -0
- package/templates/skills/orc-verify/examples/verify-mock.md +33 -0
- package/templates/skills/orc-wiki/README.md +325 -0
- package/templates/skills/orc-wiki/SKILL.md +322 -0
- package/templates/skills/orc-wiki/examples/wiki-run-mock.md +37 -0
- package/templates/skills/orc-wiki/references/claude-md-injection.md +61 -0
- package/templates/skills/orc-wiki/references/crosslink-compile.md +39 -0
- package/templates/skills/orc-wiki/references/crosslink-kinds.md +57 -0
- package/templates/skills/orc-wiki/references/crosslink.md +284 -0
- package/templates/skills/orc-wiki/references/extra.md +79 -0
- package/templates/skills/orc-wiki/references/integrity-check.md +86 -0
- package/templates/skills/orc-wiki/references/orientation.md +51 -0
- package/templates/skills/orc-wiki/references/partial-refresh.md +203 -0
- package/templates/skills/orc-wiki/references/pattern-prewarm.md +19 -0
- package/templates/skills/orc-wiki/references/staleness.md +288 -0
- package/templates/skills/orc-wiki/schemas/crosslink-tag.md +166 -0
- package/templates/skills/orc-wiki/schemas/wiki-doc.md +120 -0
|
@@ -0,0 +1,3011 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* panels/extra.js — orc ui client
|
|
3
|
+
Integrate with other AI model. The panel for `orc extra`: which providers ORC
|
|
4
|
+
can reach, which connections you have, and whether each one has ever answered.
|
|
5
|
+
|
|
6
|
+
THE PANEL NAMES NO PROVIDER, NO MODEL AND NO PRICE. Every tile below is a row
|
|
7
|
+
of `orc extra providers --json`, every chip is a field of `orc extra list
|
|
8
|
+
--json`, and every state word is one the CLI computed — the Flow-stepper rule.
|
|
9
|
+
Model ids are deliberately not shipped at all (they rot within a quarter), so
|
|
10
|
+
the only model names that ever appear here are the ones a real connection test
|
|
11
|
+
read back from the provider.
|
|
12
|
+
|
|
13
|
+
THE ONE THING THIS PANEL DERIVES IS NOTHING. Not a freshness tier, not a
|
|
14
|
+
verification state, not an attempt ceiling: STALE and UNVERIFIED arrive as
|
|
15
|
+
`orc extra doctor` findings keyed by profile, and the attempt ceiling is the
|
|
16
|
+
config key's own value.
|
|
17
|
+
|
|
18
|
+
THE BOUNDARY CARD IS PANEL PROSE, and that is deliberate rather than an
|
|
19
|
+
oversight. It makes no claim about this repo's state — it explains what the
|
|
20
|
+
subsystem does — so it is a sentence a human reads and therefore a sentence
|
|
21
|
+
that must translate. Everything beside it that IS a fact about your setup (the
|
|
22
|
+
engine, the base URL, whether a key was found) comes from the CLI verbatim.
|
|
23
|
+
|
|
24
|
+
Loaded by app.html in the order its numeric prefix names. Classic
|
|
25
|
+
script, no import/export: an ES module import carries no query string,
|
|
26
|
+
and every static request here needs the per-launch session token. */
|
|
27
|
+
|
|
28
|
+
/* ================================================================== EXTRA == */
|
|
29
|
+
|
|
30
|
+
PANELS.extra = function (host) {
|
|
31
|
+
head(host, t("extra.title"), t("extra.sub"));
|
|
32
|
+
const body = el("div", "stack");
|
|
33
|
+
host.append(body);
|
|
34
|
+
renderExtra(body);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
async function renderExtra(body) {
|
|
38
|
+
body.replaceChildren(skeleton(6));
|
|
39
|
+
// Four reads, in parallel. A read that FAILED is not a read that came back
|
|
40
|
+
// empty (v0.49.4): the error is kept so the card can say which half is
|
|
41
|
+
// missing instead of rendering as "you have nothing configured".
|
|
42
|
+
const [listRes, provRes, docRes, cfgRes, routeRes, roleRes, laneRes, statRes, rateRes, toolRes, jourRes] = await Promise.all([
|
|
43
|
+
read("/api/extra").catch((e) => ({ data: null, error: e })),
|
|
44
|
+
read("/api/extra/providers").catch((e) => ({ data: null, error: e })),
|
|
45
|
+
read("/api/extra/doctor").catch((e) => ({ data: null, error: e })),
|
|
46
|
+
read("/api/config").catch((e) => ({ data: null, error: e })),
|
|
47
|
+
read("/api/extra/route").catch((e) => ({ data: null, error: e })),
|
|
48
|
+
// v0.55.0 — THE POSITIONS. It exits 1 when nothing routes, which is
|
|
49
|
+
// exit-code-as-DATA like every other gate command on this panel.
|
|
50
|
+
read("/api/extra/role").catch((e) => ({ data: null, error: e })),
|
|
51
|
+
read("/api/extra/lanes").catch((e) => ({ data: null, error: e })),
|
|
52
|
+
read("/api/extra/stats").catch((e) => ({ data: null, error: e })),
|
|
53
|
+
read("/api/extra/rates").catch((e) => ({ data: null, error: e })),
|
|
54
|
+
// v0.51.0 — the LOCAL TOOLS read. It exits 1 when nothing is ready, which is
|
|
55
|
+
// exit-code-as-DATA like every other gate command on this panel.
|
|
56
|
+
read("/api/extra/tools").catch((e) => ({ data: null, error: e })),
|
|
57
|
+
// v0.54.0 — the journal listing. It is a FREE read and its `orphans` count
|
|
58
|
+
// is the one thing the Recovery tab exists to surface.
|
|
59
|
+
read("/api/extra/journal").catch((e) => ({ data: null, error: e })),
|
|
60
|
+
]);
|
|
61
|
+
const d = {
|
|
62
|
+
list: listRes.data,
|
|
63
|
+
providers: provRes.data,
|
|
64
|
+
doctor: docRes.data,
|
|
65
|
+
config: cfgRes.data,
|
|
66
|
+
route: routeRes.data,
|
|
67
|
+
role: roleRes.data,
|
|
68
|
+
lanes: laneRes.data,
|
|
69
|
+
// `orc extra stats` exits 1 with a real object when nothing has been
|
|
70
|
+
// dispatched yet — an ANSWER, not an error, so it is read like every other
|
|
71
|
+
// exit-code-as-data command on this panel.
|
|
72
|
+
stats: statRes.data,
|
|
73
|
+
rates: rateRes.data,
|
|
74
|
+
tools: toolRes.data,
|
|
75
|
+
journal: jourRes.data,
|
|
76
|
+
errors: {
|
|
77
|
+
list: listRes.error || null,
|
|
78
|
+
providers: provRes.error || null,
|
|
79
|
+
route: routeRes.error || null,
|
|
80
|
+
role: roleRes.error || null,
|
|
81
|
+
lanes: laneRes.error || null,
|
|
82
|
+
tools: toolRes.error || null,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// ONE staged-edit set for the whole panel, and it OUTLIVES a re-render on
|
|
87
|
+
// purpose: a connection test in the middle of planning a routing change must
|
|
88
|
+
// not silently throw the plan away.
|
|
89
|
+
// NOTHING RE-RENDERS UNTIL APPLY (v0.44.1), so staging only repaints the bar
|
|
90
|
+
// and each control repaints its OWN state. A full re-render on every click
|
|
91
|
+
// would re-fetch five endpoints and scroll the list out from under the person
|
|
92
|
+
// using it — the exact fight that release was written to end.
|
|
93
|
+
if (!EX_EDITS) EX_EDITS = editSet(() => EX_BAR && EX_BAR.paint());
|
|
94
|
+
// A render that gated the bar away must not leave the previous one behind as
|
|
95
|
+
// a live reference: paint() on a detached node is a write to nothing.
|
|
96
|
+
EX_BAR = null;
|
|
97
|
+
const edits = EX_EDITS;
|
|
98
|
+
// Which config keys this panel owns is the CLI's answer, read fresh every
|
|
99
|
+
// render. A hard-coded list here would be a second registry.
|
|
100
|
+
EX_CONFIG_KEYS = ((d.config && d.config.keys) || []).filter((k) => k.key.indexOf("extra_") === 0).map((k) => k.key);
|
|
101
|
+
|
|
102
|
+
// W8 (v0.51.0) — THE SETUP GATE. `connected` is the CLI's answer (it computes
|
|
103
|
+
// it once, and the config gate and the doctor finding read the same one), so
|
|
104
|
+
// there is no second idea here of what "has anything ever answered" means.
|
|
105
|
+
const connected = !!(d.list && d.list.gate && d.list.gate.connected);
|
|
106
|
+
const out = frag();
|
|
107
|
+
|
|
108
|
+
// THE HEADER STRIP AND "WHAT NEEDS YOUR ATTENTION" ARE ON EVERY TAB. The
|
|
109
|
+
// strip is the Knowledge precedent (v0.49.1), and the findings card is the
|
|
110
|
+
// one card that must not be behind a tab: a caution you have to go looking
|
|
111
|
+
// for is a caution nobody reads. Everything else is tabbed.
|
|
112
|
+
out.append(exStrip(d));
|
|
113
|
+
out.append(exFindingsCard(d));
|
|
114
|
+
|
|
115
|
+
const tabs = el("div", "tabs");
|
|
116
|
+
// `stack` as well as `tab-pane`: every tab holds several cards, and the
|
|
117
|
+
// container is what spaces panel blocks.
|
|
118
|
+
const pane = el("div", "tab-pane stack");
|
|
119
|
+
// THE GATE STILL DECIDES WHAT EXISTS. With nothing connected there is no
|
|
120
|
+
// routing to draw, no limits worth setting and nothing spent — so those tabs
|
|
121
|
+
// are NOT RENDERED AS EMPTY SHELLS. Setup is the only tab there is, and its
|
|
122
|
+
// own tab is spotlighted (the Crosslink "nothing linked" rule).
|
|
123
|
+
const views = connected
|
|
124
|
+
? {
|
|
125
|
+
setup: () => exSetupTab(d, body),
|
|
126
|
+
routing: () => exRoutingTab(d, body, edits),
|
|
127
|
+
limits: () => exLimitsTab(d, body, edits),
|
|
128
|
+
spending: () => exSpendingTab(d),
|
|
129
|
+
recovery: () => exRecoveryTab(d, body),
|
|
130
|
+
providers: () => exProvidersTab(d),
|
|
131
|
+
}
|
|
132
|
+
: { setup: () => exSetupTab(d, body) };
|
|
133
|
+
const select = (which) => {
|
|
134
|
+
EX_TAB = which;
|
|
135
|
+
for (const b of tabs.children) b.setAttribute("aria-selected", String(b.dataset.tab === which));
|
|
136
|
+
pane.replaceChildren(views[which]());
|
|
137
|
+
};
|
|
138
|
+
// Keys are written out in full, never assembled from the tab id — a key built
|
|
139
|
+
// from a fragment is invisible to every check that looks for one.
|
|
140
|
+
for (const [which, label] of [
|
|
141
|
+
["setup", t("extra.tab.setup")],
|
|
142
|
+
["routing", t("extra.tab.routing")],
|
|
143
|
+
["limits", t("extra.tab.limits")],
|
|
144
|
+
["spending", t("extra.tab.spending")],
|
|
145
|
+
["recovery", t("extra.tab.recovery")],
|
|
146
|
+
["providers", t("extra.tab.providers")],
|
|
147
|
+
]) {
|
|
148
|
+
if (!views[which]) continue;
|
|
149
|
+
const b = el("button", null, label);
|
|
150
|
+
b.type = "button";
|
|
151
|
+
b.dataset.tab = which;
|
|
152
|
+
if (!connected && which === "setup") b.classList.add("tab-spot");
|
|
153
|
+
b.addEventListener("click", () => select(which));
|
|
154
|
+
tabs.append(b);
|
|
155
|
+
}
|
|
156
|
+
out.append(tabs, pane);
|
|
157
|
+
body.replaceChildren(out);
|
|
158
|
+
select(views[EX_TAB] ? EX_TAB : "setup");
|
|
159
|
+
// The bar sticks only while dirty, and Discard renders only while dirty —
|
|
160
|
+
// both are editBar's own rules, unchanged. It is GATED with the two cards it
|
|
161
|
+
// belongs to (v0.51.0): "Reset the guardrails" beside a panel that is not
|
|
162
|
+
// showing any guardrails is the same mistake as a Connect button on a tool
|
|
163
|
+
// that is not installed.
|
|
164
|
+
EX_BAR = connected ? exEditBar(edits, body) : null;
|
|
165
|
+
if (EX_BAR) body.append(EX_BAR);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* THE FIVE TABS (v0.53.0).
|
|
169
|
+
|
|
170
|
+
This panel was nine cards in one 8,800px scroll — no first step, no last
|
|
171
|
+
step, and no way to be DONE with a section. Knowledge (five tabs, v0.49.1)
|
|
172
|
+
and Crosslink (two, v0.43.7) are the precedent, down to the shared `.tabs` /
|
|
173
|
+
`.tab-pane` in runs.css; Extra was the biggest panel in the app and the only
|
|
174
|
+
big one that never adopted it.
|
|
175
|
+
|
|
176
|
+
The grouping is what you DO, in the order you do it: connect something,
|
|
177
|
+
decide where work goes, set the rules, read what it cost, look a provider up.
|
|
178
|
+
Routing carries the band ladder AND the lane table together because a band is
|
|
179
|
+
arithmetic and the lane is the decision — they were two cards you scrolled
|
|
180
|
+
between. */
|
|
181
|
+
function exSetupTab(d, body) {
|
|
182
|
+
const out = frag();
|
|
183
|
+
// THE BOUNDARY CARD RENDERS ALWAYS. It is the one thing a first-time reader
|
|
184
|
+
// has to see before they connect anything.
|
|
185
|
+
out.append(exBoundaryCard());
|
|
186
|
+
if (!(d.list && d.list.gate && d.list.gate.connected)) out.append(exGateNotice(d));
|
|
187
|
+
out.append(exToolsCard(d, body));
|
|
188
|
+
out.append(exProfilesCard(d, body));
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
function exRoutingTab(d, body, edits) {
|
|
192
|
+
const out = frag();
|
|
193
|
+
out.append(exRoutingCard(d, body, edits));
|
|
194
|
+
// v0.55.0 — the second ladder, directly below the bands. One tab, two halves,
|
|
195
|
+
// no seventh tab: a band and a position are two answers to the same question
|
|
196
|
+
// and reading them apart from each other is how the doc checker ended up
|
|
197
|
+
// resolving the writer's band for a release.
|
|
198
|
+
out.append(exSlotCard(d, body, edits));
|
|
199
|
+
out.append(exLanesCard(d));
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
function exLimitsTab(d, body, edits) {
|
|
203
|
+
const out = frag();
|
|
204
|
+
out.append(exGuardrailsCard(d, body, edits));
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
function exSpendingTab(d) {
|
|
208
|
+
const out = frag();
|
|
209
|
+
out.append(exCostCard(d));
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
function exProvidersTab(d) {
|
|
213
|
+
const out = frag();
|
|
214
|
+
out.append(exProvidersCard(d));
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* ── TAB 6 — RECOVERY (v0.54.0) ──────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
A failed foreign dispatch is a POSITION, not a blank page. This tab renders
|
|
221
|
+
what the position IS and hands over the two commands that act on it.
|
|
222
|
+
|
|
223
|
+
THE FREE/PAID LINE IS VISIBLE HERE, not hidden. `orc extra journal list` and
|
|
224
|
+
`orc extra reconcile` are deterministic and cost no model tokens, so both are
|
|
225
|
+
real BUTTONS. `orc extra resume-slice` composes a slice for a dispatch that
|
|
226
|
+
WILL cost money, and the dispatch itself is a lane action — so Recovery ships
|
|
227
|
+
copy-able commands and NO Run button. The panel never runs a lane; that is the
|
|
228
|
+
founding boundary, narrowed in v0.50.0 and still binding.
|
|
229
|
+
|
|
230
|
+
EVERY STATE WORD IS THE CLI'S — `resumable`, `nothing-to-resume`,
|
|
231
|
+
`no-journal`, `complete`, `in-flight`. Never a friendlier synonym and never a
|
|
232
|
+
translated one: a simplified state word is a state that does not exist.
|
|
233
|
+
|
|
234
|
+
A ROW WITH NOTHING TO SHOW KEEPS ITS SLOT. `nothing-to-resume` and `complete`
|
|
235
|
+
render as rows, not as filtered-out entries — the `used 0/20` and `TOO_RECENT`
|
|
236
|
+
rule. Filtering them makes "there was nothing to resume" and "ORC did not
|
|
237
|
+
look" identical. */
|
|
238
|
+
function exRecoveryTab(d, body) {
|
|
239
|
+
const out = frag();
|
|
240
|
+
out.append(exJournalCard(d, body));
|
|
241
|
+
out.append(exPruneCard(body));
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Which row is open, so a prune or a re-render does not close it — the Runs-row
|
|
246
|
+
// rule. One row open at a time, detail fetched on first open, EXPANDED IN PLACE:
|
|
247
|
+
// there is no detail box below the list.
|
|
248
|
+
let EX_JOURNAL_OPEN = null;
|
|
249
|
+
|
|
250
|
+
function exJournalCard(d, body) {
|
|
251
|
+
const c = card(t("extra.recovery.title"));
|
|
252
|
+
c.append(el("div", "note", t("extra.recovery.note")));
|
|
253
|
+
c.append(exWhy(t("extra.recovery.noteWhy")));
|
|
254
|
+
|
|
255
|
+
const j = d.journal;
|
|
256
|
+
if (!j) {
|
|
257
|
+
c.append(empty(t("extra.recovery.none"), t("extra.recovery.noneHint")));
|
|
258
|
+
return c;
|
|
259
|
+
}
|
|
260
|
+
const rows = j.journals || [];
|
|
261
|
+
const head = el("div", "row-actions");
|
|
262
|
+
head.append(chip(t("extra.recovery.count", { n: j.entries }), j.entries ? "info" : "idle"));
|
|
263
|
+
// The ONE state this listing exists to make visible. It renders even at zero,
|
|
264
|
+
// because an absent count and a healthy one must never look the same.
|
|
265
|
+
head.append(chip(t("extra.recovery.orphans", { n: j.orphans }), j.orphans ? "bad" : "ok"));
|
|
266
|
+
if (j.in_flight) head.append(chip(t("extra.recovery.inFlight", { n: j.in_flight }), "warn"));
|
|
267
|
+
head.append(el("span", "note", t("extra.recovery.retention", { days: j.retention_days })));
|
|
268
|
+
c.append(head);
|
|
269
|
+
|
|
270
|
+
if (!rows.length) {
|
|
271
|
+
c.append(empty(t("extra.recovery.none"), t("extra.recovery.noneHint")));
|
|
272
|
+
return c;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const list = el("div", "ex-rec-list");
|
|
276
|
+
const entries = [];
|
|
277
|
+
for (const r of rows) entries.push(exJournalRow(r, list, entries, body));
|
|
278
|
+
c.append(list);
|
|
279
|
+
return c;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function exJournalRow(r, list, entries, body) {
|
|
283
|
+
const row = el("div", "ex-rec-row");
|
|
284
|
+
const headBtn = el("button", "ex-rec-head");
|
|
285
|
+
headBtn.type = "button";
|
|
286
|
+
const pane = el("div", "ex-rec-pane");
|
|
287
|
+
const entry = { row, headBtn, pane, task: r.task_id, loaded: false };
|
|
288
|
+
|
|
289
|
+
headBtn.append(el("span", "mono ex-rec-task", r.task_id));
|
|
290
|
+
const who = el("span", "note ex-rec-who", `${r.profile || "—"}/${r.model_requested || "?"} · ${r.engine || "?"}`);
|
|
291
|
+
headBtn.append(who);
|
|
292
|
+
// The CLI computed these three; the panel repeats them.
|
|
293
|
+
const chips = el("span", "ex-rec-chips");
|
|
294
|
+
if (r.live) chips.append(chip(t("extra.recovery.chipLive"), "warn"));
|
|
295
|
+
else if (r.orphan) chips.append(chip(t("extra.recovery.chipOrphan"), "bad"));
|
|
296
|
+
else if (r.outcome) chips.append(chip(r.outcome, r.outcome === "done" ? "ok" : r.outcome === "partial" ? "warn" : "bad"));
|
|
297
|
+
if (r.attempts > 1) chips.append(chip(t("extra.recovery.chipAttempts", { n: r.attempts }), "info"));
|
|
298
|
+
// FIDELITY IS NEVER RENDERED STRONGER THAN IT IS. A `streamed-opaque` journal
|
|
299
|
+
// has no per-turn tool attribution, and a reader must be able to see that
|
|
300
|
+
// before they trust `last action`.
|
|
301
|
+
if (r.journal_fidelity) chips.append(chip(r.journal_fidelity, r.journal_fidelity === "per-turn" ? "info" : "idle"));
|
|
302
|
+
headBtn.append(chips);
|
|
303
|
+
headBtn.append(el("span", "ex-rec-caret", "▸"));
|
|
304
|
+
|
|
305
|
+
headBtn.addEventListener("click", () => {
|
|
306
|
+
const open = !row.classList.contains("open");
|
|
307
|
+
for (const e of entries) exJournalSetOpen(e, false);
|
|
308
|
+
exJournalSetOpen(entry, open, body);
|
|
309
|
+
EX_JOURNAL_OPEN = open ? r.task_id : null;
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
row.append(headBtn, pane);
|
|
313
|
+
list.append(row);
|
|
314
|
+
if (EX_JOURNAL_OPEN === r.task_id) exJournalSetOpen(entry, true, body);
|
|
315
|
+
return entry;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function exJournalSetOpen(entry, open, body) {
|
|
319
|
+
entry.row.classList.toggle("open", open);
|
|
320
|
+
entry.headBtn.setAttribute("aria-expanded", String(open));
|
|
321
|
+
if (!open || entry.loaded) return;
|
|
322
|
+
entry.loaded = true;
|
|
323
|
+
entry.pane.replaceChildren(skeleton(2));
|
|
324
|
+
read("/api/extra/reconcile?task=" + encodeURIComponent(entry.task))
|
|
325
|
+
.then((r) => entry.pane.replaceChildren(exReconcileView(r.data, entry.task, body)))
|
|
326
|
+
.catch((e) => entry.pane.replaceChildren(failBox(e)));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* THE POSITION, rendered. Every line here is a field of `orc extra reconcile
|
|
330
|
+
--json`; the panel computes nothing — not the state, not the line counts, not
|
|
331
|
+
the verdict, and above all not the prose beside the verdict. Writing
|
|
332
|
+
"probably your wifi" next to `network` would be the panel deciding what a
|
|
333
|
+
verdict means, which is the Flow-stepper rule. */
|
|
334
|
+
function exReconcileView(v, task, body) {
|
|
335
|
+
const out = el("div", "stack stack-sm");
|
|
336
|
+
if (!v) return failBox(new Error(t("extra.recovery.readFailed")));
|
|
337
|
+
|
|
338
|
+
// The state word, verbatim, with the CLI's own sentence under it.
|
|
339
|
+
const st = el("div", "row-actions");
|
|
340
|
+
st.append(chip(v.state, v.state === "resumable" ? "ok" : v.state === "complete" ? "info" : v.state === "in-flight" ? "bad" : "idle"));
|
|
341
|
+
if (v.attempt) st.append(el("span", "note", t("extra.recovery.attempt", { n: v.attempt, of: v.attempts_total })));
|
|
342
|
+
if (v.reason) st.append(el("span", "mono note", v.reason));
|
|
343
|
+
out.append(st);
|
|
344
|
+
|
|
345
|
+
if (!v.ok) {
|
|
346
|
+
out.append(el("div", "note", v.error || ""));
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!v.reported_back) out.append(el("div", "note bad", t("extra.recovery.neverReported")));
|
|
351
|
+
|
|
352
|
+
// WHOSE FAULT IT WAS, in the CLI's words, with its evidence.
|
|
353
|
+
if (v.attribution && v.attribution.verdict) {
|
|
354
|
+
const a = el("div", "ex-rec-attr");
|
|
355
|
+
const h = el("div", "row-actions");
|
|
356
|
+
h.append(chip(v.attribution.verdict, v.attribution.verdict === "worker" ? "warn" : v.attribution.verdict === "orc" ? "bad" : "info"));
|
|
357
|
+
h.append(el("span", "ex-probe-head", t("extra.recovery.attribution")));
|
|
358
|
+
a.append(h);
|
|
359
|
+
a.append(el("div", "note", v.attribution.why));
|
|
360
|
+
for (const e of v.attribution.evidence || []) a.append(el("div", "note mono", e));
|
|
361
|
+
// THE FIELD THAT CHANGES THE RECOVERY.
|
|
362
|
+
if (v.attribution.fallback_would_also_fail) a.append(el("div", "note bad", t("extra.recovery.holdWave")));
|
|
363
|
+
out.append(a);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// THE PER-FILE POSITION. An untouched file keeps its row: it is the strongest
|
|
367
|
+
// signal there is, and hiding it would make "unchanged" and "not looked at"
|
|
368
|
+
// the same picture.
|
|
369
|
+
if ((v.files || []).length) {
|
|
370
|
+
const box = el("div", "ex-rec-files");
|
|
371
|
+
box.append(el("div", "ex-probe-head", t("extra.recovery.position")));
|
|
372
|
+
for (const f of v.files) {
|
|
373
|
+
const line = el("div", "row-actions ex-rec-file");
|
|
374
|
+
line.append(el("span", "mono", f.path));
|
|
375
|
+
line.append(chip(f.state, f.state === "created" || f.state === "modified" ? "warn" : f.state === "reverted" || f.state === "deleted" ? "bad" : "idle"));
|
|
376
|
+
// UNKNOWN IS NOT ZERO. A line count that would mix two people's changes
|
|
377
|
+
// reads as an em dash and never as `+0 −0`.
|
|
378
|
+
line.append(
|
|
379
|
+
el("span", "note mono", f.numstat.added === null ? "—" : `+${f.numstat.added} −${f.numstat.removed}`)
|
|
380
|
+
);
|
|
381
|
+
box.append(line);
|
|
382
|
+
}
|
|
383
|
+
out.append(box);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// A FENCE BREACH, surfaced here as well as at the worktree delta — because a
|
|
387
|
+
// crashed dispatch is exactly when that check never ran.
|
|
388
|
+
for (const u of v.touched_undeclared || [])
|
|
389
|
+
out.append(el("div", "note bad", t("extra.recovery.undeclared", { path: u.path })));
|
|
390
|
+
|
|
391
|
+
if (v.last_action) out.append(el("div", "note", t("extra.recovery.lastAction", { action: v.last_action })));
|
|
392
|
+
if (v.journal_fidelity_note) out.append(el("div", "note", v.journal_fidelity_note));
|
|
393
|
+
if (v.unreadable_progress_lines)
|
|
394
|
+
out.append(el("div", "note bad", t("extra.recovery.torn", { n: v.unreadable_progress_lines })));
|
|
395
|
+
|
|
396
|
+
// A RECOVERED VECTOR IS A FLOOR AND SAYS SO. It is drawn distinctly and its
|
|
397
|
+
// note is not optional chrome.
|
|
398
|
+
if (v.partial_usage) {
|
|
399
|
+
const u = el("div", "ex-rec-floor");
|
|
400
|
+
u.append(docTokenBar(v.partial_usage));
|
|
401
|
+
u.append(el("div", "note bad", v.partial_usage_note));
|
|
402
|
+
out.append(u);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// CARRIED FORWARD, UNEVALUATED.
|
|
406
|
+
if ((v.acceptance || []).length) {
|
|
407
|
+
const acc = el("div", "ex-rec-acc");
|
|
408
|
+
acc.append(el("div", "ex-probe-head", t("extra.recovery.acceptance")));
|
|
409
|
+
for (const a of v.acceptance) acc.append(el("div", "note mono", a));
|
|
410
|
+
acc.append(el("div", "note", v.acceptance_note));
|
|
411
|
+
out.append(acc);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// A REFUSAL IS RENDERED AS A REFUSAL WITH ITS REASON, never as a disabled
|
|
415
|
+
// control with no explanation.
|
|
416
|
+
if (v.blocked_by) out.append(el("div", "note bad", t("extra.recovery.blocked", { why: v.blocked_by })));
|
|
417
|
+
|
|
418
|
+
if (v.resume_target) {
|
|
419
|
+
const rt = el("div", "row-actions");
|
|
420
|
+
rt.append(chip(v.resume_target.kind, v.resume_target.kind === "extra" || v.resume_target.kind === "claude" ? "ok" : "warn"));
|
|
421
|
+
rt.append(el("span", "ex-probe-head", t("extra.recovery.target")));
|
|
422
|
+
out.append(rt);
|
|
423
|
+
out.append(el("div", "note", v.resume_target.why));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// THE TWO COMMANDS. `reconcile` is free and is the button that opened this
|
|
427
|
+
// row; the paid half is a command you copy and run where lanes run.
|
|
428
|
+
if (v.next && v.state === "resumable" && !(v.reverted || []).length)
|
|
429
|
+
out.append(laneCommand("orc extra resume-slice " + task + " --out .orc/" + task + ".resume.json", t("extra.recovery.resumeWhy")));
|
|
430
|
+
out.append(laneCommand("orc extra reconcile " + task, t("extra.recovery.reconcileWhy")));
|
|
431
|
+
return out;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/* Preview-then-apply, and the preview NAMES EVERY DIRECTORY. A count is not
|
|
435
|
+
consent, one mutation at a time, and nothing ever runs automatically. Only a
|
|
436
|
+
journal whose EVERY attempt closed `done` 30+ days ago is ever a candidate, so
|
|
437
|
+
the record of a dispatch that never reported back can never be swept. */
|
|
438
|
+
function exPruneCard(body) {
|
|
439
|
+
const c = card(t("extra.recovery.prune.title"));
|
|
440
|
+
c.append(el("div", "note", t("extra.recovery.prune.note")));
|
|
441
|
+
const acts = el("div", "row-actions");
|
|
442
|
+
const preview = el("button", "btn btn-ghost btn-sm", t("extra.recovery.prune.preview"));
|
|
443
|
+
const apply = el("button", "btn btn-sm", t("extra.recovery.prune.apply"));
|
|
444
|
+
preview.type = "button";
|
|
445
|
+
apply.type = "button";
|
|
446
|
+
// The apply button stays disabled until a preview was fetched — the
|
|
447
|
+
// maintenance rule, and the reason the list below is not decoration.
|
|
448
|
+
apply.disabled = true;
|
|
449
|
+
const list = el("div", "stack stack-sm");
|
|
450
|
+
preview.addEventListener("click", async () => {
|
|
451
|
+
const r = await read("/api/extra/journal/prune/preview").catch((e) => ({ data: null, error: e }));
|
|
452
|
+
const d = r.data;
|
|
453
|
+
list.replaceChildren();
|
|
454
|
+
if (!d || !(d.candidates || []).length) {
|
|
455
|
+
list.append(el("div", "note ok", t("extra.recovery.prune.none")));
|
|
456
|
+
apply.disabled = true;
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
list.append(el("div", "note warn", t("extra.recovery.prune.would", { n: d.candidates.length })));
|
|
460
|
+
for (const x of d.candidates) list.append(el("div", "note mono", `${x.task_id} · ${x.dir}`));
|
|
461
|
+
// WHY EACH ONE IS KEPT is as much of the answer as why one goes.
|
|
462
|
+
for (const k of d.kept || []) list.append(el("div", "note", `${k.task_id} — ${k.why}`));
|
|
463
|
+
apply.disabled = false;
|
|
464
|
+
});
|
|
465
|
+
apply.addEventListener("click", async () => {
|
|
466
|
+
const r = await post("/api/extra/journal/prune", {});
|
|
467
|
+
toast(r.command, r.ok ? "ok" : "bad", r.output);
|
|
468
|
+
renderExtra(body);
|
|
469
|
+
});
|
|
470
|
+
acts.append(preview, apply);
|
|
471
|
+
c.append(acts, list);
|
|
472
|
+
return c;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/* THE RELIABILITY STRIP (Spending tab). A provider that drops one dispatch in
|
|
476
|
+
three is a fact that belongs where the user CHOOSES a profile, not in
|
|
477
|
+
folklore.
|
|
478
|
+
|
|
479
|
+
BELOW THE SAMPLE FLOOR THERE IS NO PERCENTAGE — the counts render and the
|
|
480
|
+
panel says `sample too small`, the identical restraint to the doctor finding:
|
|
481
|
+
a rate computed from three dispatches is noise with a percent sign on it. The
|
|
482
|
+
floor is the CLI's number, never one written here. */
|
|
483
|
+
function exReliabilityStrip(rel) {
|
|
484
|
+
const box = el("div", "ex-rel");
|
|
485
|
+
box.append(el("div", "ex-probe-head", t("extra.rel.title")));
|
|
486
|
+
if (!rel || !(rel.profiles || []).length) {
|
|
487
|
+
box.append(el("div", "note", t("extra.rel.none")));
|
|
488
|
+
return box;
|
|
489
|
+
}
|
|
490
|
+
for (const g of rel.profiles) {
|
|
491
|
+
const row = el("div", "ex-rel-row");
|
|
492
|
+
const h = el("div", "row-actions");
|
|
493
|
+
h.append(el("span", "mono ex-name", g.profile));
|
|
494
|
+
h.append(
|
|
495
|
+
el("span", "note", t("extra.rel.counts", { n: g.dispatches_total, failed: g.failed, resumed: g.resumed, orphaned: g.orphaned }))
|
|
496
|
+
);
|
|
497
|
+
// NO PERCENTAGE BELOW THE FLOOR.
|
|
498
|
+
if (g.sample_too_small) h.append(chip(t("extra.rel.tooSmall", { floor: rel.sample_floor }), "idle"));
|
|
499
|
+
else h.append(chip(t("extra.rel.rate", { pct: Math.round(g.failure_rate * 100) }), g.failure_rate > 0.3 ? "bad" : "ok"));
|
|
500
|
+
row.append(h);
|
|
501
|
+
const a = g.attribution || {};
|
|
502
|
+
// `unattributed` is ALWAYS printed, including when zero.
|
|
503
|
+
row.append(
|
|
504
|
+
el(
|
|
505
|
+
"div",
|
|
506
|
+
"note mono",
|
|
507
|
+
t("extra.rel.attribution", {
|
|
508
|
+
provider: a.provider || 0,
|
|
509
|
+
network: a.network || 0,
|
|
510
|
+
local: a.local || 0,
|
|
511
|
+
worker: a.worker || 0,
|
|
512
|
+
orc: a.orc || 0,
|
|
513
|
+
unattributed: g.unattributed || 0,
|
|
514
|
+
})
|
|
515
|
+
)
|
|
516
|
+
);
|
|
517
|
+
if (g.mean_time_to_failure_ms !== null && g.mean_time_to_failure_ms !== undefined)
|
|
518
|
+
row.append(el("div", "note", t("extra.rel.mttf", { s: Math.round(g.mean_time_to_failure_ms / 1000) })));
|
|
519
|
+
box.append(row);
|
|
520
|
+
}
|
|
521
|
+
// The two ABSENT counts, named rather than absorbed — the same rule the cost
|
|
522
|
+
// card's torn-line and undated-row warnings follow.
|
|
523
|
+
if (rel.unreadable_journals)
|
|
524
|
+
box.append(el("div", "note bad", t("extra.rel.unreadable", { n: rel.unreadable_journals })));
|
|
525
|
+
if (rel.journals_without_result)
|
|
526
|
+
box.append(el("div", "note bad", t("extra.rel.noResult", { n: rel.journals_without_result })));
|
|
527
|
+
return box;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Which tab was open, so a write that re-renders the panel does not throw you
|
|
531
|
+
// back to Setup — the KN_TAB rule.
|
|
532
|
+
let EX_TAB = "setup";
|
|
533
|
+
|
|
534
|
+
// The panel's staged writes. EVERY entry here is an ACTION — a route with a
|
|
535
|
+
// body — including the config ones, which stage as `/api/config/set` rather than
|
|
536
|
+
// through `applyEdits`. That is why one bar can carry a routing change and a
|
|
537
|
+
// guardrail change together, and why `applyActions` needed no new parameter: a
|
|
538
|
+
// panel with two Apply buttons is a panel where you forget to press one.
|
|
539
|
+
let EX_EDITS = null;
|
|
540
|
+
let EX_BAR = null;
|
|
541
|
+
|
|
542
|
+
function exEditBar(edits, body) {
|
|
543
|
+
return editBar(edits, {
|
|
544
|
+
resetLabel: t("extra.reset.label"),
|
|
545
|
+
onApply: async (btn) => {
|
|
546
|
+
await applyActions(edits, btn);
|
|
547
|
+
edits.clear();
|
|
548
|
+
renderExtra(body);
|
|
549
|
+
},
|
|
550
|
+
onReset: () => exResetModal(edits, body),
|
|
551
|
+
onCancel: () => edits.clear(),
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/* THE BOUNDARY CARD. It renders ALWAYS and it is never behind a hover or a
|
|
556
|
+
fold — the house-rules precedent. A person about to send their source code to
|
|
557
|
+
a third party should not have to go looking for the paragraph that says so. */
|
|
558
|
+
function exBoundaryCard() {
|
|
559
|
+
const c = card(t("extra.boundary.title"));
|
|
560
|
+
c.classList.add("ex-boundary");
|
|
561
|
+
for (const line of [
|
|
562
|
+
t("extra.boundary.leaves"),
|
|
563
|
+
t("extra.boundary.whom"),
|
|
564
|
+
t("extra.boundary.cannot"),
|
|
565
|
+
t("extra.boundary.fence"),
|
|
566
|
+
])
|
|
567
|
+
c.append(el("p", "ex-boundary-line", line));
|
|
568
|
+
c.append(el("div", "note", t("extra.boundary.probe")));
|
|
569
|
+
return c;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/* The header strip. Every number is the CLI's own count; one it could not
|
|
573
|
+
compute renders as an em dash, never as a zero (the Knowledge rule). */
|
|
574
|
+
function exStrip(d) {
|
|
575
|
+
const strip = el("div", "ex-strip");
|
|
576
|
+
const counts = (d.list && d.list.counts) || null;
|
|
577
|
+
const item = (label, value) => {
|
|
578
|
+
const box = el("div", "ex-strip-item");
|
|
579
|
+
box.append(el("span", "ex-strip-value", value));
|
|
580
|
+
box.append(el("span", "ex-strip-label", label));
|
|
581
|
+
strip.append(box);
|
|
582
|
+
};
|
|
583
|
+
item(t("extra.strip.profiles"), counts ? String(counts.profiles) : "—");
|
|
584
|
+
// The CLI already counts this. Recounting the array here would be a second
|
|
585
|
+
// idea of what "verified" means, in the panel whose whole gate it is.
|
|
586
|
+
item(t("extra.strip.verified"), counts ? String(counts.verified) : "—");
|
|
587
|
+
item(t("extra.strip.routed"), d.list ? String((d.list.routes || []).length) : "—");
|
|
588
|
+
item(t("extra.strip.findings"), d.doctor ? String((d.doctor.findings || []).length) : "—");
|
|
589
|
+
// The SOONEST deadline across every saved passphrase. An uncomputable value is
|
|
590
|
+
// an em dash, never a guess, and the date is the CLI's — this panel does no
|
|
591
|
+
// arithmetic on it.
|
|
592
|
+
const soon = ((d.list && d.list.profiles) || [])
|
|
593
|
+
.map((x) => x.session)
|
|
594
|
+
.filter((x) => x && x.expires_at)
|
|
595
|
+
.sort((a, b) => String(a.expires_at).localeCompare(String(b.expires_at)))[0];
|
|
596
|
+
item(t("extra.strip.passphrase"), soon ? String(soon.expires_at).slice(0, 10) : "—");
|
|
597
|
+
item(t("extra.strip.catalog"), (d.list && d.list.catalog_as_of) || "—");
|
|
598
|
+
return strip;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/* ---------------------------------------------------------------- profiles */
|
|
602
|
+
|
|
603
|
+
function exProfilesCard(d, body) {
|
|
604
|
+
// "Add a connection" is a WRITE, so it lives in the card head where every
|
|
605
|
+
// other write on this panel will live (W12's rail Apply bar sits below).
|
|
606
|
+
const add = el("button", "btn btn-sm btn-primary", t("extra.add.button"));
|
|
607
|
+
add.type = "button";
|
|
608
|
+
add.disabled = !d.providers;
|
|
609
|
+
add.addEventListener("click", () => exAddModal(d.providers, body, null, d.config));
|
|
610
|
+
const c = card(t("extra.profiles.title"), add);
|
|
611
|
+
if (d.errors.list) {
|
|
612
|
+
c.append(failBox(d.errors.list));
|
|
613
|
+
return c;
|
|
614
|
+
}
|
|
615
|
+
const rows = (d.list && d.list.profiles) || [];
|
|
616
|
+
if (!rows.length) {
|
|
617
|
+
c.append(empty(t("extra.profiles.none"), t("extra.profiles.noneHint")));
|
|
618
|
+
// A connection is added at the terminal until the connect modal lands.
|
|
619
|
+
c.append(el("div", "note", t("extra.profiles.addWhy")));
|
|
620
|
+
return c;
|
|
621
|
+
}
|
|
622
|
+
const max = exConfigValue(d.config, "extra_vault_max_attempts");
|
|
623
|
+
const byProfile = exFindingsByProfile(d.doctor);
|
|
624
|
+
for (const p of rows) c.append(exProfileRow(p, byProfile.get(p.name) || [], max, body, d.config));
|
|
625
|
+
return c;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function exProfileRow(p, findings, maxAttempts, body, cfg) {
|
|
629
|
+
const row = el("div", "ex-profile");
|
|
630
|
+
|
|
631
|
+
const top = el("div", "row-actions");
|
|
632
|
+
top.append(el("span", "mono ex-name", p.name));
|
|
633
|
+
// `provider/engine` is the CLI's own composite label everywhere else, so it
|
|
634
|
+
// is written the same way here rather than split into two friendlier words.
|
|
635
|
+
top.append(el("span", "note", p.provider + "/" + p.engine));
|
|
636
|
+
top.append(exVerifyChip(p, findings));
|
|
637
|
+
const vault = exVaultChip(p, maxAttempts);
|
|
638
|
+
if (vault) top.append(vault);
|
|
639
|
+
const sess = exSessionChip(p);
|
|
640
|
+
if (sess) top.append(sess);
|
|
641
|
+
row.append(top);
|
|
642
|
+
|
|
643
|
+
const kv = [
|
|
644
|
+
[t("extra.profile.engine"), p.engine],
|
|
645
|
+
[t("extra.profile.base"), p.anthropic_base_url || p.base_url || "—"],
|
|
646
|
+
[t("extra.profile.region"), p.region === "default" ? "—" : p.region],
|
|
647
|
+
[t("extra.profile.credential"), exCredentialLine(p)],
|
|
648
|
+
[
|
|
649
|
+
t("extra.profile.modelsLabel"),
|
|
650
|
+
p.models_seen.length ? t("extra.profile.models", { n: p.models_seen.length }) : t("extra.profile.modelsNone"),
|
|
651
|
+
],
|
|
652
|
+
];
|
|
653
|
+
row.append(kvList(kv));
|
|
654
|
+
|
|
655
|
+
// The model names themselves are DATA the provider returned. They are shown
|
|
656
|
+
// verbatim and never abbreviated into a family name — a family name is a
|
|
657
|
+
// model id that does not exist.
|
|
658
|
+
if (p.models_seen.length) {
|
|
659
|
+
const list = el("div", "ex-models");
|
|
660
|
+
for (const m of p.models_seen) list.append(el("span", "mono ex-model", m));
|
|
661
|
+
row.append(list);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (p.privacy) row.append(el("div", "note", t("extra.profile.privacy")));
|
|
665
|
+
|
|
666
|
+
// The findings that are about THIS connection, next to it — the caution and
|
|
667
|
+
// the thing it is a caution about on one screen.
|
|
668
|
+
for (const f of findings) row.append(exFindingLine(f));
|
|
669
|
+
|
|
670
|
+
const actions = el("div", "row-actions");
|
|
671
|
+
const test = el("button", "btn btn-sm", t("extra.test.button"));
|
|
672
|
+
test.type = "button";
|
|
673
|
+
test.addEventListener("click", () => exTestModal(p, body, cfg));
|
|
674
|
+
actions.append(test);
|
|
675
|
+
// The countdown is only actionable through the one command that can clear it:
|
|
676
|
+
// a correct unlock proves the passphrase and resets the counter to zero. It is
|
|
677
|
+
// offered only where there is a stored key to unlock — never on a tombstone.
|
|
678
|
+
if ((p.credential || {}).vault && p.credential.vault.state === "stored") {
|
|
679
|
+
const unlock = el("button", "btn btn-ghost btn-sm", t("extra.unlock.button"));
|
|
680
|
+
unlock.type = "button";
|
|
681
|
+
unlock.addEventListener("click", () => exUnlockModal(p, body));
|
|
682
|
+
actions.append(unlock);
|
|
683
|
+
}
|
|
684
|
+
// The passphrase's own two actions. EXTEND re-opens the picker with a new
|
|
685
|
+
// deadline measured from today — never an auto-extend on use, because a
|
|
686
|
+
// deadline that renews itself every time you use it is not a deadline.
|
|
687
|
+
if (p.session) {
|
|
688
|
+
const extend = el("button", "btn btn-ghost btn-sm", t("extra.session.extend"));
|
|
689
|
+
extend.type = "button";
|
|
690
|
+
extend.addEventListener("click", () => exSessionModal(p, body, cfg, true));
|
|
691
|
+
actions.append(extend);
|
|
692
|
+
if (p.session.state !== "ABSENT") {
|
|
693
|
+
const forget = el("button", "btn btn-ghost btn-sm", t("extra.session.forget"));
|
|
694
|
+
forget.type = "button";
|
|
695
|
+
forget.addEventListener("click", () => exSessionForgetModal(p, body));
|
|
696
|
+
actions.append(forget);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
const drop = el("button", "btn btn-ghost btn-sm", t("extra.remove.button"));
|
|
700
|
+
drop.type = "button";
|
|
701
|
+
drop.addEventListener("click", () => exRemoveModal(p, body));
|
|
702
|
+
actions.append(drop);
|
|
703
|
+
row.append(actions);
|
|
704
|
+
return row;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// THE DEADLINE, beside the vault chip. Every word and every date is the CLI's:
|
|
708
|
+
// the state is COMPUTED there on read and never stored, so this renders four
|
|
709
|
+
// answers and decides none of them. `not saved` KEEPS ITS SLOT on a vaulted
|
|
710
|
+
// connection — that is the state a run STOPS on, and a missing chip would make
|
|
711
|
+
// it look like nothing was wrong.
|
|
712
|
+
function exSessionChip(p) {
|
|
713
|
+
const sn = p.session;
|
|
714
|
+
if (!sn) return null;
|
|
715
|
+
if (sn.state === "ACTIVE") return chip(t("extra.session.active", { date: String(sn.expires_at).slice(0, 10) }), "ok");
|
|
716
|
+
if (sn.state === "EXPIRING")
|
|
717
|
+
return chip(t("extra.session.expiring", { date: String(sn.expires_at).slice(0, 10), days: sn.days_left }), "warn");
|
|
718
|
+
if (sn.state === "EXPIRED") return chip(t("extra.session.expired"), "bad");
|
|
719
|
+
return chip(t("extra.session.none"), "warn");
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// UNVERIFIED and STALE are the CLI's states, and they arrive as doctor findings
|
|
723
|
+
// rather than being recomputed here: this panel has no idea what
|
|
724
|
+
// `extra_verify_max_days` means and must not learn.
|
|
725
|
+
function exVerifyChip(p, findings) {
|
|
726
|
+
if (findings.some((f) => f.id === "extra-unverified")) return chip(t("extra.profile.never"), "warn");
|
|
727
|
+
if (!p.verified_at) return chip(t("extra.profile.never"), "warn");
|
|
728
|
+
const stale = findings.some((f) => f.id === "extra-stale-verify");
|
|
729
|
+
const when = String(p.verified_at).slice(0, 10);
|
|
730
|
+
const label =
|
|
731
|
+
t("extra.profile.verifiedAt", { when, how: p.verify_method || "—" }) +
|
|
732
|
+
(p.latency_ms ? " · " + t("extra.profile.latency", { ms: p.latency_ms }) : "");
|
|
733
|
+
return chip(label, stale ? "warn" : "ok");
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function exCredentialLine(p) {
|
|
737
|
+
const c = p.credential || {};
|
|
738
|
+
// v0.51.0 — a LOCAL TOOL may hold its own credential, and then ORC has nothing
|
|
739
|
+
// to send and nothing is missing. `found`/`not found` are both wrong, so the
|
|
740
|
+
// CLI answers `present: null` and this says what is actually true.
|
|
741
|
+
if (c.source === "tool") return t("extra.profile.credentialTool");
|
|
742
|
+
const where =
|
|
743
|
+
c.source === "vault"
|
|
744
|
+
? t("extra.profile.credentialVault")
|
|
745
|
+
: t("extra.profile.credentialEnv", { name: c.key_name || "—" });
|
|
746
|
+
return where + " · " + (c.present ? t("extra.profile.present") : t("extra.profile.missing"));
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// The vault chip carries the CLI's own state word plus the countdown when it is
|
|
750
|
+
// non-zero. A zero countdown KEEPS ITS SLOT and reads as no attempts used — a
|
|
751
|
+
// countdown toward a destructive action that is not shown is indistinguishable
|
|
752
|
+
// from no countdown at all.
|
|
753
|
+
function exVaultChip(p, maxAttempts) {
|
|
754
|
+
const v = (p.credential || {}).vault;
|
|
755
|
+
if (!v) return null;
|
|
756
|
+
const used = Number(v.attempts_used) || 0;
|
|
757
|
+
const count =
|
|
758
|
+
used > 0 && maxAttempts !== null
|
|
759
|
+
? t("extra.vault.attempts", { used, max: maxAttempts })
|
|
760
|
+
: used > 0
|
|
761
|
+
? t("extra.vault.attempts", { used, max: "—" })
|
|
762
|
+
: t("extra.vault.attemptsNone");
|
|
763
|
+
if (v.state === "wiped") return chip(t("extra.vault.wiped"), "bad");
|
|
764
|
+
if (v.state === "none") return chip(t("extra.vault.none"), "warn");
|
|
765
|
+
return chip(t("extra.vault.stored") + " · " + count, used > 0 ? "warn" : "ok");
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/* ---------------------------------------------------------------- findings */
|
|
769
|
+
|
|
770
|
+
function exFindingsByProfile(doctor) {
|
|
771
|
+
const map = new Map();
|
|
772
|
+
for (const f of (doctor && doctor.findings) || []) {
|
|
773
|
+
if (!f.profile) continue;
|
|
774
|
+
if (!map.has(f.profile)) map.set(f.profile, []);
|
|
775
|
+
map.get(f.profile).push(f);
|
|
776
|
+
}
|
|
777
|
+
return map;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function exFindingLine(f) {
|
|
781
|
+
const line = el("div", "note ex-finding");
|
|
782
|
+
// The id and the message are the CLI's words. Never softened, never rewritten.
|
|
783
|
+
line.append(chip(f.id, f.fixable === false ? "bad" : "warn"));
|
|
784
|
+
line.append(document.createTextNode(" " + f.message));
|
|
785
|
+
if (f.fixable === false) line.append(el("div", "note", t("extra.findings.notFixable")));
|
|
786
|
+
return line;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function exFindingsCard(d) {
|
|
790
|
+
const all = (d.doctor && d.doctor.findings) || [];
|
|
791
|
+
// Only what belongs to no profile lands here; the rest is rendered beside the
|
|
792
|
+
// connection it is about.
|
|
793
|
+
const loose = all.filter((f) => !f.profile);
|
|
794
|
+
const c = card(t("extra.findings.title"));
|
|
795
|
+
if (!all.length) {
|
|
796
|
+
c.append(el("div", "note", t("extra.findings.none")));
|
|
797
|
+
return c;
|
|
798
|
+
}
|
|
799
|
+
for (const f of loose) c.append(exFindingLine(f));
|
|
800
|
+
c.append(laneCommand("orc extra doctor", t("extra.findings.cmdWhy")));
|
|
801
|
+
return c;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/* --------------------------------------------------------------- providers */
|
|
805
|
+
|
|
806
|
+
function exProvidersCard(d) {
|
|
807
|
+
const c = card(t("extra.providers.title"));
|
|
808
|
+
if (d.errors.providers) {
|
|
809
|
+
c.append(failBox(d.errors.providers));
|
|
810
|
+
return c;
|
|
811
|
+
}
|
|
812
|
+
const cat = d.providers || {};
|
|
813
|
+
c.append(el("div", "note", t("extra.providers.sub")));
|
|
814
|
+
c.append(exWhy(t("extra.providers.subWhy")));
|
|
815
|
+
if (cat.stale) c.append(el("div", "banner banner-bad", t("extra.providers.stale")));
|
|
816
|
+
|
|
817
|
+
const grid = el("div", "ex-provider-grid");
|
|
818
|
+
for (const p of cat.providers || []) grid.append(exProviderTile(p));
|
|
819
|
+
c.append(grid);
|
|
820
|
+
// The one thing the catalog deliberately does NOT carry, said out loud so
|
|
821
|
+
// nobody goes looking for a model dropdown that will never exist.
|
|
822
|
+
c.append(el("div", "note", t("extra.providers.noModels")));
|
|
823
|
+
return c;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function exProviderTile(p) {
|
|
827
|
+
const tile = el("div", "ex-provider");
|
|
828
|
+
// id and label are catalog data: rendered as written, never translated.
|
|
829
|
+
tile.append(el("div", "mono ex-provider-id", p.id));
|
|
830
|
+
tile.append(el("div", "ex-provider-label", p.label));
|
|
831
|
+
const eng = el("div", "row-actions");
|
|
832
|
+
for (const e of p.engines || []) eng.append(chip(e, "info"));
|
|
833
|
+
tile.append(eng);
|
|
834
|
+
if ((p.regions || []).length) {
|
|
835
|
+
const r = el("div", "note");
|
|
836
|
+
r.append(document.createTextNode(t("extra.providers.regions") + ": " + p.regions.map((x) => x.id).join(", ")));
|
|
837
|
+
tile.append(r);
|
|
838
|
+
}
|
|
839
|
+
const links = el("div", "row-actions");
|
|
840
|
+
if (p.docs_url) links.append(exLink(p.docs_url, t("extra.providers.docs")));
|
|
841
|
+
if (p.terms_url) links.append(exLink(p.terms_url, t("extra.providers.terms")));
|
|
842
|
+
if (links.childNodes.length) tile.append(links);
|
|
843
|
+
return tile;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// A catalog URL opens in a new tab with no referrer. The shell already sets a
|
|
847
|
+
// no-referrer policy; `rel` says it again at the link, because this is the one
|
|
848
|
+
// place in the panel that points off the machine.
|
|
849
|
+
function exLink(href, label) {
|
|
850
|
+
const a = el("a", "ex-link", label);
|
|
851
|
+
a.href = href;
|
|
852
|
+
a.target = "_blank";
|
|
853
|
+
a.rel = "noreferrer noopener";
|
|
854
|
+
return a;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// One config key's effective value, straight from `orc config list --json`.
|
|
858
|
+
// `null` when the read failed — which is what makes the attempt ceiling render
|
|
859
|
+
// as an em dash rather than as a number this panel made up.
|
|
860
|
+
function exConfigValue(cfg, key) {
|
|
861
|
+
const row = ((cfg && cfg.keys) || []).find((k) => k.key === key);
|
|
862
|
+
return row && row.value !== undefined && row.value !== null ? row.value : null;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// The legal values of a key, from `orc config list --json`. THE PANEL NAMES NO
|
|
866
|
+
// NUMBER ITSELF — the same rule the flow dropdowns are built under, and the
|
|
867
|
+
// same grep test enforces it.
|
|
868
|
+
function exConfigOptions(cfg, key) {
|
|
869
|
+
const row = ((cfg && cfg.keys) || []).find((k) => k.key === key);
|
|
870
|
+
return (row && Array.isArray(row.options) ? row.options : []).map(String);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/* ------------------------------------------- the passphrase and its deadline
|
|
874
|
+
|
|
875
|
+
A passphrase stored on the same machine as the vault it opens is NOT A SECOND
|
|
876
|
+
FACTOR ANY MORE. It is a DEADLINE. Every surface that shows the countdown
|
|
877
|
+
repeats the CLI's own sentence saying so, because a promise the product does
|
|
878
|
+
not keep is worse than a feature it does not have.
|
|
879
|
+
|
|
880
|
+
At CONNECT TIME this modal is NOT DISMISSIBLE. It has exactly one other
|
|
881
|
+
button, and it is destructive and named — "do not save, disconnect this
|
|
882
|
+
connection" — because a modal with genuinely no way out is a trap the first
|
|
883
|
+
time a write fails, and an escape that DESTROYS the thing being configured
|
|
884
|
+
cannot be pressed by accident and leaves no half-configured state behind. */
|
|
885
|
+
function exSessionModal(p, body, cfg, dismissible, pastedKey) {
|
|
886
|
+
const form = el("div", "stack stack-sm");
|
|
887
|
+
form.append(el("div", "note", p.name + " " + "·" + " " + p.provider + "/" + p.engine));
|
|
888
|
+
|
|
889
|
+
const pass = exInput(t("extra.save.passPh"), "password");
|
|
890
|
+
const again = exInput(t("extra.save.againPh"), "password");
|
|
891
|
+
form.append(exField(t("extra.save.pass"), pass, t("extra.save.passHint")));
|
|
892
|
+
form.append(exField(t("extra.save.again"), again));
|
|
893
|
+
|
|
894
|
+
const opts = exConfigOptions(cfg, "extra_passphrase_ttl_days");
|
|
895
|
+
const current = String(exConfigValue(cfg, "extra_passphrase_ttl_days") || "");
|
|
896
|
+
const ttl = exSelect(opts.map((v) => ({ value: v, label: t("extra.session.days", { n: v }) })));
|
|
897
|
+
if (opts.indexOf(current) !== -1) ttl.value = current;
|
|
898
|
+
form.append(exField(t("extra.session.keepFor"), ttl, t("extra.session.keepForHint")));
|
|
899
|
+
|
|
900
|
+
// THE DEADLINE AS A DATE, live under the picker. "30 days" is not something a
|
|
901
|
+
// person can plan around; a date is.
|
|
902
|
+
const when = el("div", "note");
|
|
903
|
+
const paint = () => {
|
|
904
|
+
const d = new Date(Date.now() + Number(ttl.value || 0) * 86400000);
|
|
905
|
+
when.textContent = t("extra.session.until", { date: d.toISOString().slice(0, 10) });
|
|
906
|
+
};
|
|
907
|
+
ttl.addEventListener("change", paint);
|
|
908
|
+
paint();
|
|
909
|
+
form.append(when);
|
|
910
|
+
|
|
911
|
+
form.append(el("div", "banner", t("extra.session.honesty")));
|
|
912
|
+
const out = el("div", "note");
|
|
913
|
+
form.append(out);
|
|
914
|
+
|
|
915
|
+
const save = async (close) => {
|
|
916
|
+
out.textContent = "";
|
|
917
|
+
if (!pass.value || pass.value !== again.value) {
|
|
918
|
+
out.textContent = t("extra.save.mismatch");
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
setBusy(true);
|
|
922
|
+
try {
|
|
923
|
+
// TWO CALLS, because storing the KEY and caching the PASSPHRASE are two
|
|
924
|
+
// different acts and only one of them has a deadline.
|
|
925
|
+
if (pastedKey) {
|
|
926
|
+
const r = await post("/api/extra/ping", { profile: p.name, key: pastedKey, passphrase: pass.value });
|
|
927
|
+
const d = (r && r.data) || null;
|
|
928
|
+
if (!d || !d.vault || !d.vault.stored) {
|
|
929
|
+
out.textContent = (d && d.vault && d.vault.error) || (r && r.error) || t("common.loadFail");
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
const r2 = await post("/api/extra/session/save", { profile: p.name, ttl_days: ttl.value, passphrase: pass.value });
|
|
934
|
+
const d2 = (r2 && r2.data) || null;
|
|
935
|
+
if (!d2 || !d2.ok) {
|
|
936
|
+
out.textContent = (d2 && d2.error) || (r2 && r2.error) || t("common.loadFail");
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
close();
|
|
940
|
+
exRefresh(body);
|
|
941
|
+
} catch (e) {
|
|
942
|
+
out.textContent = String(e.message);
|
|
943
|
+
} finally {
|
|
944
|
+
setBusy(false);
|
|
945
|
+
pass.value = "";
|
|
946
|
+
again.value = "";
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
const actions = [];
|
|
951
|
+
if (dismissible) actions.push({ label: t("common.cancel"), onClick: (c) => c() });
|
|
952
|
+
else
|
|
953
|
+
actions.push({
|
|
954
|
+
// NOT a cancel. It deletes the key and the connection just made, and it
|
|
955
|
+
// says so in the label.
|
|
956
|
+
label: t("extra.session.abandon"),
|
|
957
|
+
cls: "btn-ghost",
|
|
958
|
+
onClick: async (c) => {
|
|
959
|
+
setBusy(true);
|
|
960
|
+
try {
|
|
961
|
+
await post("/api/extra/remove", { name: p.name, reason: "the passphrase was not saved" });
|
|
962
|
+
} catch (_) {
|
|
963
|
+
} finally {
|
|
964
|
+
setBusy(false);
|
|
965
|
+
}
|
|
966
|
+
c();
|
|
967
|
+
exRefresh(body);
|
|
968
|
+
},
|
|
969
|
+
});
|
|
970
|
+
actions.push({ label: t("extra.session.save"), cls: "btn-primary", onClick: (c) => save(c) });
|
|
971
|
+
|
|
972
|
+
modal({ title: t("extra.session.title"), body: form, actions, dismissible: dismissible !== false ? true : false });
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function exSessionForgetModal(p, body) {
|
|
976
|
+
const form = el("div", "stack stack-sm");
|
|
977
|
+
form.append(el("div", "note", t("extra.session.forgetWhat", { name: p.name })));
|
|
978
|
+
modal({
|
|
979
|
+
title: t("extra.session.forget"),
|
|
980
|
+
body: form,
|
|
981
|
+
actions: [
|
|
982
|
+
{ label: t("common.cancel"), onClick: (c) => c() },
|
|
983
|
+
{
|
|
984
|
+
label: t("extra.session.forget"),
|
|
985
|
+
cls: "btn-danger",
|
|
986
|
+
onClick: async (c) => {
|
|
987
|
+
c();
|
|
988
|
+
setBusy(true);
|
|
989
|
+
try {
|
|
990
|
+
await post("/api/extra/session/forget", { profile: p.name });
|
|
991
|
+
} finally {
|
|
992
|
+
setBusy(false);
|
|
993
|
+
exRefresh(body);
|
|
994
|
+
}
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
],
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/* ==================================== the connect modal and the wire (W11) ==
|
|
1002
|
+
THE PANEL STILL NEVER TALKS TO A MODEL. Testing a connection POSTs to a route
|
|
1003
|
+
that subprocesses `orc extra ping <profile> --json` — the same command you
|
|
1004
|
+
would type — so the HTTPS call is the CLI's and every validator, redaction and
|
|
1005
|
+
exit code comes for free. That is the whole of what v0.50.0 narrowed in the
|
|
1006
|
+
`orc ui` boundary: a probe is a DIAGNOSTIC, the family `orc doctor` is in.
|
|
1007
|
+
|
|
1008
|
+
THE LIFECYCLE IS THE CLI'S: test first, then store. A pasted key is held for
|
|
1009
|
+
the probe and NEVER written before the test is green, and a failed test on a
|
|
1010
|
+
never-verified profile removes the profile the CLI just wrote — so a typo'd
|
|
1011
|
+
key cannot rot in a vault nobody can open. The panel reimplements none of
|
|
1012
|
+
that; it calls `add`, then `ping`, and renders what came back.
|
|
1013
|
+
|
|
1014
|
+
A PASTED KEY LEAVES THE BROWSER ONCE, in a POST body over loopback with the
|
|
1015
|
+
per-launch token, and reaches the CLI on STDIN. It is never put in a URL,
|
|
1016
|
+
never kept past the request, never written to localStorage and never
|
|
1017
|
+
re-rendered as a field value. */
|
|
1018
|
+
|
|
1019
|
+
// What the probe will do and what it costs, said BEFORE the button — the CLI
|
|
1020
|
+
// picks the rung, so this describes its ladder and never predicts the outcome.
|
|
1021
|
+
// Which rung actually answered comes back as `verify_method`, verbatim.
|
|
1022
|
+
function exProbeNote(engine) {
|
|
1023
|
+
const box = el("div", "ex-probe");
|
|
1024
|
+
box.append(el("div", "ex-probe-head", t("extra.probe.head")));
|
|
1025
|
+
// v0.51.0 — a LOCAL TOOL climbs a different ladder from an endpoint, and its
|
|
1026
|
+
// paid rung is not a cheap ping: the tool loads its own system prompt and tool
|
|
1027
|
+
// schemas before it sends anything, which is thousands of input tokens against
|
|
1028
|
+
// an endpoint probe's ten. The two are therefore quoted separately, and both
|
|
1029
|
+
// BEFORE the button.
|
|
1030
|
+
if (engine === "cli") {
|
|
1031
|
+
box.append(el("div", "note", t("extra.probe.cliFree")));
|
|
1032
|
+
box.append(el("div", "note", t("extra.probe.cliPaid")));
|
|
1033
|
+
return box;
|
|
1034
|
+
}
|
|
1035
|
+
box.append(el("div", "note", t("extra.probe.free")));
|
|
1036
|
+
box.append(el("div", "note", t("extra.probe.paid")));
|
|
1037
|
+
return box;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/* THE WIRE. A pulse travels from the credential to the provider and lands green
|
|
1041
|
+
or red — the feedback that makes an invisible network call feel like an event.
|
|
1042
|
+
The dot's animation is INFINITE while the probe is in flight, which is why
|
|
1043
|
+
04-motion.css REMOVES it under reduced motion rather than capping it: a capped
|
|
1044
|
+
infinite animation freezes mid-cycle. */
|
|
1045
|
+
function exWire(fromLabel, toLabel) {
|
|
1046
|
+
const wrap = el("div", "ex-wire");
|
|
1047
|
+
wrap.append(el("span", "ex-wire-end", fromLabel));
|
|
1048
|
+
const line = el("span", "ex-wire-line");
|
|
1049
|
+
line.append(el("span", "ex-wire-dot"));
|
|
1050
|
+
wrap.append(line);
|
|
1051
|
+
wrap.append(el("span", "ex-wire-end", toLabel));
|
|
1052
|
+
wrap.arm = () => {
|
|
1053
|
+
wrap.classList.remove("ex-wire-ok", "ex-wire-bad");
|
|
1054
|
+
wrap.classList.add("ex-wire-live");
|
|
1055
|
+
};
|
|
1056
|
+
wrap.settle = (ok) => {
|
|
1057
|
+
wrap.classList.remove("ex-wire-live");
|
|
1058
|
+
wrap.classList.add(ok ? "ex-wire-ok" : "ex-wire-bad");
|
|
1059
|
+
};
|
|
1060
|
+
return wrap;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// One form row, in the shape crosslink's add form already uses.
|
|
1064
|
+
function exField(labelText, node, hint) {
|
|
1065
|
+
// `ex-form-hide` is not decoration: `.field` is a flex box, and an author
|
|
1066
|
+
// `display` beats the UA stylesheet's `[hidden]` rule — so a field this form
|
|
1067
|
+
// hides needs a rule that names it. See css/panels/extra.css.
|
|
1068
|
+
const f = el("label", "field ex-form-hide");
|
|
1069
|
+
f.append(el("span", "field-label", labelText));
|
|
1070
|
+
f.append(node);
|
|
1071
|
+
if (hint) f.append(el("span", "field-hint", hint));
|
|
1072
|
+
return f;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function exInput(placeholder, type) {
|
|
1076
|
+
const i = el("input", "text-input");
|
|
1077
|
+
i.type = type || "text";
|
|
1078
|
+
if (type === "password") {
|
|
1079
|
+
// Never remembered, never autofilled, never re-rendered with a value.
|
|
1080
|
+
i.autocomplete = "off";
|
|
1081
|
+
i.spellcheck = false;
|
|
1082
|
+
}
|
|
1083
|
+
if (placeholder) i.placeholder = placeholder;
|
|
1084
|
+
return i;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function exSelect(options, onChange) {
|
|
1088
|
+
const sel = el("select", "text-input");
|
|
1089
|
+
for (const o of options) {
|
|
1090
|
+
const opt = el("option", null, o.label);
|
|
1091
|
+
opt.value = o.value;
|
|
1092
|
+
// A value outside its own set leads the list, is labelled, and is DISABLED:
|
|
1093
|
+
// the state must be visible, never re-offerable (the v0.44.0 rule).
|
|
1094
|
+
if (o.disabled) opt.disabled = true;
|
|
1095
|
+
sel.append(opt);
|
|
1096
|
+
}
|
|
1097
|
+
if (onChange) sel.addEventListener("change", onChange);
|
|
1098
|
+
return sel;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/* ================================ the native-tools card, and the setup gate ==
|
|
1102
|
+
W6/W8 (v0.51.0). Some providers are a LOCAL TOOL rather than an endpoint, and
|
|
1103
|
+
a local tool can simply not be installed. When it is absent this card is the
|
|
1104
|
+
FIRST thing on the panel and the ONLY thing it offers — no Connect box, no
|
|
1105
|
+
test button, no model list, because all three are buttons that cannot succeed.
|
|
1106
|
+
|
|
1107
|
+
THE PANEL SWITCHES ON `state` AND DERIVES NOTHING. Four states arrive from
|
|
1108
|
+
`orc extra tools --json`, each with exactly one next action, and every label,
|
|
1109
|
+
command, version, URL and alternative in this card came out of that JSON. The
|
|
1110
|
+
card names no tool: it cannot, and a test asserts it cannot.
|
|
1111
|
+
|
|
1112
|
+
THE INSTALL OPENS THE USER'S OWN TERMINAL. Not a background job (a hidden
|
|
1113
|
+
subprocess makes an elevation prompt, a permissions error, an 80 MB download
|
|
1114
|
+
and a forty-second wait all look identical: nothing happened) and not merely a
|
|
1115
|
+
string to copy. The exact command renders ABOVE the button — preview-then-
|
|
1116
|
+
apply, unchanged — the window is theirs to read and Ctrl-C, and ORC never
|
|
1117
|
+
elevates. A machine with no terminal to open degrades to the command, never to
|
|
1118
|
+
a dead button. */
|
|
1119
|
+
|
|
1120
|
+
function exToolsCard(d, body) {
|
|
1121
|
+
const c = card(t("extra.tools.title"));
|
|
1122
|
+
const tools = (d.tools && d.tools.tools) || [];
|
|
1123
|
+
if (d.errors.tools) {
|
|
1124
|
+
c.append(failBox(d.errors.tools));
|
|
1125
|
+
return c;
|
|
1126
|
+
}
|
|
1127
|
+
if (!tools.length) {
|
|
1128
|
+
c.append(el("div", "note", t("extra.tools.none")));
|
|
1129
|
+
return c;
|
|
1130
|
+
}
|
|
1131
|
+
c.append(el("div", "note", t("extra.tools.sub")));
|
|
1132
|
+
c.append(exWhy(t("extra.tools.subWhy")));
|
|
1133
|
+
const grid = el("div", "ex-tool-grid");
|
|
1134
|
+
for (const tool of tools) grid.append(exToolBox(tool, d, body));
|
|
1135
|
+
c.append(grid);
|
|
1136
|
+
// The user will install in another terminal and come back. A card that only
|
|
1137
|
+
// refreshed on a full page load would send them away thinking it failed.
|
|
1138
|
+
const re = el("button", "btn btn-sm", t("extra.tools.recheck"));
|
|
1139
|
+
re.type = "button";
|
|
1140
|
+
re.addEventListener("click", () => exRefresh(body));
|
|
1141
|
+
const foot = el("div", "row-actions");
|
|
1142
|
+
foot.append(re);
|
|
1143
|
+
foot.append(el("span", "note", t("extra.tools.recheckWhy")));
|
|
1144
|
+
c.append(foot);
|
|
1145
|
+
return c;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function exToolBox(tool, d, body) {
|
|
1149
|
+
const box = el("div", "ex-tool ex-tool-" + tool.state);
|
|
1150
|
+
const top = el("div", "row-actions");
|
|
1151
|
+
// The label and the binary name are catalog data: written as they arrived.
|
|
1152
|
+
top.append(el("span", "ex-tool-name", tool.label));
|
|
1153
|
+
top.append(el("span", "mono note", tool.bin));
|
|
1154
|
+
top.append(chip(tool.state, tool.state === "ready" ? "ok" : tool.state === "absent" ? "bad" : "warn"));
|
|
1155
|
+
box.append(top);
|
|
1156
|
+
|
|
1157
|
+
// v0.53.0 — ONE SENTENCE AND ONE CONTROL PER STATE. Four states carry four
|
|
1158
|
+
// different shapes and exactly ONE action each, and the `absent` card used to
|
|
1159
|
+
// stack five paragraphs above its button. Everything a state does not need in
|
|
1160
|
+
// order to be acted on now lives behind `exToolMore`. Nothing is removed and
|
|
1161
|
+
// nothing about the states, their colours or their next actions changes —
|
|
1162
|
+
// those are all `orc extra tools`'s answers.
|
|
1163
|
+
if (tool.state === "absent") {
|
|
1164
|
+
box.append(el("div", "note", t("extra.tools.absentWhat")));
|
|
1165
|
+
box.append(exInstallRow(tool, body));
|
|
1166
|
+
const more = exToolMore();
|
|
1167
|
+
// `null` MEANS THERE IS NONE — never that ORC forgot to look. The two cases
|
|
1168
|
+
// must not render the same, and neither may render as an empty slot.
|
|
1169
|
+
if (tool.no_install_alternative) {
|
|
1170
|
+
const alt = el("div", "note");
|
|
1171
|
+
alt.append(document.createTextNode(t("extra.tools.altYes") + " "));
|
|
1172
|
+
alt.append(el("span", "mono", tool.no_install_alternative));
|
|
1173
|
+
more.append(alt);
|
|
1174
|
+
} else {
|
|
1175
|
+
more.append(el("div", "note", t("extra.tools.altNone")));
|
|
1176
|
+
}
|
|
1177
|
+
if (tool.docs_url) more.append(exLink(tool.docs_url, t("extra.providers.docs")));
|
|
1178
|
+
box.append(more);
|
|
1179
|
+
return box;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const kv = [
|
|
1183
|
+
[t("extra.tools.version"), tool.version || t("extra.tools.versionUnknown")],
|
|
1184
|
+
[t("extra.tools.floor"), tool.min_version || "—"],
|
|
1185
|
+
[t("extra.tools.auth"), tool.auth_detail || (tool.authed === null ? "—" : tool.authed ? t("extra.tools.authYes") : t("extra.tools.authNo"))],
|
|
1186
|
+
[t("extra.tools.models"), tool.models_count === null ? "—" : String(tool.models_count)],
|
|
1187
|
+
];
|
|
1188
|
+
const kvDl = kvList(kv);
|
|
1189
|
+
// The auth string is the one unbounded field on this card, so it is TAGGED and
|
|
1190
|
+
// clamped in CSS — see `.ex-tool .kv dd.ex-auth-detail`. Found by its own
|
|
1191
|
+
// label rather than by an index, because kvList drops empty rows.
|
|
1192
|
+
const authDt = Array.from(kvDl.querySelectorAll("dt")).find((x) => x.textContent === t("extra.tools.auth"));
|
|
1193
|
+
if (authDt && authDt.nextElementSibling) authDt.nextElementSibling.classList.add("ex-auth-detail");
|
|
1194
|
+
// THE DIAGNOSTICS GO BEHIND THE DISCLOSURE. A version, a floor, an auth string
|
|
1195
|
+
// and a model count are what you read when something is wrong; the state chip
|
|
1196
|
+
// already told you whether it is. The PROBE ERROR stays out here — it is the
|
|
1197
|
+
// reason for the state, not detail about it.
|
|
1198
|
+
const more = exToolMore();
|
|
1199
|
+
more.append(kvDl);
|
|
1200
|
+
if (tool.bin_path) more.append(el("div", "note mono ex-tool-path", tool.bin_path));
|
|
1201
|
+
if (tool.probe_error) box.append(el("div", "note bad", tool.probe_error));
|
|
1202
|
+
|
|
1203
|
+
if (tool.state === "outdated") {
|
|
1204
|
+
box.append(el("div", "note", t("extra.tools.outdatedWhat")));
|
|
1205
|
+
box.append(exInstallRow(tool, body));
|
|
1206
|
+
box.append(more);
|
|
1207
|
+
return box;
|
|
1208
|
+
}
|
|
1209
|
+
if (tool.state === "unauthenticated") {
|
|
1210
|
+
box.append(el("div", "note", t("extra.tools.unauthWhat")));
|
|
1211
|
+
box.append(exKeyhelpRow(tool, d, body));
|
|
1212
|
+
box.append(more);
|
|
1213
|
+
return box;
|
|
1214
|
+
}
|
|
1215
|
+
// ready — and READY IS NOT THE SAME AS UNCONNECTED. `connected` / `verified`
|
|
1216
|
+
// are computed by `orc extra tools`, never joined here.
|
|
1217
|
+
const actions = el("div", "row-actions");
|
|
1218
|
+
if (tool.verified) {
|
|
1219
|
+
// A state must be VISIBLE, never re-offerable. There is NO Connect button
|
|
1220
|
+
// here, not a disabled one: an absent control and a dead control must not
|
|
1221
|
+
// look the same (the `no_install_alternative: null` rule).
|
|
1222
|
+
const named = tool.connected_profiles.filter((x) => x.verified_at).map((x) => x.name).join(", ");
|
|
1223
|
+
box.append(chip(t("extra.tools.connectedAs") + " " + named, "ok"));
|
|
1224
|
+
actions.append(exToolTestBtn(tool, d, body));
|
|
1225
|
+
} else if (tool.connected) {
|
|
1226
|
+
// Configured and never tested — the setup gate's own state, in the setup
|
|
1227
|
+
// gate's own words. The action is a test, not another connection.
|
|
1228
|
+
box.append(el("div", "note", t("extra.tools.connectedUntested")));
|
|
1229
|
+
actions.append(exToolTestBtn(tool, d, body));
|
|
1230
|
+
} else {
|
|
1231
|
+
const connect = el("button", "btn btn-sm btn-primary", t("extra.tools.connect"));
|
|
1232
|
+
connect.type = "button";
|
|
1233
|
+
connect.addEventListener("click", () => exAddModal(d.providers, body, tool, d.config));
|
|
1234
|
+
actions.append(connect);
|
|
1235
|
+
}
|
|
1236
|
+
// A second connection to the same tool with a different model map is
|
|
1237
|
+
// legitimate, so it stays available — as a SECONDARY, because it is not what
|
|
1238
|
+
// someone looking at a connected card came for.
|
|
1239
|
+
if (tool.connected) {
|
|
1240
|
+
const again = el("button", "btn btn-sm", t("extra.tools.addAnother"));
|
|
1241
|
+
again.type = "button";
|
|
1242
|
+
again.addEventListener("click", () => exAddModal(d.providers, body, tool, d.config));
|
|
1243
|
+
actions.append(again);
|
|
1244
|
+
}
|
|
1245
|
+
box.append(actions);
|
|
1246
|
+
box.append(more);
|
|
1247
|
+
return box;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/* THE "WHY" DISCLOSURE (v0.53.0).
|
|
1251
|
+
|
|
1252
|
+
The copy problem on this panel was never that it was untranslated — both
|
|
1253
|
+
tables are complete. It was that DESIGN RATIONALE WAS BEING SERVED AS USER
|
|
1254
|
+
INSTRUCTION. "A gap is not a hole … so 'I left the hardest work on Claude on
|
|
1255
|
+
purpose' and 'there is no top band' can never look the same" is true, and it
|
|
1256
|
+
belongs in knowledge.md; as the first thing under a table it tells a
|
|
1257
|
+
first-time reader nothing about what to do.
|
|
1258
|
+
|
|
1259
|
+
So the shape is: the INSTRUCTION first, in Simplified Technical English (one
|
|
1260
|
+
sentence, active, twenty words at most — bin/webui/i18n/TERMS.md), and the
|
|
1261
|
+
REASONING underneath, collapsed. Nothing is deleted; the rationale keeps its
|
|
1262
|
+
own voice, which is the one thing TERMS.md protects. It is just no longer in
|
|
1263
|
+
the way. */
|
|
1264
|
+
function exWhy(text) {
|
|
1265
|
+
const d = document.createElement("details");
|
|
1266
|
+
d.className = "ex-more ex-why";
|
|
1267
|
+
const s = document.createElement("summary");
|
|
1268
|
+
s.textContent = t("extra.why");
|
|
1269
|
+
d.append(s, el("div", "note", text));
|
|
1270
|
+
return d;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/* THE DISCLOSURE. A native <details>, so the open/closed state, the keyboard
|
|
1274
|
+
handling and the toggle are the browser's rather than a fourth hand-rolled
|
|
1275
|
+
expander. It is CLOSED by default because a card whose whole content is
|
|
1276
|
+
visible has no first thing to look at — and it holds only detail: a state
|
|
1277
|
+
never hides the control that acts on it. */
|
|
1278
|
+
function exToolMore() {
|
|
1279
|
+
const d = document.createElement("details");
|
|
1280
|
+
d.className = "ex-more";
|
|
1281
|
+
const s = document.createElement("summary");
|
|
1282
|
+
s.textContent = t("extra.tools.more");
|
|
1283
|
+
d.append(s);
|
|
1284
|
+
return d;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// The one action that can still tell you something about a connected tool. The
|
|
1288
|
+
// profile it opens on is the CLI's — the first one this tool's row names.
|
|
1289
|
+
function exToolTestBtn(tool, d, body) {
|
|
1290
|
+
const btn = el("button", "btn btn-sm btn-primary", t("extra.tools.test"));
|
|
1291
|
+
btn.type = "button";
|
|
1292
|
+
btn.addEventListener("click", () => {
|
|
1293
|
+
const first = tool.connected_profiles[0];
|
|
1294
|
+
const full = ((d.list && d.list.profiles) || []).find((x) => x.name === first.name);
|
|
1295
|
+
if (full) exTestModal(full, body, d.config);
|
|
1296
|
+
});
|
|
1297
|
+
return btn;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// PREVIEW THEN APPLY, unchanged: the exact command is visible ABOVE the button,
|
|
1301
|
+
// and the button is what runs it in a terminal the user can watch.
|
|
1302
|
+
function exInstallRow(tool, body) {
|
|
1303
|
+
const wrap = el("div", "ex-install");
|
|
1304
|
+
const inst = tool.install || {};
|
|
1305
|
+
const cmds = (inst.cmds || []).length ? inst.cmds : inst.all_cmds || [];
|
|
1306
|
+
if (!cmds.length) {
|
|
1307
|
+
wrap.append(el("div", "note", t("extra.tools.noCommand")));
|
|
1308
|
+
if (inst.docs_url) wrap.append(exLink(inst.docs_url, t("extra.providers.docs")));
|
|
1309
|
+
return wrap;
|
|
1310
|
+
}
|
|
1311
|
+
// The CLI already filtered these to this platform, so the panel picks nothing.
|
|
1312
|
+
const pick = cmds.length > 1 ? exSelect(cmds.map((x) => ({ value: x.manager, label: x.manager }))) : null;
|
|
1313
|
+
const cmdLine = el("div", "action-cmd", cmds[0].cmd);
|
|
1314
|
+
const setCmd = () => {
|
|
1315
|
+
const chosen = cmds.find((x) => x.manager === (pick ? pick.value : cmds[0].manager)) || cmds[0];
|
|
1316
|
+
cmdLine.textContent = chosen.cmd;
|
|
1317
|
+
};
|
|
1318
|
+
if (pick) pick.addEventListener("change", setCmd);
|
|
1319
|
+
wrap.append(el("div", "note", t("extra.tools.willRun")));
|
|
1320
|
+
if (pick) wrap.append(pick);
|
|
1321
|
+
wrap.append(cmdLine);
|
|
1322
|
+
const out = el("div", "note");
|
|
1323
|
+
const btn = el("button", "btn btn-sm btn-primary", t("extra.tools.install"));
|
|
1324
|
+
btn.type = "button";
|
|
1325
|
+
btn.addEventListener("click", async () => {
|
|
1326
|
+
btn.disabled = true;
|
|
1327
|
+
out.textContent = "";
|
|
1328
|
+
try {
|
|
1329
|
+
const r = await post("/api/extra/install", {
|
|
1330
|
+
provider: tool.provider,
|
|
1331
|
+
manager: pick ? pick.value : cmds[0].manager,
|
|
1332
|
+
});
|
|
1333
|
+
const j = (r && r.data) || null;
|
|
1334
|
+
if (!j) {
|
|
1335
|
+
out.textContent = (r && r.error) || t("common.loadFail");
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
// `launched: false` is an ANSWER, not an error — the command stays on
|
|
1339
|
+
// screen and the user runs it themselves.
|
|
1340
|
+
out.textContent = j.note;
|
|
1341
|
+
wrap.append(el("div", "note", t("extra.tools.neverElevates")));
|
|
1342
|
+
} catch (e) {
|
|
1343
|
+
out.textContent = String(e.message);
|
|
1344
|
+
} finally {
|
|
1345
|
+
btn.disabled = false;
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
wrap.append(btn, out);
|
|
1349
|
+
return wrap;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// Whatever `orc extra keyhelp` returned, rendered. The panel does not decide
|
|
1353
|
+
// which of the three routes applies and never pipes a key itself.
|
|
1354
|
+
function exKeyhelpRow(tool, d, body) {
|
|
1355
|
+
const wrap = el("div", "ex-keyhelp");
|
|
1356
|
+
wrap.append(el("div", "note", t("extra.tools.keyhelpSub")));
|
|
1357
|
+
const profiles = ((d.list && d.list.profiles) || []).filter((p) => p.provider === tool.provider);
|
|
1358
|
+
if (!profiles.length) {
|
|
1359
|
+
// The credential route is a property of a PROFILE (which variable, which
|
|
1360
|
+
// key), so there is nothing to compute until one exists.
|
|
1361
|
+
wrap.append(el("div", "note", t("extra.tools.keyhelpNoProfile")));
|
|
1362
|
+
const connect = el("button", "btn btn-sm", t("extra.tools.connect"));
|
|
1363
|
+
connect.type = "button";
|
|
1364
|
+
connect.addEventListener("click", () => exAddModal(d.providers, body, tool, d.config));
|
|
1365
|
+
wrap.append(connect);
|
|
1366
|
+
return wrap;
|
|
1367
|
+
}
|
|
1368
|
+
const out = el("div", "stack stack-sm");
|
|
1369
|
+
wrap.append(out);
|
|
1370
|
+
read("/api/extra/keyhelp?profile=" + encodeURIComponent(profiles[0].name))
|
|
1371
|
+
.then((r) => {
|
|
1372
|
+
const k = r.data;
|
|
1373
|
+
if (!k) return;
|
|
1374
|
+
out.append(chip(k.route, k.route === "env" ? "ok" : "warn"));
|
|
1375
|
+
out.append(el("div", "note", k.why));
|
|
1376
|
+
if (k.env_var) out.append(el("div", "note mono", k.env_var));
|
|
1377
|
+
if (k.note) out.append(el("div", "note", k.note));
|
|
1378
|
+
if (k.cmd) {
|
|
1379
|
+
out.append(el("div", "action-cmd", k.cmd));
|
|
1380
|
+
out.append(el("div", "note", t("extra.tools.loginManual")));
|
|
1381
|
+
}
|
|
1382
|
+
// v0.52.0 (D3 Part 2) — HOW to set the variable, per OS. Every command is
|
|
1383
|
+
// the CLI's and carries a PLACEHOLDER, never a key, so nothing here can
|
|
1384
|
+
// leak into a screenshot or a copy button. ORC does not run it: `setx`
|
|
1385
|
+
// would put the key in argv and an `export` line writes it in plaintext.
|
|
1386
|
+
exEnvSetBlock(out, k.env_set);
|
|
1387
|
+
// v0.53.3 — the route WITH A DEADLINE first, then the variable. Both
|
|
1388
|
+
// lines are the CLI's; the panel names no command of its own.
|
|
1389
|
+
if (k.vault_unlock) {
|
|
1390
|
+
out.append(el("div", "action-cmd", k.vault_unlock.cmd));
|
|
1391
|
+
out.append(el("div", "note", k.vault_unlock.why));
|
|
1392
|
+
}
|
|
1393
|
+
exEnvSetBlock(out, k.key_env);
|
|
1394
|
+
})
|
|
1395
|
+
.catch((e) => out.append(failBox(e)));
|
|
1396
|
+
return wrap;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// The per-OS instruction, rendered. The panel names no command of its own — the
|
|
1400
|
+
// two lines and the note are all computed by `orc extra keyhelp`.
|
|
1401
|
+
function exEnvSetBlock(out, block) {
|
|
1402
|
+
if (!block) return;
|
|
1403
|
+
out.append(el("div", "field-label", t("extra.keyhelp.session")));
|
|
1404
|
+
out.append(el("div", "action-cmd", block.session));
|
|
1405
|
+
out.append(el("div", "field-label", t("extra.keyhelp.persist")));
|
|
1406
|
+
out.append(el("div", "action-cmd", block.persist));
|
|
1407
|
+
out.append(el("div", "note", block.persist_note));
|
|
1408
|
+
// The honest half of the persistent form, when there is one.
|
|
1409
|
+
if (block.warning) out.append(el("div", "note bad", block.warning));
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/* THE SETUP GATE. Until one connection has answered, the routing table, the
|
|
1413
|
+
limits, the cost report and the full provider catalogue are NOT APPENDED —
|
|
1414
|
+
not hidden, not disabled: absent. Every one of them is a control for work that
|
|
1415
|
+
cannot happen yet, and a panel that offers them teaches somebody to configure
|
|
1416
|
+
a routing table that will never fire.
|
|
1417
|
+
|
|
1418
|
+
It has TWO FLOORS and the CLI says which one you are on, because the
|
|
1419
|
+
instruction differs: with nothing installed and no key the answer is an
|
|
1420
|
+
INSTALL, and with a connection that has never answered the answer is TEST IT.
|
|
1421
|
+
Someone with neither should never be shown a Connect box that cannot succeed. */
|
|
1422
|
+
function exGateNotice(d) {
|
|
1423
|
+
const gate = (d.list && d.list.gate) || null;
|
|
1424
|
+
const box = el("div", "banner ex-gate");
|
|
1425
|
+
box.append(el("div", "ex-gate-head", t("extra.gate.title")));
|
|
1426
|
+
box.append(
|
|
1427
|
+
el("div", null, gate && gate.floor === "never-tested" ? t("extra.gate.neverTested") : t("extra.gate.noConnection"))
|
|
1428
|
+
);
|
|
1429
|
+
box.append(el("div", "note", t("extra.gate.hidden")));
|
|
1430
|
+
box.append(exWhy(t("extra.gate.hiddenWhy")));
|
|
1431
|
+
// The CLI's own sentence about what to do next, in the CLI's own words.
|
|
1432
|
+
if (gate && gate.next) box.append(el("div", "action-cmd", gate.next));
|
|
1433
|
+
return box;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/* ---------------------------------------------------------- the add modal -- */
|
|
1437
|
+
|
|
1438
|
+
function exAddModal(cat, body, tool, cfg) {
|
|
1439
|
+
const providers = (cat && cat.providers) || [];
|
|
1440
|
+
const form = el("div", "stack stack-sm");
|
|
1441
|
+
|
|
1442
|
+
const name = exInput(t("extra.add.namePh"));
|
|
1443
|
+
// The provider list is the catalog's, in the catalog's order, with the
|
|
1444
|
+
// catalog's own ids and labels. The panel names none of them.
|
|
1445
|
+
const provider = exSelect(
|
|
1446
|
+
providers.map((p) => ({ value: p.id, label: p.id + " — " + p.label })),
|
|
1447
|
+
() => sync()
|
|
1448
|
+
);
|
|
1449
|
+
const engine = exSelect([]);
|
|
1450
|
+
const region = exSelect([]);
|
|
1451
|
+
const baseUrl = exInput("");
|
|
1452
|
+
const envKey = exInput("");
|
|
1453
|
+
// v0.51.0 — this placeholder used to be a hard-coded tool name, which became
|
|
1454
|
+
// a catalog id and therefore a provider this panel named. It is the selected
|
|
1455
|
+
// provider's own `cli_bin` now, or empty.
|
|
1456
|
+
const cliBin = exInput("");
|
|
1457
|
+
const cliAgent = exInput(t("extra.add.cliAgentPh"));
|
|
1458
|
+
const key = exInput(t("extra.add.keyPh"), "password");
|
|
1459
|
+
|
|
1460
|
+
// Env-var NAME first and selected by default (D3): an environment variable
|
|
1461
|
+
// your OS already protects beats a passphrase you will forget. Pasting a key
|
|
1462
|
+
// is the second option, and it is warned rather than hidden.
|
|
1463
|
+
const srcEnv = el("input");
|
|
1464
|
+
srcEnv.type = "radio";
|
|
1465
|
+
srcEnv.name = "ex-cred";
|
|
1466
|
+
srcEnv.checked = true;
|
|
1467
|
+
const srcVault = el("input");
|
|
1468
|
+
srcVault.type = "radio";
|
|
1469
|
+
srcVault.name = "ex-cred";
|
|
1470
|
+
// v0.52.0 — THE THIRD SOURCE, and the one that was missing. A local tool that
|
|
1471
|
+
// already signed itself in holds its own credential, so it needs no key from
|
|
1472
|
+
// ORC: no env var to set, no vault, no passphrase and no deadline. The CLI has
|
|
1473
|
+
// had `--tool-auth` since v0.51.0; the panel offered two radios, so a
|
|
1474
|
+
// signed-in tool was pushed into the vault and the vault then locked the run.
|
|
1475
|
+
// Offered only where it can be true: engine `cli`, on a catalog row that has a
|
|
1476
|
+
// binary.
|
|
1477
|
+
const srcTool = el("input");
|
|
1478
|
+
srcTool.type = "radio";
|
|
1479
|
+
srcTool.name = "ex-cred";
|
|
1480
|
+
const srcRow = el("div", "ex-cred-choice");
|
|
1481
|
+
const lblEnv = el("label", "ex-cred-opt");
|
|
1482
|
+
lblEnv.append(srcEnv, el("span", null, t("extra.add.sourceEnv")));
|
|
1483
|
+
const lblVault = el("label", "ex-cred-opt");
|
|
1484
|
+
lblVault.append(srcVault, el("span", null, t("extra.add.sourceVault")));
|
|
1485
|
+
const lblTool = el("label", "ex-cred-opt");
|
|
1486
|
+
lblTool.append(srcTool, el("span", null, t("extra.add.sourceTool")));
|
|
1487
|
+
srcRow.append(lblEnv, lblVault, lblTool);
|
|
1488
|
+
srcEnv.addEventListener("change", () => sync());
|
|
1489
|
+
srcVault.addEventListener("change", () => sync());
|
|
1490
|
+
srcTool.addEventListener("change", () => sync());
|
|
1491
|
+
|
|
1492
|
+
const fName = exField(t("extra.add.name"), name, t("extra.add.nameHint"));
|
|
1493
|
+
const fProvider = exField(t("extra.add.provider"), provider);
|
|
1494
|
+
const fEngine = exField(t("extra.add.engine"), engine, t("extra.add.engineHint"));
|
|
1495
|
+
const fRegion = exField(t("extra.add.region"), region);
|
|
1496
|
+
const fBase = exField(t("extra.add.base"), baseUrl, t("extra.add.baseHint"));
|
|
1497
|
+
const fCliBin = exField(t("extra.add.cliBin"), cliBin, t("extra.add.cliBinHint"));
|
|
1498
|
+
const fCliAgent = exField(t("extra.add.cliAgent"), cliAgent);
|
|
1499
|
+
const fEnvKey = exField(t("extra.add.envKey"), envKey, t("extra.add.envKeyHint"));
|
|
1500
|
+
const fKey = exField(t("extra.add.key"), key, t("extra.add.keyHint"));
|
|
1501
|
+
|
|
1502
|
+
const grid = el("div", "linkform");
|
|
1503
|
+
grid.append(fName, fProvider, fEngine, fRegion, fBase, fCliBin, fCliAgent);
|
|
1504
|
+
form.append(grid);
|
|
1505
|
+
form.append(el("div", "field-label", t("extra.add.credential")));
|
|
1506
|
+
form.append(srcRow);
|
|
1507
|
+
const credGrid = el("div", "linkform");
|
|
1508
|
+
credGrid.append(fEnvKey, fKey);
|
|
1509
|
+
form.append(credGrid);
|
|
1510
|
+
const vaultWarn = el("div", "banner banner-bad ex-form-hide", t("extra.add.vaultWarn"));
|
|
1511
|
+
form.append(vaultWarn);
|
|
1512
|
+
const toolNote = el("div", "banner ex-form-hide", t("extra.add.sourceToolHint"));
|
|
1513
|
+
form.append(toolNote);
|
|
1514
|
+
// The rung ladder differs per engine and so does what it COSTS, so the note is
|
|
1515
|
+
// repainted whenever the engine changes rather than fixed at the top of a form
|
|
1516
|
+
// whose engine the user has not picked yet.
|
|
1517
|
+
const probeNote = el("div");
|
|
1518
|
+
form.append(probeNote);
|
|
1519
|
+
|
|
1520
|
+
const wire = exWire(t("extra.wire.you"), t("extra.wire.provider"));
|
|
1521
|
+
form.append(wire);
|
|
1522
|
+
const result = el("div", "ex-result");
|
|
1523
|
+
form.append(result);
|
|
1524
|
+
|
|
1525
|
+
const row = () => providers.find((p) => p.id === provider.value) || null;
|
|
1526
|
+
|
|
1527
|
+
// Everything the form offers is derived from the catalog row: the engines this
|
|
1528
|
+
// provider ships, the regions it has, the variable it reads by default. A
|
|
1529
|
+
// field the CLI would refuse is never offered here.
|
|
1530
|
+
function sync() {
|
|
1531
|
+
const r = row();
|
|
1532
|
+
const engines = (r && r.engines) || [];
|
|
1533
|
+
const keep = engine.value;
|
|
1534
|
+
engine.replaceChildren();
|
|
1535
|
+
for (const e of engines) {
|
|
1536
|
+
const o = el("option", null, e);
|
|
1537
|
+
o.value = e;
|
|
1538
|
+
engine.append(o);
|
|
1539
|
+
}
|
|
1540
|
+
if (engines.includes(keep)) engine.value = keep;
|
|
1541
|
+
|
|
1542
|
+
const regions = [{ value: "default", label: "default" }].concat(
|
|
1543
|
+
((r && r.regions) || []).map((x) => ({ value: x.id, label: x.id + " — " + x.label }))
|
|
1544
|
+
);
|
|
1545
|
+
const keepR = region.value;
|
|
1546
|
+
region.replaceChildren();
|
|
1547
|
+
for (const x of regions) {
|
|
1548
|
+
const o = el("option", null, x.label);
|
|
1549
|
+
o.value = x.value;
|
|
1550
|
+
region.append(o);
|
|
1551
|
+
}
|
|
1552
|
+
if (regions.some((x) => x.value === keepR)) region.value = keepR;
|
|
1553
|
+
fRegion.hidden = regions.length < 2;
|
|
1554
|
+
|
|
1555
|
+
const isCli = engine.value === "cli";
|
|
1556
|
+
fCliBin.hidden = !isCli;
|
|
1557
|
+
fCliAgent.hidden = !isCli;
|
|
1558
|
+
// Engine `cli` has no endpoint at all, and `api` / `claude-shim` read
|
|
1559
|
+
// different halves of the catalog — the placeholder is whichever base the
|
|
1560
|
+
// CLI will actually use, so an empty field means "the catalog's".
|
|
1561
|
+
fBase.hidden = isCli;
|
|
1562
|
+
baseUrl.placeholder = (engine.value === "claude-shim" ? r && r.anthropic_base : r && r.api_base) || "";
|
|
1563
|
+
cliBin.placeholder = (r && r.cli_bin) || "";
|
|
1564
|
+
envKey.placeholder = (r && r.env_key_default) || "";
|
|
1565
|
+
|
|
1566
|
+
probeNote.replaceChildren(exProbeNote(engine.value));
|
|
1567
|
+
|
|
1568
|
+
// The tool source only exists where a tool does. If the engine moves away
|
|
1569
|
+
// from `cli` while it is selected, the choice falls back to the default
|
|
1570
|
+
// rather than staying checked on a hidden radio.
|
|
1571
|
+
const toolable = isCli && !!(r && r.cli_bin);
|
|
1572
|
+
lblTool.hidden = !toolable;
|
|
1573
|
+
if (!toolable && srcTool.checked) srcEnv.checked = true;
|
|
1574
|
+
|
|
1575
|
+
const vault = srcVault.checked;
|
|
1576
|
+
const toolAuth = srcTool.checked && toolable;
|
|
1577
|
+
// A tool that holds its own credential is asked for NEITHER field. Showing
|
|
1578
|
+
// an empty env box beside it would read as something still to fill in.
|
|
1579
|
+
fEnvKey.hidden = vault || toolAuth;
|
|
1580
|
+
fKey.hidden = !vault;
|
|
1581
|
+
vaultWarn.hidden = !vault;
|
|
1582
|
+
toolNote.hidden = !toolAuth;
|
|
1583
|
+
}
|
|
1584
|
+
engine.addEventListener("change", () => sync());
|
|
1585
|
+
// Connect from a READY tool box pre-selects that provider and pre-fills the
|
|
1586
|
+
// binary, so the one thing the user was looking at is the one thing the form
|
|
1587
|
+
// opens on. Both values are the CLI's — the panel names neither.
|
|
1588
|
+
if (tool && providers.some((p) => p.id === tool.provider)) {
|
|
1589
|
+
provider.value = tool.provider;
|
|
1590
|
+
sync();
|
|
1591
|
+
if (Array.from(engine.options).some((o) => o.value === "cli")) engine.value = "cli";
|
|
1592
|
+
cliBin.value = tool.bin || "";
|
|
1593
|
+
// Decision 2 — PRE-SELECT the tool source when the card the user pressed
|
|
1594
|
+
// Connect on says the tool is signed in. The form opens on the state they
|
|
1595
|
+
// were already looking at, and the hint says what the other two are for.
|
|
1596
|
+
if (tool.authed) srcTool.checked = true;
|
|
1597
|
+
}
|
|
1598
|
+
sync();
|
|
1599
|
+
|
|
1600
|
+
const run = async (btn) => {
|
|
1601
|
+
result.replaceChildren();
|
|
1602
|
+
if (!name.value.trim()) {
|
|
1603
|
+
result.append(el("div", "note bad", t("extra.add.needName")));
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
btn.disabled = true;
|
|
1607
|
+
btn.textContent = t("extra.test.running");
|
|
1608
|
+
wire.arm();
|
|
1609
|
+
setBusy(true);
|
|
1610
|
+
try {
|
|
1611
|
+
const added = await post("/api/extra/add", {
|
|
1612
|
+
name: name.value.trim(),
|
|
1613
|
+
provider: provider.value,
|
|
1614
|
+
engine: engine.value,
|
|
1615
|
+
region: region.value,
|
|
1616
|
+
base_url: engine.value === "claude-shim" ? "" : baseUrl.value.trim(),
|
|
1617
|
+
anthropic_base_url: engine.value === "claude-shim" ? baseUrl.value.trim() : "",
|
|
1618
|
+
cli_bin: engine.value === "cli" ? cliBin.value.trim() : "",
|
|
1619
|
+
cli_agent: engine.value === "cli" ? cliAgent.value.trim() : "",
|
|
1620
|
+
tool_auth: srcTool.checked,
|
|
1621
|
+
vault: srcVault.checked,
|
|
1622
|
+
env_key: srcVault.checked || srcTool.checked ? "" : envKey.value.trim(),
|
|
1623
|
+
});
|
|
1624
|
+
if (added.fixture) {
|
|
1625
|
+
wire.settle(false);
|
|
1626
|
+
result.append(el("div", "note", t("extra.fixture")));
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
if (!added.ok) {
|
|
1630
|
+
wire.settle(false);
|
|
1631
|
+
// The CLI's own refusal, in the CLI's own words. There is no second idea
|
|
1632
|
+
// of a valid provider, engine, region or credential source in this panel.
|
|
1633
|
+
result.append(exOutput(added.output || added.command));
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
const ping = await post("/api/extra/ping", {
|
|
1637
|
+
profile: name.value.trim(),
|
|
1638
|
+
key: srcVault.checked ? key.value : "",
|
|
1639
|
+
});
|
|
1640
|
+
exPingResult(result, wire, ping, name.value.trim(), key.value, body, cfg);
|
|
1641
|
+
} catch (e) {
|
|
1642
|
+
wire.settle(false);
|
|
1643
|
+
result.append(failBox(e));
|
|
1644
|
+
} finally {
|
|
1645
|
+
btn.disabled = false;
|
|
1646
|
+
btn.textContent = t("extra.test.button");
|
|
1647
|
+
setBusy(false);
|
|
1648
|
+
// The pasted key leaves the form the moment it has been sent.
|
|
1649
|
+
key.value = "";
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
|
|
1653
|
+
modal({
|
|
1654
|
+
title: t("extra.add.title"),
|
|
1655
|
+
body: form,
|
|
1656
|
+
actions: [
|
|
1657
|
+
{ label: t("common.cancel"), onClick: (c) => c() },
|
|
1658
|
+
{
|
|
1659
|
+
label: t("extra.test.button"),
|
|
1660
|
+
cls: "btn-primary",
|
|
1661
|
+
id: "ex-test-btn",
|
|
1662
|
+
onClick: () => run(document.getElementById("ex-test-btn")),
|
|
1663
|
+
},
|
|
1664
|
+
],
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
/* ------------------------------------------------- test an existing profile */
|
|
1669
|
+
|
|
1670
|
+
function exTestModal(p, body, cfg) {
|
|
1671
|
+
const form = el("div", "stack stack-sm");
|
|
1672
|
+
form.append(el("div", "note", p.name + " · " + p.provider + "/" + p.engine));
|
|
1673
|
+
form.append(exProbeNote(p.engine));
|
|
1674
|
+
|
|
1675
|
+
// A VAULTED KEY IS RE-PROBED WITH ITS PASSPHRASE (v0.50.0, W14): the CLI
|
|
1676
|
+
// decrypts the stored key into memory for the probe and nothing else. Pasting
|
|
1677
|
+
// a key here instead REPLACES it, which is the path for a key you rotated at
|
|
1678
|
+
// the provider — the two are different acts and the form says which is which.
|
|
1679
|
+
const vaulted = (p.credential || {}).source === "vault";
|
|
1680
|
+
const key = exInput(t("extra.add.keyPh"), "password");
|
|
1681
|
+
const pass = exInput(t("extra.save.passPh"), "password");
|
|
1682
|
+
if (vaulted) {
|
|
1683
|
+
form.append(el("div", "banner", t("extra.test.vaultedNote")));
|
|
1684
|
+
form.append(exField(t("extra.save.pass"), pass, t("extra.test.passHint")));
|
|
1685
|
+
form.append(exField(t("extra.test.replaceKey"), key, t("extra.test.replaceKeyHint")));
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// v0.51.0 — the PAID rung, and it is a button of its own so it can never be
|
|
1689
|
+
// pressed by accident. The model comes from the same CLI-decided control the
|
|
1690
|
+
// routing table uses, because a live test of a model you cannot name is not a
|
|
1691
|
+
// test of anything.
|
|
1692
|
+
const box = exModelBox(t("extra.routing.modelPh"));
|
|
1693
|
+
box.load(p.name);
|
|
1694
|
+
form.append(exField(t("extra.test.model"), box.node, t("extra.test.modelHint")));
|
|
1695
|
+
|
|
1696
|
+
const wire = exWire(t("extra.wire.you"), t("extra.wire.provider"));
|
|
1697
|
+
form.append(wire);
|
|
1698
|
+
const result = el("div", "ex-result");
|
|
1699
|
+
form.append(result);
|
|
1700
|
+
|
|
1701
|
+
const run = async (btn, live) => {
|
|
1702
|
+
result.replaceChildren();
|
|
1703
|
+
btn.disabled = true;
|
|
1704
|
+
const label = btn.textContent;
|
|
1705
|
+
btn.textContent = t("extra.test.running");
|
|
1706
|
+
wire.arm();
|
|
1707
|
+
setBusy(true);
|
|
1708
|
+
try {
|
|
1709
|
+
const r = await post("/api/extra/ping", {
|
|
1710
|
+
profile: p.name,
|
|
1711
|
+
key: vaulted ? key.value : "",
|
|
1712
|
+
passphrase: vaulted ? pass.value : "",
|
|
1713
|
+
live: !!live,
|
|
1714
|
+
model: live ? box.value() : "",
|
|
1715
|
+
});
|
|
1716
|
+
exPingResult(result, wire, r, p.name, vaulted ? key.value : "", body, cfg);
|
|
1717
|
+
} catch (e) {
|
|
1718
|
+
wire.settle(false);
|
|
1719
|
+
result.append(failBox(e));
|
|
1720
|
+
} finally {
|
|
1721
|
+
btn.disabled = false;
|
|
1722
|
+
// Its OWN label back. The free rung and the paid rung are different acts
|
|
1723
|
+
// and one of them costs money — they may never end up reading the same.
|
|
1724
|
+
btn.textContent = label;
|
|
1725
|
+
setBusy(false);
|
|
1726
|
+
key.value = "";
|
|
1727
|
+
pass.value = "";
|
|
1728
|
+
}
|
|
1729
|
+
};
|
|
1730
|
+
|
|
1731
|
+
modal({
|
|
1732
|
+
title: t("extra.test.title", { name: p.name }),
|
|
1733
|
+
body: form,
|
|
1734
|
+
actions: [
|
|
1735
|
+
{ label: t("common.close"), onClick: (c) => c() },
|
|
1736
|
+
{
|
|
1737
|
+
label: t("extra.test.live"),
|
|
1738
|
+
id: "ex-live-btn",
|
|
1739
|
+
onClick: () => run(document.getElementById("ex-live-btn"), true),
|
|
1740
|
+
},
|
|
1741
|
+
{
|
|
1742
|
+
label: t("extra.test.button"),
|
|
1743
|
+
cls: "btn-primary",
|
|
1744
|
+
id: "ex-test-btn",
|
|
1745
|
+
onClick: () => run(document.getElementById("ex-test-btn"), false),
|
|
1746
|
+
},
|
|
1747
|
+
],
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
/* ------------------------------------------------------------- the result -- */
|
|
1752
|
+
|
|
1753
|
+
// Everything rendered here is the CLI's `--json` object. The panel adds no
|
|
1754
|
+
// verdict of its own: `ok` is the CLI's, `verify_method` is which rung actually
|
|
1755
|
+
// answered, and `note` is its own caveat about what that rung did NOT prove.
|
|
1756
|
+
function exPingResult(result, wire, payload, profile, pastedKey, body, cfg) {
|
|
1757
|
+
result.replaceChildren();
|
|
1758
|
+
if (payload && payload.fixture) {
|
|
1759
|
+
wire.settle(false);
|
|
1760
|
+
result.append(el("div", "note", t("extra.fixture")));
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
const d = (payload && payload.data) || null;
|
|
1764
|
+
if (!d) {
|
|
1765
|
+
wire.settle(false);
|
|
1766
|
+
result.append(el("div", "note bad", (payload && payload.error) || t("common.loadFail")));
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
wire.settle(!!d.ok);
|
|
1770
|
+
if (!d.ok) {
|
|
1771
|
+
const box = el("div", "ex-result-bad");
|
|
1772
|
+
box.append(chip(d.reason || "unreachable", "bad"));
|
|
1773
|
+
if (d.error) box.append(el("div", "note", d.error));
|
|
1774
|
+
if (d.base_url) box.append(el("div", "note mono", d.base_url));
|
|
1775
|
+
// v0.51.0 — a tool that is not installed is not an unreachable endpoint, and
|
|
1776
|
+
// the fix is not a retry. The CLI carries the command; the panel prints it.
|
|
1777
|
+
if (d.install_cmd) box.append(el("div", "action-cmd", d.install_cmd));
|
|
1778
|
+
if (d.reason === "not-installed")
|
|
1779
|
+
box.append(
|
|
1780
|
+
el("div", "note", d.no_install_alternative ? t("extra.tools.altYes") + " " + d.no_install_alternative : t("extra.tools.altNone"))
|
|
1781
|
+
);
|
|
1782
|
+
// The reset the CLI performed, said out loud: a failed test on a connection
|
|
1783
|
+
// that had never verified leaves NOTHING behind.
|
|
1784
|
+
if (d.profile_reverted) box.append(el("div", "note", t("extra.result.reverted")));
|
|
1785
|
+
result.append(box);
|
|
1786
|
+
exRefresh(body);
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
const box = el("div", "ex-result-ok");
|
|
1791
|
+
box.append(
|
|
1792
|
+
chip(
|
|
1793
|
+
t("extra.result.verified", { how: d.verify_method || "—" }) +
|
|
1794
|
+
(d.latency_ms ? " · " + t("extra.profile.latency", { ms: d.latency_ms }) : ""),
|
|
1795
|
+
"ok"
|
|
1796
|
+
)
|
|
1797
|
+
);
|
|
1798
|
+
if (d.models_seen && d.models_seen.length) {
|
|
1799
|
+
const list = el("div", "ex-models");
|
|
1800
|
+
for (const m of d.models_seen) list.append(el("span", "mono ex-model", m));
|
|
1801
|
+
box.append(list);
|
|
1802
|
+
}
|
|
1803
|
+
// A rung that authenticated and then rejected an invented model name proves
|
|
1804
|
+
// the URL and the credential and NOTHING about a model. The CLI says so; it is
|
|
1805
|
+
// shown, never softened.
|
|
1806
|
+
if (d.note) box.append(el("div", "note", d.note));
|
|
1807
|
+
exLiveBlock(box, d);
|
|
1808
|
+
|
|
1809
|
+
// THE SAVE OPENS ONLY ON A GREEN TEST, and the self-destruct is named BEFORE
|
|
1810
|
+
// the save rather than after it.
|
|
1811
|
+
if (d.vault && d.vault.stored) {
|
|
1812
|
+
box.append(el("div", "note", t("extra.save.stored", { pepper: d.vault.pepper })));
|
|
1813
|
+
if (d.vault.honesty) box.append(el("div", "note", d.vault.honesty));
|
|
1814
|
+
} else if (d.vault && d.vault.error) {
|
|
1815
|
+
box.append(el("div", "note", d.vault.error));
|
|
1816
|
+
// v0.52.0 — THE HARD GATE. The save used to be an optional inline box the
|
|
1817
|
+
// user could ignore, and ignoring it is what left a vaulted key with no
|
|
1818
|
+
// passphrase and a run that locked at wave 1. A green test on a connection
|
|
1819
|
+
// that will hold its key in the vault now opens a modal with no exit but
|
|
1820
|
+
// Save and one destructive escape.
|
|
1821
|
+
if (pastedKey) box.append(exSaveRow(profile, pastedKey, body, cfg));
|
|
1822
|
+
}
|
|
1823
|
+
result.append(box);
|
|
1824
|
+
exRefresh(body);
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
/* THE LIVE RESULT. Everything here is the CLI's `--json` object and the panel
|
|
1828
|
+
adds no verdict of its own.
|
|
1829
|
+
|
|
1830
|
+
TWO HONESTIES ARE NOT OPTIONAL CHROME.
|
|
1831
|
+
|
|
1832
|
+
`model_reported: null` WITH `reports_model: false` is a real pair, not a
|
|
1833
|
+
missing field: neither local tool reports which model answered, so a
|
|
1834
|
+
substitution is INVISIBLE on that engine. The renderer says that sentence and
|
|
1835
|
+
never leaves a blank, because a blank reads as "nothing went wrong".
|
|
1836
|
+
|
|
1837
|
+
The reply is FOREIGN INPUT (`_shared/untrusted-input.md`): a third party's
|
|
1838
|
+
text, capped by the CLI, rendered as DOM text and never as HTML, and never
|
|
1839
|
+
acted on. It is evidence that something answered — nothing more. */
|
|
1840
|
+
function exLiveBlock(box, d) {
|
|
1841
|
+
if (!d.reply_excerpt && !d.tokens && !d.model_requested) return;
|
|
1842
|
+
const live = el("div", "ex-live");
|
|
1843
|
+
const head = el("div", "row-actions");
|
|
1844
|
+
if (d.latency_ms) head.append(chip(t("extra.profile.latency", { ms: d.latency_ms }), "info"));
|
|
1845
|
+
if (d.model_requested) head.append(el("span", "mono note", d.model_requested));
|
|
1846
|
+
live.append(head);
|
|
1847
|
+
|
|
1848
|
+
const kv = [[t("extra.live.requested"), d.model_requested || "—"]];
|
|
1849
|
+
kv.push([
|
|
1850
|
+
t("extra.live.reported"),
|
|
1851
|
+
d.model_reported ? d.model_reported : d.reports_model === false ? t("extra.live.noReport") : "—",
|
|
1852
|
+
]);
|
|
1853
|
+
live.append(kvList(kv));
|
|
1854
|
+
|
|
1855
|
+
if (d.reply_excerpt) {
|
|
1856
|
+
live.append(el("div", "field-label", t("extra.live.reply")));
|
|
1857
|
+
// DOM text, never HTML. The cap is the CLI's.
|
|
1858
|
+
live.append(el("pre", "block wrap ex-live-reply", d.reply_excerpt));
|
|
1859
|
+
if (d.reply_truncated) live.append(el("div", "note", t("extra.live.truncated")));
|
|
1860
|
+
if (d.foreign_input) live.append(el("div", "note", d.foreign_input));
|
|
1861
|
+
}
|
|
1862
|
+
if (d.tokens) {
|
|
1863
|
+
// FOUR KINDS, NEVER BLENDED (/orc-budget). A kind the tool has no concept of
|
|
1864
|
+
// reads an em dash, never a zero — unknown is not zero.
|
|
1865
|
+
const rows = [
|
|
1866
|
+
[t("extra.live.input"), d.tokens.input],
|
|
1867
|
+
[t("extra.live.cacheWrite"), d.tokens.cache_write],
|
|
1868
|
+
[t("extra.live.cacheRead"), d.tokens.cache_read],
|
|
1869
|
+
[t("extra.live.output"), d.tokens.output],
|
|
1870
|
+
];
|
|
1871
|
+
live.append(kvList(rows.map(([k, v]) => [k, v === null || v === undefined ? "—" : String(v)])));
|
|
1872
|
+
}
|
|
1873
|
+
if (d.cost_note) live.append(el("div", "note", d.cost_note));
|
|
1874
|
+
box.append(live);
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
// Saving re-runs the test with the passphrase attached, because that is the only
|
|
1878
|
+
// way the CLI ever stores a key: proved by a live connection first, every time.
|
|
1879
|
+
//
|
|
1880
|
+
// v0.52.0 — the inline form is now the LAST RESORT rather than the route. When
|
|
1881
|
+
// the panel knows the config (and it does, everywhere the connect flow runs) the
|
|
1882
|
+
// key and its deadline are asked for together, in a modal that cannot be walked
|
|
1883
|
+
// away from half-done.
|
|
1884
|
+
function exSaveRow(profile, pastedKey, body, cfg) {
|
|
1885
|
+
if (cfg) {
|
|
1886
|
+
exSessionModal({ name: profile, provider: "", engine: "", session: null }, body, cfg, false, pastedKey);
|
|
1887
|
+
return el("div", "note", t("extra.session.opened"));
|
|
1888
|
+
}
|
|
1889
|
+
return exSaveRowInline(profile, pastedKey, body);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function exSaveRowInline(profile, pastedKey, body) {
|
|
1893
|
+
const wrap = el("div", "ex-save");
|
|
1894
|
+
wrap.append(el("div", "ex-probe-head", t("extra.save.head")));
|
|
1895
|
+
wrap.append(el("div", "banner banner-bad", t("extra.save.warning")));
|
|
1896
|
+
const pass = exInput(t("extra.save.passPh"), "password");
|
|
1897
|
+
const again = exInput(t("extra.save.againPh"), "password");
|
|
1898
|
+
wrap.append(exField(t("extra.save.pass"), pass, t("extra.save.passHint")));
|
|
1899
|
+
wrap.append(exField(t("extra.save.again"), again));
|
|
1900
|
+
const out = el("div", "note");
|
|
1901
|
+
const b = el("button", "btn btn-sm btn-primary btn-allow-busy", t("extra.save.button"));
|
|
1902
|
+
b.type = "button";
|
|
1903
|
+
b.addEventListener("click", async () => {
|
|
1904
|
+
out.textContent = "";
|
|
1905
|
+
if (!pass.value || pass.value !== again.value) {
|
|
1906
|
+
out.textContent = t("extra.save.mismatch");
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
b.disabled = true;
|
|
1910
|
+
try {
|
|
1911
|
+
const r = await post("/api/extra/ping", { profile, key: pastedKey, passphrase: pass.value });
|
|
1912
|
+
const d = (r && r.data) || null;
|
|
1913
|
+
if (d && d.vault && d.vault.stored) {
|
|
1914
|
+
out.textContent = t("extra.save.stored", { pepper: d.vault.pepper });
|
|
1915
|
+
wrap.replaceChildren(out);
|
|
1916
|
+
if (d.vault.honesty) wrap.append(el("div", "note", d.vault.honesty));
|
|
1917
|
+
exRefresh(body);
|
|
1918
|
+
} else {
|
|
1919
|
+
out.textContent = (d && d.vault && d.vault.error) || (r && r.error) || t("common.loadFail");
|
|
1920
|
+
}
|
|
1921
|
+
} catch (e) {
|
|
1922
|
+
out.textContent = String(e.message);
|
|
1923
|
+
} finally {
|
|
1924
|
+
b.disabled = false;
|
|
1925
|
+
pass.value = "";
|
|
1926
|
+
again.value = "";
|
|
1927
|
+
}
|
|
1928
|
+
});
|
|
1929
|
+
wrap.append(b, out);
|
|
1930
|
+
return wrap;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
/* ------------------------------------------------------------ unlock, remove */
|
|
1934
|
+
|
|
1935
|
+
// The ONE action that clears a countdown: a correct passphrase resets the
|
|
1936
|
+
// counter to zero. It never yields the key — `orc extra unlock` answers one
|
|
1937
|
+
// question, with a yes or a no.
|
|
1938
|
+
function exUnlockModal(p, body) {
|
|
1939
|
+
const form = el("div", "stack stack-sm");
|
|
1940
|
+
form.append(el("div", "note", t("extra.unlock.what")));
|
|
1941
|
+
const pass = exInput(t("extra.save.passPh"), "password");
|
|
1942
|
+
form.append(exField(t("extra.save.pass"), pass));
|
|
1943
|
+
const out = el("div", "note");
|
|
1944
|
+
form.append(out);
|
|
1945
|
+
modal({
|
|
1946
|
+
title: t("extra.unlock.title", { name: p.name }),
|
|
1947
|
+
body: form,
|
|
1948
|
+
actions: [
|
|
1949
|
+
{ label: t("common.cancel"), onClick: (c) => c() },
|
|
1950
|
+
{
|
|
1951
|
+
label: t("extra.unlock.button"),
|
|
1952
|
+
cls: "btn-primary",
|
|
1953
|
+
id: "ex-unlock-btn",
|
|
1954
|
+
onClick: async () => {
|
|
1955
|
+
const b = document.getElementById("ex-unlock-btn");
|
|
1956
|
+
out.textContent = "";
|
|
1957
|
+
b.disabled = true;
|
|
1958
|
+
try {
|
|
1959
|
+
const r = await post("/api/extra/unlock", { profile: p.name, passphrase: pass.value });
|
|
1960
|
+
const d = (r && r.data) || null;
|
|
1961
|
+
out.replaceChildren();
|
|
1962
|
+
// The countdown is the CLI's own message, verbatim — "wrong
|
|
1963
|
+
// passphrase — attempt 8 of 10" is the point, and a panel that
|
|
1964
|
+
// summarised it would delete the one number this feature exists to
|
|
1965
|
+
// show. Its honesty line rides with it: the counter stops someone at
|
|
1966
|
+
// your keyboard and does not stop someone who copied the file.
|
|
1967
|
+
if (d && d.ok) out.append(el("div", null, t("extra.unlock.ok")));
|
|
1968
|
+
else if (d) {
|
|
1969
|
+
out.append(el("div", "bad", d.error || t("common.loadFail")));
|
|
1970
|
+
if (d.honesty) out.append(el("div", "note", d.honesty));
|
|
1971
|
+
} else out.append(el("div", "bad", (r && r.error) || t("common.loadFail")));
|
|
1972
|
+
exRefresh(body);
|
|
1973
|
+
} catch (e) {
|
|
1974
|
+
out.textContent = String(e.message);
|
|
1975
|
+
} finally {
|
|
1976
|
+
b.disabled = false;
|
|
1977
|
+
pass.value = "";
|
|
1978
|
+
}
|
|
1979
|
+
},
|
|
1980
|
+
},
|
|
1981
|
+
],
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// A reason is REQUIRED and the CLI is what refuses without one. The form only
|
|
1986
|
+
// collects it, and the CLI's answer names the route rows the removal drops.
|
|
1987
|
+
function exRemoveModal(p, body) {
|
|
1988
|
+
const form = el("div", "stack stack-sm");
|
|
1989
|
+
form.append(el("div", "note", t("extra.remove.what", { name: p.name })));
|
|
1990
|
+
const reason = exInput(t("extra.remove.reasonPh"));
|
|
1991
|
+
form.append(exField(t("extra.remove.reason"), reason, t("extra.remove.reasonHint")));
|
|
1992
|
+
const out = el("div", "note");
|
|
1993
|
+
form.append(out);
|
|
1994
|
+
modal({
|
|
1995
|
+
title: t("extra.remove.title", { name: p.name }),
|
|
1996
|
+
body: form,
|
|
1997
|
+
actions: [
|
|
1998
|
+
{ label: t("common.cancel"), onClick: (c) => c() },
|
|
1999
|
+
{
|
|
2000
|
+
label: t("extra.remove.button"),
|
|
2001
|
+
cls: "btn-danger",
|
|
2002
|
+
id: "ex-remove-btn",
|
|
2003
|
+
onClick: async (close) => {
|
|
2004
|
+
const b = document.getElementById("ex-remove-btn");
|
|
2005
|
+
out.textContent = "";
|
|
2006
|
+
b.disabled = true;
|
|
2007
|
+
try {
|
|
2008
|
+
const r = await post("/api/extra/remove", { name: p.name, reason: reason.value });
|
|
2009
|
+
if (r && r.ok) {
|
|
2010
|
+
close();
|
|
2011
|
+
toast(t("extra.remove.done", { name: p.name }), "ok");
|
|
2012
|
+
exRefresh(body);
|
|
2013
|
+
} else {
|
|
2014
|
+
out.textContent = (r && (r.output || r.error)) || t("common.loadFail");
|
|
2015
|
+
}
|
|
2016
|
+
} catch (e) {
|
|
2017
|
+
out.textContent = String(e.message);
|
|
2018
|
+
} finally {
|
|
2019
|
+
b.disabled = false;
|
|
2020
|
+
}
|
|
2021
|
+
},
|
|
2022
|
+
},
|
|
2023
|
+
],
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// The CLI's own stdout for a refused write, shown exactly as it was printed.
|
|
2028
|
+
function exOutput(text) {
|
|
2029
|
+
return el("pre", "block wrap fail-detail", String(text || "").slice(0, 2000));
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// Re-render the panel against the new truth. The modal deliberately stays open:
|
|
2033
|
+
// the result of the thing you just did is inside it.
|
|
2034
|
+
function exRefresh(body) {
|
|
2035
|
+
if (body) renderExtra(body);
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
/* ================================================ the routing rail (W12 T1) ==
|
|
2039
|
+
THE RAIL IS THE PICTURE THAT MAKES THIS CONFIGURABLE AT A GLANCE. One
|
|
2040
|
+
horizontal 0→100 axis, every band a segment, drawn from `orc extra route
|
|
2041
|
+
--json` and from nothing else — the panel computes no band edge, no agent
|
|
2042
|
+
name and no fall-through.
|
|
2043
|
+
|
|
2044
|
+
AN UNMAPPED RANGE KEEPS ITS SLOT and is drawn in Claude's colour with the
|
|
2045
|
+
agent it actually resolves to. Filtering the gaps out would make "I left the
|
|
2046
|
+
top band on Opus on purpose" and "there is no top band" identical, and would
|
|
2047
|
+
make the rail change width on every edit — the v0.43.7 OFF-phase rule.
|
|
2048
|
+
|
|
2049
|
+
GEOMETRY IS SOLVED FROM THE BOX: a segment is `var(--w)` of the axis with a
|
|
2050
|
+
readable MINIMUM, so a rail too narrow for its labels SCROLLS inside its own
|
|
2051
|
+
box and is never squeezed. (The VAULT/ringRadii lesson: a label sized by a
|
|
2052
|
+
fraction of the container knows nothing about how wide a word is.)
|
|
2053
|
+
|
|
2054
|
+
WRITES ARE STAGED AND BATCHED, the v0.44.1 rule: nothing is written until
|
|
2055
|
+
Apply, the pending edits are NAMED, a refused write never aborts the rest,
|
|
2056
|
+
and Discard renders only while dirty. */
|
|
2057
|
+
|
|
2058
|
+
// Which band is open in the editor, so an Apply that re-renders does not throw
|
|
2059
|
+
// you back to the top of the list.
|
|
2060
|
+
let EX_OPEN_BAND = null;
|
|
2061
|
+
|
|
2062
|
+
/* WHICH LANE DOES A BAND GOVERN (v0.52.0, D6).
|
|
2063
|
+
|
|
2064
|
+
The routing table says `[40,55) → opencode/big-pickle`. That is true for
|
|
2065
|
+
`/orc`, which scores every task, and it is NOT how `/orc-fast` works: that
|
|
2066
|
+
lane pins ONE executor and resolves the BAND its agent already encodes, at
|
|
2067
|
+
BOTH EDGES, requiring them to agree. The rule was implemented and written
|
|
2068
|
+
down and rendered nowhere, so a user reading this panel could not get from a
|
|
2069
|
+
band to the decision it drives.
|
|
2070
|
+
|
|
2071
|
+
Every word here is `orc extra lanes --json`: the shape, the verdict, the
|
|
2072
|
+
edges, whether they agreed, and the sentence underneath. The panel decides
|
|
2073
|
+
nothing — a second idea of the routing is drift no lint can see. */
|
|
2074
|
+
/* ================================================ the POSITIONS (v0.55.0) ==
|
|
2075
|
+
|
|
2076
|
+
A SCORE IS WHAT A BAND NEEDS, AND FOUR LANES DO NOT HAVE ONE. The ladder
|
|
2077
|
+
above this one is the SCORED half — `/orc`, `/orc-ultra`, `/orc-mini`,
|
|
2078
|
+
`/orc-diy`. This one is the other half: `/orc-quick`, `/orc-fast`, `/orc-doc`
|
|
2079
|
+
and `/orc-wiki` pin an agent to a POSITION, and a position is a point rather
|
|
2080
|
+
than an interval.
|
|
2081
|
+
|
|
2082
|
+
EVERY STRING IN THIS CARD COMES FROM `orc extra role list --json`. The panel
|
|
2083
|
+
names no slot meaning, no provider, no model and no agent of its own — the
|
|
2084
|
+
Flow-stepper rule. Writing "cheap work" beside a slot would be the panel
|
|
2085
|
+
deciding what a position means.
|
|
2086
|
+
|
|
2087
|
+
AN UNROUTED SLOT KEEPS ITS SLOT and is drawn as Claude with its pinned agent
|
|
2088
|
+
— the OFF-phase rule. Filtering it out makes "I left the checker on Claude on
|
|
2089
|
+
purpose" and "there is no checker" identical, and makes the ladder change
|
|
2090
|
+
length every time a row is cleared.
|
|
2091
|
+
|
|
2092
|
+
NO PROPORTIONAL BAR. A band has a width because it covers a range of scores;
|
|
2093
|
+
a position covers nothing, and drawing one a width would be the panel
|
|
2094
|
+
inventing an interval the CLI never computed. */
|
|
2095
|
+
function exSlotCard(d, body, edits) {
|
|
2096
|
+
const c = card(t("extra.slots.title"));
|
|
2097
|
+
if (d.errors.role) {
|
|
2098
|
+
c.append(failBox(d.errors.role));
|
|
2099
|
+
return c;
|
|
2100
|
+
}
|
|
2101
|
+
const r = d.role || {};
|
|
2102
|
+
const rows = r.slots || [];
|
|
2103
|
+
c.append(el("div", "note", t("extra.slots.sub")));
|
|
2104
|
+
c.append(exWhy(t("extra.slots.subWhy")));
|
|
2105
|
+
|
|
2106
|
+
const verified = ((d.list && d.list.counts) || {}).verified || 0;
|
|
2107
|
+
const list = el("div", "ex-ladder");
|
|
2108
|
+
const entries = [];
|
|
2109
|
+
const openOnly = (which) => {
|
|
2110
|
+
for (const e of entries) if (e !== which) e.close();
|
|
2111
|
+
};
|
|
2112
|
+
for (const row of rows) {
|
|
2113
|
+
const e = exSlotRow(row, d, edits, verified > 0, openOnly);
|
|
2114
|
+
entries.push(e);
|
|
2115
|
+
list.append(e.node);
|
|
2116
|
+
}
|
|
2117
|
+
c.append(list);
|
|
2118
|
+
// The CLI's own sentence: absence is not a hole.
|
|
2119
|
+
if (r.note) c.append(el("div", "note", r.note));
|
|
2120
|
+
c.append(exWhy(t("extra.slots.keepsSlotWhy")));
|
|
2121
|
+
|
|
2122
|
+
// The six `extra_roles` values nothing can resolve. REPORTED, not fixed — and
|
|
2123
|
+
// reported in the CLI's own words, because "nothing resolves this" is a state
|
|
2124
|
+
// and a state word is never the panel's to soften.
|
|
2125
|
+
const un = r.unreachable_roles || [];
|
|
2126
|
+
if (un.length) {
|
|
2127
|
+
const box = el("div", "ex-unreachable");
|
|
2128
|
+
box.append(el("div", "note", t("extra.slots.unreachable")));
|
|
2129
|
+
const ul = el("div", "ex-unreachable-list");
|
|
2130
|
+
for (const u of un) {
|
|
2131
|
+
const row = el("div", "row-actions");
|
|
2132
|
+
row.append(el("span", "mono", u.role));
|
|
2133
|
+
row.append(el("span", "note", u.state));
|
|
2134
|
+
ul.append(row);
|
|
2135
|
+
}
|
|
2136
|
+
box.append(ul);
|
|
2137
|
+
c.append(box);
|
|
2138
|
+
}
|
|
2139
|
+
if (!verified) c.append(empty(t("extra.routing.locked"), t("extra.routing.lockedHint")));
|
|
2140
|
+
return c;
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// Which position is open in the editor, so an Apply that re-renders does not
|
|
2144
|
+
// throw the person out of the row they were working in — the Runs-row rule.
|
|
2145
|
+
let EX_OPEN_SLOT = null;
|
|
2146
|
+
|
|
2147
|
+
function exSlotRow(raw, d, edits, verified, openOnly) {
|
|
2148
|
+
const key = "role " + raw.slot;
|
|
2149
|
+
const node = el("div", "ex-band ex-slot");
|
|
2150
|
+
const headBtn = el("button", "ex-band-head ex-slot-head");
|
|
2151
|
+
headBtn.type = "button";
|
|
2152
|
+
headBtn.setAttribute("aria-expanded", "false");
|
|
2153
|
+
const pane = el("div", "ex-band-pane");
|
|
2154
|
+
node.append(headBtn, pane);
|
|
2155
|
+
|
|
2156
|
+
const isOpen = () => node.classList.contains("open");
|
|
2157
|
+
const setOpen = (open) => {
|
|
2158
|
+
node.classList.toggle("open", open);
|
|
2159
|
+
headBtn.setAttribute("aria-expanded", String(open));
|
|
2160
|
+
if (open) EX_OPEN_SLOT = raw.slot;
|
|
2161
|
+
else if (EX_OPEN_SLOT === raw.slot) EX_OPEN_SLOT = null;
|
|
2162
|
+
pane.replaceChildren(open ? editor() : frag());
|
|
2163
|
+
};
|
|
2164
|
+
|
|
2165
|
+
// What a staged edit would make this row read as. `edits` holds the intent,
|
|
2166
|
+
// never the write — nothing reaches disk until Apply.
|
|
2167
|
+
const preview = () => {
|
|
2168
|
+
const staged = edits.map.get(key);
|
|
2169
|
+
if (!staged) return { routes: raw.resolved === "extra", profile: raw.profile, model: raw.model, staged: false };
|
|
2170
|
+
if (staged.kind === "clear" || staged.url === "/api/extra/role/rm")
|
|
2171
|
+
return { routes: false, profile: null, model: null, staged: true };
|
|
2172
|
+
return { routes: true, profile: staged.profile || null, model: staged.model || null, staged: true };
|
|
2173
|
+
};
|
|
2174
|
+
|
|
2175
|
+
const paint = () => {
|
|
2176
|
+
const p = preview();
|
|
2177
|
+
headBtn.replaceChildren();
|
|
2178
|
+
const mid = el("span", "ex-band-mid");
|
|
2179
|
+
mid.append(el("span", "mono ex-band-label", raw.slot));
|
|
2180
|
+
// The LANE, and whether this position is asked for or announced — both the
|
|
2181
|
+
// CLI's words. `announce_point` is where the sentence gets printed, which is
|
|
2182
|
+
// the thing a person actually wants to know before routing a position.
|
|
2183
|
+
mid.append(el("span", "note ex-band-range", raw.lane));
|
|
2184
|
+
headBtn.append(mid);
|
|
2185
|
+
headBtn.append(el("span", "note ex-slot-when", raw.asks ? t("extra.slots.asked") : raw.announce_point));
|
|
2186
|
+
// THE TARGET, never truncated, and never guessed: a position that is not
|
|
2187
|
+
// routed falls through to a NAMED agent, which the CLI hands over.
|
|
2188
|
+
headBtn.append(
|
|
2189
|
+
el("span", "mono ex-band-target", p.routes && p.profile ? p.profile + "/" + p.model : "claude · " + raw.claude.agent)
|
|
2190
|
+
);
|
|
2191
|
+
const chips = el("span", "ex-band-chips");
|
|
2192
|
+
if (p.staged) chips.append(chip(t("extra.routing.staged"), "info"));
|
|
2193
|
+
if (!p.staged && raw.resolved === "extra" && raw.engine) chips.append(chip(raw.engine, "info"));
|
|
2194
|
+
// The CLI's state word, verbatim.
|
|
2195
|
+
if (!p.staged && raw.routed && raw.verify_state)
|
|
2196
|
+
chips.append(chip(raw.verify_state, raw.verify_state === "VERIFIED" ? "ok" : "warn"));
|
|
2197
|
+
if (!p.staged && raw.routed && raw.model_known === false) chips.append(chip(t("extra.routing.modelGone"), "warn"));
|
|
2198
|
+
// ROUTED BUT NOT RESOLVING. The row is held back and the CLI said why — a
|
|
2199
|
+
// position that quietly fell back to Claude is the thing this chip exists
|
|
2200
|
+
// to make impossible to miss.
|
|
2201
|
+
if (!p.staged && raw.routed && raw.resolved !== "extra") chips.append(chip(raw.held_back || "claude", "warn"));
|
|
2202
|
+
headBtn.append(chips);
|
|
2203
|
+
headBtn.append(el("span", "ex-band-caret", "▸"));
|
|
2204
|
+
headBtn.title = raw.slot + " → " + (p.routes && p.profile ? p.profile + "/" + p.model : raw.claude.agent);
|
|
2205
|
+
if (isOpen()) pane.replaceChildren(editor());
|
|
2206
|
+
};
|
|
2207
|
+
|
|
2208
|
+
const editor = () => {
|
|
2209
|
+
const box = el("div", "stack stack-sm");
|
|
2210
|
+
// WHAT THIS POSITION IS, in the CLI's words.
|
|
2211
|
+
box.append(el("div", "note", raw.meaning));
|
|
2212
|
+
box.append(el("div", "note", raw.why));
|
|
2213
|
+
const staged = edits.map.get(key);
|
|
2214
|
+
if (staged) {
|
|
2215
|
+
const line = el("div", "note ex-route-staged");
|
|
2216
|
+
line.append(chip(t("extra.routing.staged"), "info"));
|
|
2217
|
+
line.append(document.createTextNode(" " + staged.value));
|
|
2218
|
+
const undo = el("button", "btn btn-ghost btn-sm", t("extra.routing.undo"));
|
|
2219
|
+
undo.type = "button";
|
|
2220
|
+
undo.addEventListener("click", () => {
|
|
2221
|
+
edits.drop(key);
|
|
2222
|
+
paint();
|
|
2223
|
+
});
|
|
2224
|
+
line.append(undo);
|
|
2225
|
+
box.append(line);
|
|
2226
|
+
return box;
|
|
2227
|
+
}
|
|
2228
|
+
if (!verified) {
|
|
2229
|
+
box.append(el("div", "note", t("extra.routing.lockedHint")));
|
|
2230
|
+
return box;
|
|
2231
|
+
}
|
|
2232
|
+
if (raw.routed) {
|
|
2233
|
+
box.append(
|
|
2234
|
+
kvList([
|
|
2235
|
+
[t("extra.routing.kvProfile"), raw.profile],
|
|
2236
|
+
[t("extra.routing.kvModel"), raw.model],
|
|
2237
|
+
[t("extra.routing.kvSmall"), raw.small_model || "—"],
|
|
2238
|
+
[t("extra.routing.kvTurns"), raw.max_turns === null || raw.max_turns === undefined ? "—" : String(raw.max_turns)],
|
|
2239
|
+
[t("extra.slots.kvDisplaces"), raw.claude.agents.join(" · ")],
|
|
2240
|
+
])
|
|
2241
|
+
);
|
|
2242
|
+
if (raw.next) box.append(el("div", "note", raw.next));
|
|
2243
|
+
const clear = el("button", "btn btn-sm", t("extra.slots.clear"));
|
|
2244
|
+
clear.type = "button";
|
|
2245
|
+
clear.addEventListener("click", () => {
|
|
2246
|
+
edits.action(key, "/api/extra/role/rm", { slot: raw.slot }, t("extra.slots.stagedClear", { agent: raw.claude.agent }));
|
|
2247
|
+
paint();
|
|
2248
|
+
});
|
|
2249
|
+
const acts = el("div", "row-actions");
|
|
2250
|
+
acts.append(clear);
|
|
2251
|
+
box.append(acts);
|
|
2252
|
+
return box;
|
|
2253
|
+
}
|
|
2254
|
+
box.append(el("div", "note", t("extra.slots.displaces", { agents: raw.claude.agents.join(" · ") })));
|
|
2255
|
+
box.append(exSlotControls(raw, d, edits, key, paint));
|
|
2256
|
+
return box;
|
|
2257
|
+
};
|
|
2258
|
+
|
|
2259
|
+
headBtn.addEventListener("click", () => {
|
|
2260
|
+
const next = !isOpen();
|
|
2261
|
+
if (next) openOnly(entry);
|
|
2262
|
+
setOpen(next);
|
|
2263
|
+
});
|
|
2264
|
+
paint();
|
|
2265
|
+
const entry = { node, close: () => isOpen() && setOpen(false) };
|
|
2266
|
+
if (EX_OPEN_SLOT === raw.slot) setOpen(true);
|
|
2267
|
+
return entry;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
function exSlotControls(row, d, edits, key, paint) {
|
|
2271
|
+
// The same two controls the band editor uses, for the same reasons: a
|
|
2272
|
+
// connection that has never answered is DISABLED and labelled rather than
|
|
2273
|
+
// hidden, and whether the model control is a dropdown or a box is the CLI's
|
|
2274
|
+
// answer, never this panel's.
|
|
2275
|
+
const profiles = (d.list && d.list.profiles) || [];
|
|
2276
|
+
const sel = exSelect(
|
|
2277
|
+
[{ value: "", label: t("extra.routing.pickProfile") }].concat(
|
|
2278
|
+
profiles.map((p) => ({
|
|
2279
|
+
value: p.name,
|
|
2280
|
+
label: p.name + (p.verified_at ? "" : " " + t("extra.routing.untested")),
|
|
2281
|
+
disabled: !p.verified_at,
|
|
2282
|
+
}))
|
|
2283
|
+
)
|
|
2284
|
+
);
|
|
2285
|
+
const box = exModelBox(t("extra.routing.modelPh"));
|
|
2286
|
+
const fillModels = () => {
|
|
2287
|
+
const p = profiles.find((x) => x.name === sel.value);
|
|
2288
|
+
box.load(p ? p.name : null);
|
|
2289
|
+
};
|
|
2290
|
+
sel.addEventListener("change", fillModels);
|
|
2291
|
+
fillModels();
|
|
2292
|
+
|
|
2293
|
+
const add = el("button", "btn btn-sm", t("extra.slots.hold"));
|
|
2294
|
+
add.type = "button";
|
|
2295
|
+
add.addEventListener("click", () => {
|
|
2296
|
+
const chosen = box.value();
|
|
2297
|
+
if (!sel.value || !chosen) return;
|
|
2298
|
+
const target = sel.value + "/" + chosen;
|
|
2299
|
+
// REPLACING A POSITION IS A ROUTING CHANGE, so the confirmation NAMES what
|
|
2300
|
+
// it replaces. A count is not consent — and neither is a slot id.
|
|
2301
|
+
if (row.routed && !confirm(t("extra.slots.confirmReplace", { slot: row.slot, old: row.profile + "/" + row.model, next: target })))
|
|
2302
|
+
return;
|
|
2303
|
+
edits.action(
|
|
2304
|
+
key,
|
|
2305
|
+
"/api/extra/role/set",
|
|
2306
|
+
{ slot: row.slot, target, profile: sel.value, model: chosen },
|
|
2307
|
+
t("extra.slots.stagedSet", { slot: row.slot, target })
|
|
2308
|
+
);
|
|
2309
|
+
paint();
|
|
2310
|
+
});
|
|
2311
|
+
|
|
2312
|
+
const controls = el("div", "ex-route-controls");
|
|
2313
|
+
controls.append(sel, box.node, add);
|
|
2314
|
+
return controls;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
function exLanesCard(d) {
|
|
2318
|
+
const c = card(t("extra.lanes.title"));
|
|
2319
|
+
c.append(el("div", "note", t("extra.lanes.sub")));
|
|
2320
|
+
c.append(exWhy(t("extra.lanes.subWhy")));
|
|
2321
|
+
if (d.errors.lanes) {
|
|
2322
|
+
c.append(failBox(d.errors.lanes));
|
|
2323
|
+
return c;
|
|
2324
|
+
}
|
|
2325
|
+
const lanes = (d.lanes && d.lanes.lanes) || [];
|
|
2326
|
+
if (!lanes.length) {
|
|
2327
|
+
c.append(empty(t("extra.lanes.none"), ""));
|
|
2328
|
+
return c;
|
|
2329
|
+
}
|
|
2330
|
+
const list = el("div", "ex-lanes");
|
|
2331
|
+
for (const l of lanes) {
|
|
2332
|
+
const row = el("div", "ex-lane");
|
|
2333
|
+
const top = el("div", "row-actions");
|
|
2334
|
+
top.append(el("span", "mono ex-lane-name", l.lane));
|
|
2335
|
+
top.append(el("span", "note", l.shape));
|
|
2336
|
+
// The verdict word is the CLI's own. A friendlier synonym would be a state
|
|
2337
|
+
// that does not exist.
|
|
2338
|
+
top.append(
|
|
2339
|
+
chip(l.routes, l.routes === "foreign" || l.routes === "roles" || l.routes === "per-task" ? "ok" : l.routes === "never" ? "info" : "warn")
|
|
2340
|
+
);
|
|
2341
|
+
row.append(top);
|
|
2342
|
+
// The disclosure `extra-dispatch.md` demands, finally rendered: both edges
|
|
2343
|
+
// and whether they agreed.
|
|
2344
|
+
if (l.band)
|
|
2345
|
+
row.append(
|
|
2346
|
+
el("div", "note mono", t("extra.lanes.edges", { band: l.band, edges: (l.edges || []).join(","), agree: String(l.agree) }))
|
|
2347
|
+
);
|
|
2348
|
+
// v0.55.0 — a SLOT lane has no band and no edges. It has one row per
|
|
2349
|
+
// POSITION, each carrying the agent it displaces, and an unrouted one keeps
|
|
2350
|
+
// its row: the whole point is that the two positions in a lane are two
|
|
2351
|
+
// separate decisions.
|
|
2352
|
+
for (const sl of l.slots || []) {
|
|
2353
|
+
const line = el("div", "note mono ex-lane-slot");
|
|
2354
|
+
line.append(el("span", "", sl.slot));
|
|
2355
|
+
line.append(el("span", "", " → "));
|
|
2356
|
+
line.append(el("span", "", sl.routes ? sl.profile + "/" + sl.model : "claude · " + sl.claude));
|
|
2357
|
+
row.append(line);
|
|
2358
|
+
}
|
|
2359
|
+
row.append(el("div", "note", l.detail));
|
|
2360
|
+
list.append(row);
|
|
2361
|
+
}
|
|
2362
|
+
c.append(list);
|
|
2363
|
+
// A lane not listed does not route foreign. Absence is a `no`, never an
|
|
2364
|
+
// omission to be interpreted.
|
|
2365
|
+
if (d.lanes && d.lanes.note) c.append(el("div", "note", d.lanes.note));
|
|
2366
|
+
return c;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
function exRoutingCard(d, body, edits) {
|
|
2370
|
+
const c = card(t("extra.routing.title"));
|
|
2371
|
+
if (d.errors.route) {
|
|
2372
|
+
c.append(failBox(d.errors.route));
|
|
2373
|
+
return c;
|
|
2374
|
+
}
|
|
2375
|
+
const r = d.route || {};
|
|
2376
|
+
const rows = r.rows || [];
|
|
2377
|
+
|
|
2378
|
+
// The CLI's own sentence when the master gate is off. Every row below is
|
|
2379
|
+
// inert and it says so — never a panel paraphrase.
|
|
2380
|
+
if (r.note) c.append(el("div", "banner", r.note));
|
|
2381
|
+
c.append(el("div", "note", t("extra.routing.sub")));
|
|
2382
|
+
|
|
2383
|
+
// R10 — THE GATE. Nothing may be routed to a connection that has never
|
|
2384
|
+
// answered, so the editor stays shut until one has, and the ONE line that
|
|
2385
|
+
// would open it is printed rather than implied. The LADDER still draws either
|
|
2386
|
+
// way: what your bands resolve to today is worth reading before you can
|
|
2387
|
+
// change it.
|
|
2388
|
+
const verified = ((d.list && d.list.counts) || {}).verified || 0;
|
|
2389
|
+
|
|
2390
|
+
c.append(exBandLadder(rows, d, edits, verified > 0));
|
|
2391
|
+
c.append(exBandLegend());
|
|
2392
|
+
c.append(el("div", "note", t("extra.routing.gapNote")));
|
|
2393
|
+
c.append(exWhy(t("extra.routing.gapWhy")));
|
|
2394
|
+
if (!verified) c.append(empty(t("extra.routing.locked"), t("extra.routing.lockedHint")));
|
|
2395
|
+
else c.append(el("div", "note", t("extra.routing.editNote")));
|
|
2396
|
+
return c;
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
/* THE BAND LADDER (v0.53.0) — ONE PICTURE, AND THE ROW YOU READ IS THE ROW YOU
|
|
2400
|
+
EDIT.
|
|
2401
|
+
|
|
2402
|
+
What this replaces was a horizontal 0→100 rail ABOVE a separate list of
|
|
2403
|
+
editable rows: the same data twice, and only the second copy was interactive.
|
|
2404
|
+
Six things were wrong with the rail and every one of them is structural.
|
|
2405
|
+
|
|
2406
|
+
1. THE TARGET WAS TRUNCATED — clipped mid-word, three rows out of seven.
|
|
2407
|
+
The single most important fact in the picture was the one you could not
|
|
2408
|
+
read.
|
|
2409
|
+
2. THE WIDTHS LIED. `min-width: 128px` fought `var(--w)`, so a 10-point
|
|
2410
|
+
band and a 30-point band came out nearly the same width while the
|
|
2411
|
+
`0 … 100` axis underneath promised they were to scale.
|
|
2412
|
+
3. THE LAST BAND WAS OFF-SCREEN with no affordance that the rail scrolled.
|
|
2413
|
+
4. NOTHING SAID WHAT THE COLOURS MEANT. Green versus blue was never
|
|
2414
|
+
explained anywhere on the page, and green is "this work leaves your
|
|
2415
|
+
machine" — see exBandLegend.
|
|
2416
|
+
5. `[0,30)` IS DEVELOPER NOTATION. The half-open bracket is load-bearing so
|
|
2417
|
+
it stays, but it now sits beside the CLI's plain reading of the same
|
|
2418
|
+
range instead of instead of one.
|
|
2419
|
+
6. AND IT WAS THE SAME DATA TWICE.
|
|
2420
|
+
|
|
2421
|
+
Vertical fixes all six at once: the proportional bar is a width INSIDE the
|
|
2422
|
+
row, so scale is honest and nothing is ever squeezed or clipped; the target
|
|
2423
|
+
gets the whole row; and the editor opens IN PLACE — the Runs-row and
|
|
2424
|
+
Knowledge-doc rule, one row open at a time. */
|
|
2425
|
+
function exBandLadder(rows, d, edits, verified) {
|
|
2426
|
+
const list = el("div", "ex-ladder");
|
|
2427
|
+
// NO FILTER, and that is the assertion rather than the absence. A range with
|
|
2428
|
+
// no connection of yours on it is a CLAUDE range and it keeps its row — the
|
|
2429
|
+
// v0.43.7 OFF-phase rule. Filtering the gaps out would make "I left the top
|
|
2430
|
+
// band on Claude on purpose" and "there is no top band" look identical.
|
|
2431
|
+
const entries = [];
|
|
2432
|
+
const openOnly = (which) => {
|
|
2433
|
+
for (const e of entries) if (e !== which) e.close();
|
|
2434
|
+
};
|
|
2435
|
+
for (const row of rows) {
|
|
2436
|
+
const e = exBandRow(row, d, edits, verified, openOnly);
|
|
2437
|
+
entries.push(e);
|
|
2438
|
+
list.append(e.node);
|
|
2439
|
+
}
|
|
2440
|
+
return list;
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
/* THE LEGEND — the fix for the single biggest comprehension gap on this panel.
|
|
2444
|
+
Two colours were carrying the whole meaning of the picture and neither was
|
|
2445
|
+
ever named. The words are the panel's own (they describe what the subsystem
|
|
2446
|
+
does, not this repo's state), which is the same rule the boundary card is
|
|
2447
|
+
written under. */
|
|
2448
|
+
function exBandLegend() {
|
|
2449
|
+
const box = el("div", "ex-legend");
|
|
2450
|
+
const item = (cls, label) => {
|
|
2451
|
+
const s = el("span", "ex-legend-item");
|
|
2452
|
+
s.append(el("span", "ex-legend-swatch " + cls));
|
|
2453
|
+
s.append(el("span", "note", label));
|
|
2454
|
+
box.append(s);
|
|
2455
|
+
};
|
|
2456
|
+
item("ex-band-extra", t("extra.routing.legendExtra"));
|
|
2457
|
+
item("ex-band-claude", t("extra.routing.legendClaude"));
|
|
2458
|
+
return box;
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
/* WHAT YOU ARE ABOUT TO DO, drawn from the STAGED shape rather than from disk.
|
|
2462
|
+
Nothing re-renders until Apply (v0.44.1), so a row has to show its own staged
|
|
2463
|
+
state and an undo has to put the control back without the panel reloading
|
|
2464
|
+
underneath it.
|
|
2465
|
+
|
|
2466
|
+
IT NEVER INVENTS THE OTHER HALF. Un-routing a band hands it back to the
|
|
2467
|
+
Claude ladder, and which agent it lands on is `claudeGaps`'s answer — split at
|
|
2468
|
+
the resolving table's own edges, which this panel does not know and must not
|
|
2469
|
+
learn. A staged un-route therefore draws a Claude row whose target is an em
|
|
2470
|
+
dash, and says it is recomputed on Apply. A guessed agent name here would be a
|
|
2471
|
+
picture of a run that is not going to happen. */
|
|
2472
|
+
function exPreviewRow(r, edits) {
|
|
2473
|
+
const staged = edits.map.get("route " + r.band);
|
|
2474
|
+
if (!staged) return r;
|
|
2475
|
+
if (staged.route === "/api/extra/route/rm")
|
|
2476
|
+
return { from: r.from, to: r.to, band: r.band, range: r.range, meaning: r.meaning, via: "claude", agent: null, staged: true };
|
|
2477
|
+
const target = String((staged.body && staged.body.target) || "");
|
|
2478
|
+
const cut = target.indexOf("/");
|
|
2479
|
+
return {
|
|
2480
|
+
from: r.from,
|
|
2481
|
+
to: r.to,
|
|
2482
|
+
band: r.band,
|
|
2483
|
+
range: r.range,
|
|
2484
|
+
meaning: r.meaning,
|
|
2485
|
+
via: "extra",
|
|
2486
|
+
profile: cut > 0 ? target.slice(0, cut) : target,
|
|
2487
|
+
model: cut > 0 ? target.slice(cut + 1) : "",
|
|
2488
|
+
engine: null,
|
|
2489
|
+
verify_state: null,
|
|
2490
|
+
model_known: true,
|
|
2491
|
+
staged: true,
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
function exPreviewRows(rows, edits) {
|
|
2495
|
+
return rows.map((r) => exPreviewRow(r, edits));
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
function exBandRow(raw, d, edits, verified, openOnly) {
|
|
2499
|
+
const key = "route " + raw.band;
|
|
2500
|
+
const node = el("div", "ex-band");
|
|
2501
|
+
const headBtn = el("button", "ex-band-head");
|
|
2502
|
+
headBtn.type = "button";
|
|
2503
|
+
headBtn.setAttribute("aria-expanded", "false");
|
|
2504
|
+
const pane = el("div", "ex-band-pane");
|
|
2505
|
+
node.append(headBtn, pane);
|
|
2506
|
+
|
|
2507
|
+
const isOpen = () => node.classList.contains("open");
|
|
2508
|
+
const setOpen = (open) => {
|
|
2509
|
+
node.classList.toggle("open", open);
|
|
2510
|
+
headBtn.setAttribute("aria-expanded", String(open));
|
|
2511
|
+
if (open) EX_OPEN_BAND = raw.band;
|
|
2512
|
+
else if (EX_OPEN_BAND === raw.band) EX_OPEN_BAND = null;
|
|
2513
|
+
pane.replaceChildren(open ? editor() : frag());
|
|
2514
|
+
};
|
|
2515
|
+
|
|
2516
|
+
const target = (p) => (p.via === "extra" ? p.profile + "/" + p.model : p.agent || "—");
|
|
2517
|
+
|
|
2518
|
+
const paint = () => {
|
|
2519
|
+
const p = exPreviewRow(raw, edits);
|
|
2520
|
+
headBtn.replaceChildren();
|
|
2521
|
+
// The proportional bar, INSIDE the row. A CUSTOM PROPERTY and not an inline
|
|
2522
|
+
// width: the panel's CSP is `style-src 'self'` and a style attribute is
|
|
2523
|
+
// blocked outright. The track is full width, so `--w` is honest at every
|
|
2524
|
+
// viewport and no band is ever squeezed to a sliver to make room for text.
|
|
2525
|
+
const track = el("span", "ex-band-track");
|
|
2526
|
+
const bar = el(
|
|
2527
|
+
"span",
|
|
2528
|
+
"ex-band-bar " + (p.via === "extra" ? "ex-band-extra" : "ex-band-claude") + (p.staged ? " ex-band-staged" : "")
|
|
2529
|
+
);
|
|
2530
|
+
bar.style.setProperty("--w", Math.max(0, raw.to - raw.from).toFixed(2) + "%");
|
|
2531
|
+
track.append(bar);
|
|
2532
|
+
headBtn.append(track);
|
|
2533
|
+
const mid = el("span", "ex-band-mid");
|
|
2534
|
+
// The CLI's own `[from,to)` string, brackets included: the half-open edge is
|
|
2535
|
+
// the whole point of the notation. And BESIDE it the CLI's plain reading of
|
|
2536
|
+
// the same range — `range` is computed by `orc extra route`, never written
|
|
2537
|
+
// here. "simple work" beside a score would be the panel deciding what a
|
|
2538
|
+
// score means, which is the Flow-stepper rule.
|
|
2539
|
+
mid.append(el("span", "mono ex-band-label", raw.band));
|
|
2540
|
+
if (raw.range) mid.append(el("span", "note ex-band-range", raw.range));
|
|
2541
|
+
headBtn.append(mid);
|
|
2542
|
+
// THE FULL TARGET, never truncated. It is the most important fact in the row.
|
|
2543
|
+
headBtn.append(el("span", "mono ex-band-target", target(p)));
|
|
2544
|
+
const chips = el("span", "ex-band-chips");
|
|
2545
|
+
if (p.staged) chips.append(chip(t("extra.routing.staged"), "info"));
|
|
2546
|
+
if (p.via === "extra" && p.engine) chips.append(chip(p.engine, "info"));
|
|
2547
|
+
// The CLI's state word, verbatim — never a friendlier synonym. A STAGED row
|
|
2548
|
+
// has no state word yet, because nothing has been written for the CLI to
|
|
2549
|
+
// have an opinion about.
|
|
2550
|
+
if (p.via === "extra" && p.verify_state)
|
|
2551
|
+
chips.append(chip(p.verify_state, p.verify_state === "VERIFIED" ? "ok" : "warn"));
|
|
2552
|
+
if (p.via === "extra" && !p.model_known) chips.append(chip(t("extra.routing.modelGone"), "warn"));
|
|
2553
|
+
if (p.staged && p.via === "claude") chips.append(el("span", "note", t("extra.routing.recomputed")));
|
|
2554
|
+
headBtn.append(chips);
|
|
2555
|
+
headBtn.append(el("span", "ex-band-caret", "▸"));
|
|
2556
|
+
headBtn.title = raw.band + " → " + target(p);
|
|
2557
|
+
if (isOpen()) pane.replaceChildren(editor());
|
|
2558
|
+
};
|
|
2559
|
+
|
|
2560
|
+
const editor = () => {
|
|
2561
|
+
const box = el("div", "stack stack-sm");
|
|
2562
|
+
// WHAT A SCORE IN THIS RANGE DESCRIBES, in the CLI's words.
|
|
2563
|
+
if (raw.meaning) box.append(el("div", "note", raw.meaning));
|
|
2564
|
+
const staged = edits.map.get(key);
|
|
2565
|
+
if (staged) {
|
|
2566
|
+
const line = el("div", "note ex-route-staged");
|
|
2567
|
+
line.append(chip(t("extra.routing.staged"), "info"));
|
|
2568
|
+
line.append(document.createTextNode(" " + staged.value));
|
|
2569
|
+
const undo = el("button", "btn btn-ghost btn-sm", t("extra.routing.undo"));
|
|
2570
|
+
undo.type = "button";
|
|
2571
|
+
undo.addEventListener("click", () => {
|
|
2572
|
+
edits.drop(key);
|
|
2573
|
+
paint();
|
|
2574
|
+
});
|
|
2575
|
+
line.append(undo);
|
|
2576
|
+
box.append(line);
|
|
2577
|
+
return box;
|
|
2578
|
+
}
|
|
2579
|
+
if (!verified) {
|
|
2580
|
+
box.append(el("div", "note", t("extra.routing.lockedHint")));
|
|
2581
|
+
return box;
|
|
2582
|
+
}
|
|
2583
|
+
if (raw.via === "extra") {
|
|
2584
|
+
// The row's own detail, all of it the CLI's. It was never visible before:
|
|
2585
|
+
// a small model and a turn cap decide what a wave actually does.
|
|
2586
|
+
box.append(
|
|
2587
|
+
kvList([
|
|
2588
|
+
[t("extra.routing.kvProfile"), raw.profile],
|
|
2589
|
+
[t("extra.routing.kvModel"), raw.model],
|
|
2590
|
+
[t("extra.routing.kvSmall"), raw.small_model || "—"],
|
|
2591
|
+
[t("extra.routing.kvTurns"), raw.max_turns === null || raw.max_turns === undefined ? "—" : String(raw.max_turns)],
|
|
2592
|
+
])
|
|
2593
|
+
);
|
|
2594
|
+
const clear = el("button", "btn btn-sm", t("extra.routing.clear"));
|
|
2595
|
+
clear.type = "button";
|
|
2596
|
+
clear.addEventListener("click", () => {
|
|
2597
|
+
edits.action(key, "/api/extra/route/rm", { band: raw.from + "-" + raw.to }, t("extra.routing.stagedClear"));
|
|
2598
|
+
paint();
|
|
2599
|
+
});
|
|
2600
|
+
const acts = el("div", "row-actions");
|
|
2601
|
+
acts.append(clear);
|
|
2602
|
+
box.append(acts);
|
|
2603
|
+
return box;
|
|
2604
|
+
}
|
|
2605
|
+
box.append(exRouteControls(raw, d, edits, key, paint));
|
|
2606
|
+
return box;
|
|
2607
|
+
};
|
|
2608
|
+
|
|
2609
|
+
headBtn.addEventListener("click", () => {
|
|
2610
|
+
const next = !isOpen();
|
|
2611
|
+
if (next) openOnly(entry);
|
|
2612
|
+
setOpen(next);
|
|
2613
|
+
});
|
|
2614
|
+
paint();
|
|
2615
|
+
const entry = { node, close: () => isOpen() && setOpen(false) };
|
|
2616
|
+
// A re-render must not close the row you were working in.
|
|
2617
|
+
if (EX_OPEN_BAND === raw.band) setOpen(true);
|
|
2618
|
+
return entry;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
function exRouteControls(row, d, edits, key, paint) {
|
|
2622
|
+
// A Claude band. The control offers every connection ORC knows about; one
|
|
2623
|
+
// that has never answered is DISABLED and labelled, never hidden — the
|
|
2624
|
+
// v0.44.0 rule that a value outside its own set must stay visible and must
|
|
2625
|
+
// not be re-offerable.
|
|
2626
|
+
const profiles = (d.list && d.list.profiles) || [];
|
|
2627
|
+
const sel = exSelect(
|
|
2628
|
+
[{ value: "", label: t("extra.routing.pickProfile") }].concat(
|
|
2629
|
+
profiles.map((p) => ({
|
|
2630
|
+
value: p.name,
|
|
2631
|
+
label: p.name + (p.verified_at ? "" : " " + t("extra.routing.untested")),
|
|
2632
|
+
disabled: !p.verified_at,
|
|
2633
|
+
}))
|
|
2634
|
+
)
|
|
2635
|
+
);
|
|
2636
|
+
// v0.51.0 — WHETHER THIS IS A DROPDOWN OR A TEXT BOX IS THE CLI'S ANSWER.
|
|
2637
|
+
// `orc extra models --json` returns `entry: "list" | "free-text"` plus a
|
|
2638
|
+
// `group` per row, and this renders it and derives nothing — the Flow-stepper
|
|
2639
|
+
// rule (`steps[]`), applied to models. A list that came back empty and the
|
|
2640
|
+
// escape-hatch provider are both free text, because a dropdown that cannot
|
|
2641
|
+
// contain the answer is worse than a box.
|
|
2642
|
+
const box = exModelBox(t("extra.routing.modelPh"));
|
|
2643
|
+
const model = box.input;
|
|
2644
|
+
const fillModels = () => {
|
|
2645
|
+
const p = profiles.find((x) => x.name === sel.value);
|
|
2646
|
+
box.load(p ? p.name : null);
|
|
2647
|
+
};
|
|
2648
|
+
sel.addEventListener("change", fillModels);
|
|
2649
|
+
fillModels();
|
|
2650
|
+
|
|
2651
|
+
const add = el("button", "btn btn-sm", t("extra.routing.route"));
|
|
2652
|
+
add.type = "button";
|
|
2653
|
+
add.addEventListener("click", () => {
|
|
2654
|
+
const chosen = box.value();
|
|
2655
|
+
if (!sel.value || !chosen) return;
|
|
2656
|
+
edits.action(
|
|
2657
|
+
key,
|
|
2658
|
+
"/api/extra/route/set",
|
|
2659
|
+
{ band: row.from + "-" + row.to, target: sel.value + "/" + chosen },
|
|
2660
|
+
t("extra.routing.stagedSet", { target: sel.value + "/" + chosen })
|
|
2661
|
+
);
|
|
2662
|
+
paint();
|
|
2663
|
+
});
|
|
2664
|
+
|
|
2665
|
+
const controls = el("div", "ex-route-controls");
|
|
2666
|
+
controls.append(sel, box.node, add);
|
|
2667
|
+
return controls;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
/* ONE model control, and the CLI decides which shape it takes.
|
|
2671
|
+
|
|
2672
|
+
`entry: "list"` renders a real <select> grouped by the CLI's own `group`, each
|
|
2673
|
+
option labelled `label (group)` from data the panel was HANDED. `free-text`
|
|
2674
|
+
renders the box that was always here. The panel never decides which — and it
|
|
2675
|
+
never composes a group from a string it does not own.
|
|
2676
|
+
|
|
2677
|
+
IT ALSO CARRIES F5's WARNING. A model being LISTED is not a model that WORKS:
|
|
2678
|
+
a listed id can be dead upstream, and only a live call tells those two apart.
|
|
2679
|
+
The caveat is the CLI's sentence, printed beside the picker, and the per-model
|
|
2680
|
+
test is offered next to it. */
|
|
2681
|
+
function exModelBox(placeholder) {
|
|
2682
|
+
const node = el("div", "ex-modelbox");
|
|
2683
|
+
const input = exInput(placeholder);
|
|
2684
|
+
const sel = exSelect([]);
|
|
2685
|
+
const note = el("div", "note");
|
|
2686
|
+
const api = { node, input, profile: null, entry: "free-text", value: () => (api.entry === "list" ? sel.value : input.value.trim()) };
|
|
2687
|
+
const paint = () => {
|
|
2688
|
+
node.replaceChildren(api.entry === "list" ? sel : input, note);
|
|
2689
|
+
};
|
|
2690
|
+
api.load = (profile) => {
|
|
2691
|
+
api.profile = profile;
|
|
2692
|
+
if (!profile) {
|
|
2693
|
+
api.entry = "free-text";
|
|
2694
|
+
note.textContent = "";
|
|
2695
|
+
paint();
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
read("/api/extra/models?profile=" + encodeURIComponent(profile))
|
|
2699
|
+
.then((r) => {
|
|
2700
|
+
const j = r.data || {};
|
|
2701
|
+
api.entry = j.entry === "list" ? "list" : "free-text";
|
|
2702
|
+
sel.replaceChildren();
|
|
2703
|
+
const groups = new Map();
|
|
2704
|
+
for (const m of j.models || []) {
|
|
2705
|
+
const g = m.group || "";
|
|
2706
|
+
if (!groups.has(g)) groups.set(g, []);
|
|
2707
|
+
groups.get(g).push(m);
|
|
2708
|
+
}
|
|
2709
|
+
for (const [g, rows] of groups) {
|
|
2710
|
+
const og = el("optgroup");
|
|
2711
|
+
og.label = g;
|
|
2712
|
+
for (const m of rows) {
|
|
2713
|
+
const o = el("option", null, m.label + (m.group ? " (" + m.group + ")" : ""));
|
|
2714
|
+
o.value = m.id;
|
|
2715
|
+
og.append(o);
|
|
2716
|
+
}
|
|
2717
|
+
sel.append(og);
|
|
2718
|
+
}
|
|
2719
|
+
// The provider's own caveat, verbatim: OFFERED is not WORKING.
|
|
2720
|
+
note.textContent = j.caveat || "";
|
|
2721
|
+
paint();
|
|
2722
|
+
})
|
|
2723
|
+
.catch(() => {
|
|
2724
|
+
api.entry = "free-text";
|
|
2725
|
+
paint();
|
|
2726
|
+
});
|
|
2727
|
+
};
|
|
2728
|
+
paint();
|
|
2729
|
+
return api;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
// Reset here is the GUARDRAILS, not the routes: `orc config reset <key>` removes
|
|
2733
|
+
// a key from the file, and doing that to nine keys is a real write with a real
|
|
2734
|
+
// effect. It deliberately does NOT touch the routing table — dropping every
|
|
2735
|
+
// route row is a change to how this repo builds, and the CLI makes that an
|
|
2736
|
+
// explicit act with a recorded reason for exactly that reason.
|
|
2737
|
+
function exResetModal(edits, body) {
|
|
2738
|
+
const keys = EX_CONFIG_KEYS.slice();
|
|
2739
|
+
const box = el("div", "stack stack-sm");
|
|
2740
|
+
box.append(el("div", null, t("extra.reset.body")));
|
|
2741
|
+
const list = el("div", "file-list");
|
|
2742
|
+
// The keys are NAMED. A count is not consent.
|
|
2743
|
+
for (const k of keys) list.append(el("div", null, k));
|
|
2744
|
+
box.append(list);
|
|
2745
|
+
box.append(el("div", "action-cmd", "orc config reset <key>"));
|
|
2746
|
+
box.append(el("div", "note", t("extra.reset.note")));
|
|
2747
|
+
modal({
|
|
2748
|
+
title: t("extra.reset.title"),
|
|
2749
|
+
body: box,
|
|
2750
|
+
actions: [
|
|
2751
|
+
{ label: t("common.cancel"), onClick: (c) => c() },
|
|
2752
|
+
{
|
|
2753
|
+
label: t("extra.reset.apply"),
|
|
2754
|
+
cls: "btn-danger",
|
|
2755
|
+
onClick: async (close) => {
|
|
2756
|
+
close();
|
|
2757
|
+
edits.clear();
|
|
2758
|
+
const failed = [];
|
|
2759
|
+
for (const key of keys) {
|
|
2760
|
+
try {
|
|
2761
|
+
const r = await post("/api/config/reset", { key });
|
|
2762
|
+
if (!r.ok) failed.push(key);
|
|
2763
|
+
} catch (_) {
|
|
2764
|
+
failed.push(key);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
if (failed.length) toast(t("edits.someFailed", { n: failed.length }), "bad", failed.join(String.fromCharCode(10)));
|
|
2768
|
+
else toast(t("extra.reset.done", { n: keys.length }), "ok");
|
|
2769
|
+
renderExtra(body);
|
|
2770
|
+
},
|
|
2771
|
+
},
|
|
2772
|
+
],
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
// The keys this panel owns, filled from `orc config list --json` on every
|
|
2777
|
+
// render. The panel never hard-codes a key name — it reads which ones exist.
|
|
2778
|
+
let EX_CONFIG_KEYS = [];
|
|
2779
|
+
|
|
2780
|
+
/* ================================== the guardrails and the cost (W12 T2) ==
|
|
2781
|
+
ZERO PANEL-SIDE WORK FOR A CONFIG KEY. The nine `extra_*` keys render through
|
|
2782
|
+
the SAME `settingRow` / `controlFor` the Settings panel uses, so the control
|
|
2783
|
+
follows the CLI's validator, the shadow lock and its reason come for free, and
|
|
2784
|
+
adding a tenth key is still zero steps here. The only adapter is the edit set:
|
|
2785
|
+
this panel stages everything as an ACTION, so one bar carries a routing change
|
|
2786
|
+
and a guardrail change together.
|
|
2787
|
+
|
|
2788
|
+
THE COST CARD RENDERS `orc extra stats --json` AND COMPUTES NOTHING. Four
|
|
2789
|
+
token kinds, never blended (`docTokenBar`, the same renderer the Docs panel
|
|
2790
|
+
uses — one idea of a token vector). A band nothing joins reads `—`, never `0`,
|
|
2791
|
+
and KEEPS ITS SLOT: "this band cost nothing" and "nothing has run here yet"
|
|
2792
|
+
are different facts. `usd` is null unless ORC priced it itself. */
|
|
2793
|
+
|
|
2794
|
+
// `settingRow`/`controlFor` call `edits.set(key, value, original)` and
|
|
2795
|
+
// `edits.reset(key)`. This translates both into ACTIONS on the panel's one edit
|
|
2796
|
+
// set, which is what lets `applyActions` stay untouched.
|
|
2797
|
+
function exConfigEdits(edits) {
|
|
2798
|
+
return {
|
|
2799
|
+
map: edits.map,
|
|
2800
|
+
get size() {
|
|
2801
|
+
return edits.size;
|
|
2802
|
+
},
|
|
2803
|
+
set(key, value, original) {
|
|
2804
|
+
// Staging a value back to what it already was CLEARS the edit rather than
|
|
2805
|
+
// recording a no-op — the v0.44.1 rule, and it has to be re-stated here
|
|
2806
|
+
// because this adapter is what `controlFor` now talks to.
|
|
2807
|
+
if (String(value) === String(original)) edits.drop(key);
|
|
2808
|
+
else edits.action(key, "/api/config/set", { key, value: String(value) }, String(value));
|
|
2809
|
+
},
|
|
2810
|
+
reset(key) {
|
|
2811
|
+
// `orc config reset <key>` REMOVES the key from the file, which is not the
|
|
2812
|
+
// same write as setting it to its default value.
|
|
2813
|
+
edits.action(key, "/api/config/reset", { key }, t("edits.toDefault"));
|
|
2814
|
+
},
|
|
2815
|
+
drop: (key) => edits.drop(key),
|
|
2816
|
+
clear: () => edits.clear(),
|
|
2817
|
+
entries: () => edits.entries(),
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
function exGuardrailsCard(d, body, edits) {
|
|
2822
|
+
const c = card(t("extra.guard.title"));
|
|
2823
|
+
const keys = ((d.config && d.config.keys) || []).filter((k) => k.key.indexOf("extra_") === 0);
|
|
2824
|
+
if (!keys.length) {
|
|
2825
|
+
c.append(empty(t("extra.guard.none")));
|
|
2826
|
+
return c;
|
|
2827
|
+
}
|
|
2828
|
+
c.append(el("div", "note", t("extra.guard.sub")));
|
|
2829
|
+
c.append(exWhy(t("extra.guard.subWhy")));
|
|
2830
|
+
const proxy = exConfigEdits(edits);
|
|
2831
|
+
const rows = el("div", "settings-list");
|
|
2832
|
+
for (const k of keys) rows.append(settingRow(k, body, proxy));
|
|
2833
|
+
c.append(rows);
|
|
2834
|
+
return c;
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
/* ------------------------------------------------------------- the cost -- */
|
|
2838
|
+
|
|
2839
|
+
function exCostCard(d) {
|
|
2840
|
+
const c = card(t("extra.cost.title"));
|
|
2841
|
+
const st = d.stats;
|
|
2842
|
+
if (!st) {
|
|
2843
|
+
c.append(empty(t("extra.cost.none"), t("extra.cost.noneHint")));
|
|
2844
|
+
return c;
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
const head = el("div", "row-actions");
|
|
2848
|
+
head.append(chip(t("extra.cost.dispatches", { n: st.dispatches }), st.dispatches ? "info" : "idle"));
|
|
2849
|
+
head.append(el("span", "note", t("extra.cost.scanned", { n: st.files_scanned })));
|
|
2850
|
+
if (st.since) head.append(el("span", "note", st.since));
|
|
2851
|
+
c.append(head);
|
|
2852
|
+
|
|
2853
|
+
// WHERE THE NUMBERS CAME FROM, and the panel decides none of it: the three
|
|
2854
|
+
// counts are the CLI's own `sources` object. "ORC wrote this down itself" and
|
|
2855
|
+
// "a trace happened to mention it" are different levels of confidence in the
|
|
2856
|
+
// same total, and a reader who cannot tell them apart cannot tell a broken
|
|
2857
|
+
// relay from a lane that never ran — which is the exact failure that had this
|
|
2858
|
+
// card reading `0 tasks sent` while two dispatches had really been paid for.
|
|
2859
|
+
const src = st.sources;
|
|
2860
|
+
if (src) {
|
|
2861
|
+
c.append(
|
|
2862
|
+
el(
|
|
2863
|
+
"div",
|
|
2864
|
+
"note",
|
|
2865
|
+
t("extra.cost.sources", {
|
|
2866
|
+
log: src.spend_log,
|
|
2867
|
+
traces: src.traces_only,
|
|
2868
|
+
backfill: src.run_returns,
|
|
2869
|
+
})
|
|
2870
|
+
)
|
|
2871
|
+
);
|
|
2872
|
+
c.append(exWhy(t("extra.cost.sourcesWhy")));
|
|
2873
|
+
// Both of these are ABSENT counts — rows that exist and are not in the
|
|
2874
|
+
// totals. Neither is chrome: a silently short report is the thing this
|
|
2875
|
+
// whole card is being fixed for.
|
|
2876
|
+
if (src.unreadable_spend_lines)
|
|
2877
|
+
c.append(el("div", "note bad", t("extra.cost.unreadable", { n: src.unreadable_spend_lines })));
|
|
2878
|
+
if (src.run_returns_undated_skipped)
|
|
2879
|
+
c.append(el("div", "note bad", t("extra.cost.undated", { n: src.run_returns_undated_skipped })));
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
// v0.54.0 — RELIABILITY BESIDE COST. Whether a profile finishes what it was
|
|
2883
|
+
// given is the other half of what it costs, and it belongs where the user is
|
|
2884
|
+
// choosing one.
|
|
2885
|
+
if (st.reliability) c.append(exReliabilityStrip(st.reliability));
|
|
2886
|
+
|
|
2887
|
+
// The price table's own dating, and its own staleness word. No dollar figure
|
|
2888
|
+
// exists without one, and a stale table says so — a cost ORC did not price is
|
|
2889
|
+
// never printed as a number.
|
|
2890
|
+
if (st.price_table)
|
|
2891
|
+
c.append(
|
|
2892
|
+
el(
|
|
2893
|
+
"div",
|
|
2894
|
+
"note" + (st.price_table.stale ? " bad" : ""),
|
|
2895
|
+
t("extra.cost.priceTable", { as_of: st.price_table.as_of, days: st.price_table.age_days })
|
|
2896
|
+
)
|
|
2897
|
+
);
|
|
2898
|
+
|
|
2899
|
+
if (!st.dispatches) {
|
|
2900
|
+
// The CLI's own hint, verbatim: it names the next command.
|
|
2901
|
+
c.append(empty(t("extra.cost.none"), st.hint));
|
|
2902
|
+
return c;
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
// EVERY ROUTED BAND KEEPS ITS SLOT, joined by profile+band. One that nothing
|
|
2906
|
+
// has run through yet reads `—` in every column, never `0`.
|
|
2907
|
+
const byKey = new Map();
|
|
2908
|
+
for (const b of st.bands || []) byKey.set(b.profile + "|" + b.band, b);
|
|
2909
|
+
const slots = [];
|
|
2910
|
+
for (const r of ((d.route && d.route.rows) || []).filter((x) => x.via === "extra")) {
|
|
2911
|
+
const key = r.profile + "|" + r.band;
|
|
2912
|
+
slots.push({ profile: r.profile, band: r.band, stat: byKey.get(key) || null });
|
|
2913
|
+
byKey.delete(key);
|
|
2914
|
+
}
|
|
2915
|
+
// …and a band the traces know about that is no longer routed keeps its slot
|
|
2916
|
+
// too, because that history is exactly what tells you whether unrouting it was
|
|
2917
|
+
// the right call.
|
|
2918
|
+
for (const b of byKey.values()) slots.push({ profile: b.profile, band: b.band, stat: b, unrouted: true });
|
|
2919
|
+
|
|
2920
|
+
for (const s of slots) c.append(exCostRow(s));
|
|
2921
|
+
|
|
2922
|
+
exCostList(c, t("extra.cost.substitutions"), st.substitutions, (x) => `${x.task} ${x.requested} → ${x.reported}`);
|
|
2923
|
+
exCostList(c, t("extra.cost.reroutes"), st.reroutes, (x) => `${x.task} ${(x.providers || []).join(" → ")}`);
|
|
2924
|
+
exCostList(c, t("extra.cost.fallbacks"), st.fallbacks, (x) => `${x.task} ${x.reason} → ${x.agent}`);
|
|
2925
|
+
|
|
2926
|
+
if ((st.missing_rates || []).length) c.append(exRatesBox(d, st));
|
|
2927
|
+
return c;
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
function exCostRow(s) {
|
|
2931
|
+
const row = el("div", "ex-cost-row");
|
|
2932
|
+
const head = el("div", "row-actions");
|
|
2933
|
+
head.append(el("span", "mono ex-name", s.profile));
|
|
2934
|
+
head.append(el("span", "mono ex-route-band", s.band));
|
|
2935
|
+
if (s.unrouted) head.append(chip(t("extra.cost.unrouted"), "idle"));
|
|
2936
|
+
const g = s.stat;
|
|
2937
|
+
head.append(el("span", "note", g ? t("extra.cost.nDispatches", { n: g.dispatches }) : "—"));
|
|
2938
|
+
row.append(head);
|
|
2939
|
+
|
|
2940
|
+
if (!g) {
|
|
2941
|
+
row.append(el("div", "note", t("extra.cost.never")));
|
|
2942
|
+
return row;
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
const oc = el("div", "row-actions");
|
|
2946
|
+
// The outcome names are the CLI's own field names — never translated.
|
|
2947
|
+
for (const [k, n] of Object.entries(g.outcomes || {}))
|
|
2948
|
+
if (n) oc.append(chip(k + " " + n, k === "done" ? "ok" : k === "failed" ? "bad" : "warn"));
|
|
2949
|
+
for (const e of Object.keys(g.engines || {})) oc.append(chip(e, "info"));
|
|
2950
|
+
row.append(oc);
|
|
2951
|
+
|
|
2952
|
+
// The four kinds, never blended, through the SAME renderer the Docs panel
|
|
2953
|
+
// uses. `cache_read` is usually the largest count and about a tenth of the
|
|
2954
|
+
// price, which is exactly why one blended number would mislead.
|
|
2955
|
+
row.append(docTokenBar(g.usage));
|
|
2956
|
+
|
|
2957
|
+
// THE COUNT THAT KEEPS THE VECTOR HONEST. A total assembled from six of ten
|
|
2958
|
+
// dispatches is not this band's cost, and it says so rather than averaging
|
|
2959
|
+
// the gap away.
|
|
2960
|
+
if (g.usage_reported !== g.dispatches)
|
|
2961
|
+
row.append(
|
|
2962
|
+
el("div", "note bad", t("extra.cost.partial", { got: g.usage_reported, of: g.dispatches, missing: g.usage_missing }))
|
|
2963
|
+
);
|
|
2964
|
+
|
|
2965
|
+
row.append(
|
|
2966
|
+
el(
|
|
2967
|
+
"div",
|
|
2968
|
+
"note",
|
|
2969
|
+
g.usd === null
|
|
2970
|
+
? t("extra.cost.noRate", { provider: g.provider || "—" })
|
|
2971
|
+
: t("extra.cost.usd", { usd: g.usd < 0.01 ? "<0.01" : g.usd.toFixed(2) })
|
|
2972
|
+
)
|
|
2973
|
+
);
|
|
2974
|
+
return row;
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
function exCostList(c, title, arr, fmt) {
|
|
2978
|
+
if (!arr || !arr.length) return;
|
|
2979
|
+
const box = el("div", "ex-cost-list");
|
|
2980
|
+
const h = el("div", "row-actions");
|
|
2981
|
+
h.append(chip(String(arr.length), "warn"));
|
|
2982
|
+
h.append(el("span", "ex-probe-head", title));
|
|
2983
|
+
box.append(h);
|
|
2984
|
+
for (const x of arr.slice(0, 8)) box.append(el("div", "note mono", fmt(x)));
|
|
2985
|
+
if (arr.length > 8) box.append(el("div", "note", t("extra.cost.more", { n: arr.length - 8 })));
|
|
2986
|
+
c.append(box);
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
// The paste path. `bin/pricing.json` ships every models map EMPTY on purpose —
|
|
2990
|
+
// a figure that is wrong by 2x gets believed — so this is the JSON the CLI built
|
|
2991
|
+
// for the pairs it actually saw, and the note about where to put it is the
|
|
2992
|
+
// CLI's own sentence.
|
|
2993
|
+
function exRatesBox(d, st) {
|
|
2994
|
+
const box = el("div", "ex-rates");
|
|
2995
|
+
box.append(el("div", "ex-probe-head", t("extra.cost.missingRates")));
|
|
2996
|
+
for (const m of st.missing_rates) box.append(el("div", "note mono", m.pair + " · " + t("extra.cost.nDispatches", { n: m.dispatches })));
|
|
2997
|
+
const rates = d.rates;
|
|
2998
|
+
if (rates && rates.paste) {
|
|
2999
|
+
const pre = el("pre", "block wrap", JSON.stringify(rates.paste, null, 2));
|
|
3000
|
+
box.append(pre);
|
|
3001
|
+
const b = el("button", "btn btn-ghost btn-sm", t("common.copy"));
|
|
3002
|
+
b.type = "button";
|
|
3003
|
+
b.addEventListener("click", () => copy(JSON.stringify(rates.paste, null, 2), t("extra.cost.missingRates")));
|
|
3004
|
+
box.append(b);
|
|
3005
|
+
if (rates.where) box.append(el("div", "note", rates.where));
|
|
3006
|
+
for (const c of rates.caveats || []) box.append(el("div", "note", c.provider + ": " + c.caveat));
|
|
3007
|
+
} else {
|
|
3008
|
+
box.append(laneCommand("orc extra rates", t("extra.cost.ratesWhy")));
|
|
3009
|
+
}
|
|
3010
|
+
return box;
|
|
3011
|
+
}
|