@geraldmaron/construct 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -19
- package/bin/construct +311 -81
- package/bin/construct-postinstall.mjs +30 -2
- package/examples/distribution/README.md +42 -0
- package/examples/distribution/manifest.json +53 -0
- package/examples/distribution/sources/adr.md +84 -0
- package/examples/distribution/sources/deck-one-pager.md +65 -0
- package/examples/distribution/sources/prd-platform.md +161 -0
- package/examples/distribution/sources/research-brief.md +80 -0
- package/examples/distribution/sources/rfc-platform.md +82 -0
- package/examples/distribution/sources/runbook.md +103 -0
- package/examples/distribution/sources/strategy.md +88 -0
- package/lib/adapters-sync.mjs +67 -0
- package/lib/artifact-gate-notice.mjs +38 -0
- package/lib/artifact-manifest.mjs +112 -0
- package/lib/artifact-release-gate.mjs +171 -0
- package/lib/artifact-reviewers.mjs +68 -0
- package/lib/artifact-type-from-path.mjs +79 -0
- package/lib/audit-skills.mjs +4 -0
- package/lib/audit-specialists.mjs +285 -0
- package/lib/audit-trail.mjs +1 -1
- package/lib/auto-docs.mjs +87 -18
- package/lib/beads/drift.mjs +27 -6
- package/lib/beads-client.mjs +49 -121
- package/lib/beads-optimistic.mjs +5 -12
- package/lib/brand-fonts.mjs +93 -0
- package/lib/brand-prose.mjs +214 -0
- package/lib/brand-tokens.mjs +92 -0
- package/lib/bridges/copilot-proxy.mjs +13 -26
- package/lib/capability-ledger.mjs +156 -0
- package/lib/certification/artifact-fixtures.mjs +132 -0
- package/lib/certification/artifact-gates.mjs +63 -0
- package/lib/certification/artifact-provenance.mjs +97 -0
- package/lib/certification/canonical-scenarios.mjs +78 -0
- package/lib/certification/cli.mjs +191 -0
- package/lib/certification/dashboard-api.mjs +71 -0
- package/lib/certification/demo-parity.mjs +116 -0
- package/lib/certification/document-io-fixtures.mjs +246 -0
- package/lib/certification/document-workflow.mjs +97 -0
- package/lib/certification/eval-bridge.mjs +77 -0
- package/lib/certification/model-routing.mjs +149 -0
- package/lib/certification/prompt-budget.mjs +119 -0
- package/lib/certification/rc-gate.mjs +206 -0
- package/lib/certification/real-llm-scenarios.mjs +303 -0
- package/lib/certification/role-cards.mjs +113 -0
- package/lib/certification/role-overlays.mjs +117 -0
- package/lib/certification/run.mjs +122 -0
- package/lib/certification/runner.mjs +323 -0
- package/lib/certification/scenarios.mjs +51 -0
- package/lib/certification/skill-inventory.mjs +289 -0
- package/lib/certification/skill-scenarios.mjs +147 -0
- package/lib/certification/specialist-contracts.mjs +85 -0
- package/lib/certification/specialist-scenarios.mjs +175 -0
- package/lib/certification/stale-impact.mjs +146 -0
- package/lib/certification/status.mjs +252 -0
- package/lib/certification/store.mjs +77 -0
- package/lib/chat/cli.mjs +333 -0
- package/lib/chat/command-suggest.mjs +161 -0
- package/lib/chat/commands.mjs +215 -0
- package/lib/chat/config.mjs +142 -0
- package/lib/chat/context-compactor.mjs +250 -0
- package/lib/chat/context-continuation.mjs +253 -0
- package/lib/chat/continuation-source.mjs +58 -0
- package/lib/chat/demo-guide.mjs +61 -0
- package/lib/chat/design-tokens.mjs +91 -0
- package/lib/chat/desktop-binary.mjs +79 -0
- package/lib/chat/desktop-build.mjs +130 -0
- package/lib/chat/desktop-launcher.mjs +133 -0
- package/lib/chat/evidence.mjs +145 -0
- package/lib/chat/export.mjs +74 -0
- package/lib/chat/harness/driver.mjs +91 -0
- package/lib/chat/list-picker.mjs +112 -0
- package/lib/chat/model-picker.mjs +356 -0
- package/lib/chat/openrouter-fallback.mjs +151 -0
- package/lib/chat/permission-prompt.mjs +33 -0
- package/lib/chat/picker-catalog.mjs +45 -0
- package/lib/chat/policy-telemetry.mjs +34 -0
- package/lib/chat/present.mjs +245 -0
- package/lib/chat/session-context.mjs +39 -0
- package/lib/chat/session-persist.mjs +73 -0
- package/lib/chat/session-restore.mjs +71 -0
- package/lib/chat/session-settings.mjs +53 -0
- package/lib/chat/system-prompt.mjs +52 -0
- package/lib/chat/transparency.mjs +93 -0
- package/lib/chat/tui/color-scheme.mjs +42 -0
- package/lib/chat/tui/markdown.mjs +123 -0
- package/lib/chat/tui/presentation.mjs +100 -0
- package/lib/chat/tui/render.mjs +500 -0
- package/lib/chat/tui/turn-block.mjs +284 -0
- package/lib/chat/tui/turn-present.mjs +18 -0
- package/lib/chat/tui/turn-state.mjs +88 -0
- package/lib/chat/tui/usage.mjs +122 -0
- package/lib/chat/web-commands.mjs +146 -0
- package/lib/chat/web-launcher.mjs +63 -0
- package/lib/chat/web-picker-keys.mjs +46 -0
- package/lib/chat/web-session.mjs +159 -0
- package/lib/cli-commands.mjs +129 -15
- package/lib/comment-lint.mjs +6 -4
- package/lib/config/schema.mjs +36 -5
- package/lib/contract-schemas/decision.json +50 -0
- package/lib/contract-schemas/implementation.json +51 -0
- package/lib/contract-schemas/review-report.json +32 -0
- package/lib/contract-schemas/test-report.json +43 -0
- package/lib/contracts/construct-handoff.mjs +60 -0
- package/lib/contracts/validate.mjs +32 -3
- package/lib/contracts/violation-log.mjs +58 -5
- package/lib/dashboard-demo.mjs +71 -0
- package/lib/dashboard-static.mjs +19 -5
- package/lib/decisions/registry.mjs +5 -3
- package/lib/deck-export-pptx.mjs +1152 -0
- package/lib/demo-recording.mjs +142 -0
- package/lib/demo-script.mjs +114 -0
- package/lib/demo-surface.mjs +249 -0
- package/lib/demo.mjs +598 -140
- package/lib/diagram-export.mjs +192 -0
- package/lib/diagram.mjs +13 -11
- package/lib/docs-verify.mjs +25 -7
- package/lib/doctor/index.mjs +3 -1
- package/lib/doctor/project-adapters.mjs +44 -0
- package/lib/doctor/watchers/cx-budget.mjs +98 -0
- package/lib/doctor/watchers/graph-staleness.mjs +52 -0
- package/lib/doctor/watchers/process-pressure.mjs +4 -3
- package/lib/document-export.mjs +360 -36
- package/lib/document-extract.mjs +44 -4
- package/lib/document-ingest.mjs +29 -20
- package/lib/embed/cli.mjs +2 -2
- package/lib/embed/daemon.mjs +18 -46
- package/lib/embed/worker.mjs +2 -2
- package/lib/embedded-contract/model-resolve.mjs +10 -9
- package/lib/env-config.mjs +26 -0
- package/lib/evals/dataset.mjs +137 -0
- package/lib/evals/gates.mjs +175 -0
- package/lib/graph/build-co-change.mjs +54 -0
- package/lib/graph/build-from-registry.mjs +136 -0
- package/lib/graph/build-import-graph.mjs +153 -0
- package/lib/graph/cli.mjs +110 -0
- package/lib/graph/impact-cli.mjs +89 -0
- package/lib/graph/impact.mjs +108 -0
- package/lib/graph/staleness.mjs +44 -0
- package/lib/graph/store.mjs +172 -0
- package/lib/health-check.mjs +90 -76
- package/lib/hooks/artifact-release-gate.mjs +43 -0
- package/lib/hooks/brand-prose-lint.mjs +38 -0
- package/lib/hooks/graph-impact-advisory.mjs +62 -0
- package/lib/hooks/session-optimize.mjs +84 -207
- package/lib/hooks/session-start.mjs +4 -5
- package/lib/host-disposition.mjs +7 -0
- package/lib/improvement/cli.mjs +189 -0
- package/lib/improvement/controller.mjs +137 -0
- package/lib/improvement/proposal.mjs +120 -0
- package/lib/improvement/specialist-loop.mjs +192 -0
- package/lib/improvement/store.mjs +89 -0
- package/lib/improvement/surface.mjs +219 -0
- package/lib/ingest-tooling.mjs +97 -0
- package/lib/init/doc-lanes.mjs +165 -0
- package/lib/init-docs.mjs +31 -178
- package/lib/init-unified.mjs +27 -97
- package/lib/init-update-guide.mjs +102 -0
- package/lib/init-update.mjs +32 -1
- package/lib/init.mjs +8 -0
- package/lib/install/desktop-binary-download.mjs +85 -0
- package/lib/install/legacy-global-cleanup.mjs +189 -0
- package/lib/intake/daemon.mjs +2 -0
- package/lib/intake/git-queue.mjs +9 -8
- package/lib/intake/queue.mjs +2 -1
- package/lib/intake/session-prelude.mjs +90 -1
- package/lib/libreoffice-export.mjs +97 -0
- package/lib/logging/rotate.mjs +9 -0
- package/lib/maintenance/docker-reclaim.mjs +206 -0
- package/lib/mcp/external-schema-cost.mjs +74 -0
- package/lib/mcp/server.mjs +84 -9
- package/lib/mcp/stdio-mcp-probe.mjs +188 -0
- package/lib/mcp/tool-budget.mjs +55 -4
- package/lib/mcp/tools/document.mjs +18 -4
- package/lib/mcp/tools/skills.mjs +32 -3
- package/lib/mcp/tools/workflow.mjs +13 -1
- package/lib/model-free-selector.mjs +18 -0
- package/lib/model-registry.mjs +13 -229
- package/lib/model-router.mjs +373 -92
- package/lib/models/behavior-matrix.mjs +289 -0
- package/lib/models/catalog.mjs +209 -0
- package/lib/models/execution-capability-profile.mjs +196 -0
- package/lib/models/execution-policy.mjs +307 -0
- package/lib/models/provider-poll.mjs +383 -0
- package/lib/npm-spawn-env.mjs +17 -0
- package/lib/ollama/installed-models.mjs +129 -0
- package/lib/oracle/actions.mjs +187 -0
- package/lib/oracle/artifact-gate.mjs +99 -0
- package/lib/oracle/cli.mjs +204 -0
- package/lib/oracle/daemon-entry.mjs +14 -0
- package/lib/oracle/dispatch.mjs +81 -0
- package/lib/oracle/execute.mjs +143 -0
- package/lib/oracle/gaps.mjs +76 -0
- package/lib/oracle/index.mjs +84 -0
- package/lib/oracle/issues.mjs +164 -0
- package/lib/oracle/org-graph.mjs +170 -0
- package/lib/oracle/policy.mjs +80 -0
- package/lib/oracle/read-model.mjs +398 -0
- package/lib/oracle/reconcile.mjs +191 -0
- package/lib/oracle/routing.mjs +89 -0
- package/lib/oracle/synthesize.mjs +384 -0
- package/lib/oracle/verdicts.mjs +51 -0
- package/lib/orchestration/worker.mjs +88 -27
- package/lib/orchestration-policy.mjs +50 -0
- package/lib/parity.mjs +96 -2
- package/lib/playwright-demo.mjs +252 -0
- package/lib/project-init-shared.mjs +4 -1
- package/lib/prompt-composer.js +7 -3
- package/lib/prompt-validation-contract.mjs +24 -0
- package/lib/provider-capabilities.js +57 -11
- package/lib/providers/contract/adapters/confluence/index.mjs +181 -0
- package/lib/providers/contract/adapters/git/index.mjs +115 -0
- package/lib/providers/contract/adapters/github/index.mjs +166 -0
- package/lib/providers/contract/adapters/jira/index.mjs +187 -0
- package/lib/providers/contract/adapters/slack/index.mjs +175 -0
- package/lib/providers/contract/contract-tests.mjs +57 -0
- package/lib/providers/contract/errors.mjs +48 -0
- package/lib/providers/contract/interface.mjs +50 -0
- package/lib/providers/contract/registry.mjs +102 -0
- package/lib/providers/copilot-auth.mjs +297 -0
- package/lib/providers/credential-bootstrap.mjs +178 -0
- package/lib/providers/credential-catalog.mjs +46 -0
- package/lib/providers/credential-sources.mjs +63 -0
- package/lib/providers/creds.mjs +5 -2
- package/lib/providers/op-run.mjs +59 -0
- package/lib/providers/secret-resolver.mjs +159 -0
- package/lib/publish-template.mjs +163 -0
- package/lib/publish-tooling.mjs +119 -0
- package/lib/publish.mjs +305 -0
- package/lib/registry/cli.mjs +82 -0
- package/lib/registry/consolidation.mjs +147 -0
- package/lib/registry/generate-docs.mjs +75 -0
- package/lib/registry/skill-verification.mjs +57 -0
- package/lib/registry/surface-map.mjs +76 -0
- package/lib/registry/validate.mjs +135 -0
- package/lib/resources/budget.mjs +82 -0
- package/lib/resources/process-budget.mjs +45 -0
- package/lib/rules-delivery.mjs +9 -2
- package/lib/rules-read.mjs +26 -0
- package/lib/runtime-env.mjs +23 -0
- package/lib/runtime-pressure.mjs +51 -7
- package/lib/schema-infer.mjs +13 -2
- package/lib/server/chat-loop.mjs +622 -0
- package/lib/server/demo-preview.mjs +63 -0
- package/lib/server/index.mjs +228 -7
- package/lib/service-manager.mjs +60 -8
- package/lib/setup.mjs +70 -7
- package/lib/specialists/prompt-schema.mjs +19 -10
- package/lib/specialists/roster.mjs +43 -0
- package/lib/specialists/scaffold.mjs +56 -0
- package/lib/storage/backend.mjs +6 -6
- package/lib/storage/hybrid-query.mjs +7 -4
- package/lib/storage/state-source.mjs +5 -5
- package/lib/storage/sync.mjs +2 -3
- package/lib/telemetry/rule-calls.mjs +52 -0
- package/lib/template-registry.mjs +2 -2
- package/lib/templates/visual-requirements.mjs +27 -51
- package/lib/test-corpus-inventory.mjs +313 -0
- package/lib/uninstall/uninstall.mjs +19 -7
- package/lib/update.mjs +12 -0
- package/lib/upgrade.mjs +14 -0
- package/lib/wireframe.mjs +20 -14
- package/lib/worker/run.mjs +17 -4
- package/lib/worker/trace.mjs +11 -3
- package/package.json +28 -14
- package/personas/construct.md +2 -2
- package/platforms/claude/settings.template.json +36 -0
- package/rules/common/patterns.md +1 -1
- package/rules/common/release-gates.md +4 -3
- package/scripts/sync-specialists.mjs +41 -28
- package/skills/devops/data-engineering.md +1 -1
- package/skills/docs/adr-workflow.md +1 -0
- package/skills/docs/backlog-proposal-workflow.md +1 -0
- package/skills/docs/codebase-research-workflow.md +40 -0
- package/skills/docs/customer-profile-workflow.md +1 -0
- package/skills/docs/document-ingest-workflow.md +1 -0
- package/skills/docs/evidence-ingest-workflow.md +1 -0
- package/skills/docs/init-docs.md +2 -2
- package/skills/docs/init-project.md +7 -2
- package/skills/docs/prd-workflow.md +24 -1
- package/skills/docs/prfaq-workflow.md +1 -0
- package/skills/docs/product-intelligence-workflow.md +1 -0
- package/skills/docs/product-signal-workflow.md +1 -0
- package/skills/docs/research-workflow.md +54 -37
- package/skills/docs/runbook-workflow.md +1 -0
- package/skills/docs/strategy-workflow.md +1 -0
- package/skills/docs/user-research-workflow.md +40 -0
- package/skills/operating/orchestration-reference.md +1 -1
- package/skills/roles/architect.md +5 -0
- package/skills/roles/operator.docs.md +4 -0
- package/skills/routing.md +4 -2
- package/specialists/artifact-manifest.json +480 -0
- package/specialists/artifact-manifest.schema.json +53 -0
- package/specialists/audit-enrichments.json +454 -0
- package/specialists/contracts.json +37 -25
- package/specialists/prompts/_shared/validation-contract.md +26 -0
- package/specialists/prompts/cx-accessibility.md +21 -2
- package/specialists/prompts/cx-ai-engineer.md +24 -1
- package/specialists/prompts/cx-architect.md +4 -1
- package/specialists/prompts/cx-business-strategist.md +23 -2
- package/specialists/prompts/cx-data-analyst.md +22 -1
- package/specialists/prompts/cx-data-engineer.md +23 -2
- package/specialists/prompts/cx-debugger.md +21 -2
- package/specialists/prompts/cx-designer.md +26 -3
- package/specialists/prompts/cx-devil-advocate.md +19 -2
- package/specialists/prompts/cx-docs-keeper.md +28 -1
- package/specialists/prompts/cx-engineer.md +22 -1
- package/specialists/prompts/cx-evaluator.md +18 -1
- package/specialists/prompts/cx-explorer.md +21 -2
- package/specialists/prompts/cx-legal-compliance.md +21 -2
- package/specialists/prompts/cx-operations.md +21 -2
- package/specialists/prompts/cx-oracle.md +94 -0
- package/specialists/prompts/cx-orchestrator.md +20 -1
- package/specialists/prompts/cx-platform-engineer.md +25 -2
- package/specialists/prompts/cx-product-manager.md +32 -1
- package/specialists/prompts/cx-qa.md +24 -2
- package/specialists/prompts/cx-rd-lead.md +23 -2
- package/specialists/prompts/cx-release-manager.md +23 -2
- package/specialists/prompts/cx-researcher.md +22 -2
- package/specialists/prompts/cx-reviewer.md +18 -1
- package/specialists/prompts/cx-security.md +22 -2
- package/specialists/prompts/cx-sre.md +30 -3
- package/specialists/prompts/cx-test-automation.md +7 -1
- package/specialists/prompts/cx-trace-reviewer.md +22 -3
- package/specialists/prompts/cx-ux-researcher.md +21 -2
- package/specialists/registry.json +52 -173
- package/specialists/tone-profiles.json +42 -0
- package/templates/demos/playwright/demo-recording.config.mjs +47 -0
- package/templates/demos/recordings/agentic-platforms-prd.json +19 -0
- package/templates/demos/scripts/agentic-platforms-prd.json +44 -0
- package/templates/demos/specs/_helpers/scroll-artifact.ts +89 -0
- package/templates/demos/tapes/agentic-platforms-prd.tape +49 -0
- package/templates/demos/tapes/resource-guard-rails.tape +49 -0
- package/templates/demos/vhs/construct-cockpit.json +24 -0
- package/templates/distribution/construct-brand.typ +446 -0
- package/templates/distribution/construct-decision.typ +38 -0
- package/templates/distribution/construct-deck.html +95 -0
- package/templates/distribution/construct-pdf.typ +38 -0
- package/templates/distribution/construct-prd.typ +38 -0
- package/templates/distribution/construct-research.typ +38 -0
- package/templates/distribution/construct-web.html +92 -0
- package/templates/distribution/fonts/Geist-Bold.ttf +0 -0
- package/templates/distribution/fonts/Geist-Medium.ttf +0 -0
- package/templates/distribution/fonts/Geist-Regular.ttf +0 -0
- package/templates/distribution/fonts/Geist-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/GeistMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Medium.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-Regular.ttf +0 -0
- package/templates/distribution/fonts/JetBrainsMono-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Bold.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Medium.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-Regular.ttf +0 -0
- package/templates/distribution/fonts/PlusJakartaSans-SemiBold.ttf +0 -0
- package/templates/distribution/fonts/README.md +36 -0
- package/templates/distribution/fonts/SpaceGrotesk-Variable.ttf +0 -0
- package/templates/distribution/fonts/handwritten/Caveat.ttf +0 -0
- package/templates/distribution/fonts/legacy/IBMPlexMono-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Medium.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/Inter-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/InterDisplay-SemiBold.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Regular.otf +0 -0
- package/templates/distribution/fonts/legacy/SourceSerif4-Semibold.otf +0 -0
- package/templates/distribution/icons/activity.svg +15 -0
- package/templates/distribution/icons/alert-triangle.svg +17 -0
- package/templates/distribution/icons/book-open.svg +16 -0
- package/templates/distribution/icons/bot.svg +20 -0
- package/templates/distribution/icons/brain.svg +22 -0
- package/templates/distribution/icons/circle-check.svg +16 -0
- package/templates/distribution/icons/clipboard-check.svg +17 -0
- package/templates/distribution/icons/cpu.svg +28 -0
- package/templates/distribution/icons/database.svg +17 -0
- package/templates/distribution/icons/eye.svg +16 -0
- package/templates/distribution/icons/file-text.svg +19 -0
- package/templates/distribution/icons/gauge.svg +16 -0
- package/templates/distribution/icons/git-branch.svg +17 -0
- package/templates/distribution/icons/key.svg +17 -0
- package/templates/distribution/icons/layers.svg +17 -0
- package/templates/distribution/icons/list-checks.svg +19 -0
- package/templates/distribution/icons/lock.svg +16 -0
- package/templates/distribution/icons/message-square.svg +15 -0
- package/templates/distribution/icons/network.svg +19 -0
- package/templates/distribution/icons/route.svg +17 -0
- package/templates/distribution/icons/scale.svg +19 -0
- package/templates/distribution/icons/search.svg +16 -0
- package/templates/distribution/icons/send.svg +16 -0
- package/templates/distribution/icons/server.svg +18 -0
- package/templates/distribution/icons/shield-check.svg +16 -0
- package/templates/distribution/icons/users.svg +18 -0
- package/templates/distribution/icons/webhook.svg +17 -0
- package/templates/distribution/icons/workflow.svg +17 -0
- package/templates/distribution/icons/wrench.svg +15 -0
- package/templates/distribution/run.mjs +18 -1
- package/templates/docs/adr.md +7 -0
- package/templates/docs/construct_guide.md +8 -8
- package/templates/docs/customer-profile.md +4 -0
- package/templates/docs/one-pager.md +4 -0
- package/templates/docs/postmortem.md +31 -0
- package/templates/docs/prd-platform.md +19 -0
- package/templates/docs/prd.md +15 -0
- package/templates/docs/prfaq.md +7 -0
- package/templates/docs/qa-strategy.md +103 -0
- package/templates/docs/research-brief.md +8 -0
- package/templates/docs/rfc-platform.md +12 -0
- package/templates/docs/test-plan.md +7 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chat/harness/driver.mjs — normalized harness driver contract for the
|
|
3
|
+
* construct terminal chat surface.
|
|
4
|
+
*
|
|
5
|
+
* A driver runs the agent loop and normalizes its streaming output into one event
|
|
6
|
+
* model, so the transparency engine and the TUI never depend on how the loop is
|
|
7
|
+
* produced. ADR-0041: Construct's owned loop (apps/chat/engine) implements this
|
|
8
|
+
* contract; the union stays the stable seam between the engine and the renderers.
|
|
9
|
+
* Every driver exposes the same shape:
|
|
10
|
+
* start() opens a session and returns { sessionId, capabilities }
|
|
11
|
+
* prompt(text, opts) async-iterable of normalized events for one turn; opts may
|
|
12
|
+
* carry { model, sandbox, permissionMode } when the host supports them
|
|
13
|
+
* cancel() aborts the active turn
|
|
14
|
+
* stop() tears down the host process / connection
|
|
15
|
+
* listModels() optional: returns [{ id, label, ... }] when the host can enumerate models
|
|
16
|
+
*
|
|
17
|
+
* Normalized event union consumed by the rest of the surface:
|
|
18
|
+
* { type: 'thinking', text, messageId } agent internal reasoning chunk
|
|
19
|
+
* { type: 'text', text, messageId } agent message chunk
|
|
20
|
+
* { type: 'plan', entries } planned path / task entries
|
|
21
|
+
* { type: 'tool_call', id, title, kind, status }
|
|
22
|
+
* { type: 'tool_update', id, status, content }
|
|
23
|
+
* { type: 'usage', tokens, cost, context, model }
|
|
24
|
+
* tokens: { input, output, cacheRead, cacheWrite, reasoning, total }
|
|
25
|
+
* cost: { amount, currency } context: { used, size }
|
|
26
|
+
* every field optional — adapters emit only what the host
|
|
27
|
+
* reports (no fabricated splits)
|
|
28
|
+
* { type: 'specialist', role, status, detail } construct orchestration overlay
|
|
29
|
+
* { type: 'permission', requestId, toolCall, options }
|
|
30
|
+
* { type: 'notice', level, code, message } surface-visible advisory (e.g. context-pressure)
|
|
31
|
+
* { type: 'context_continuation', packet, compacted } persisted continuation packet (construct-6zga.1.10)
|
|
32
|
+
* { type: 'error', message }
|
|
33
|
+
* { type: 'done', stopReason }
|
|
34
|
+
*
|
|
35
|
+
* Keeping this union stable is what lets ACP agents and per-host adapters be
|
|
36
|
+
* swapped without touching the transparency or rendering layers.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export const EVENT_TYPES = Object.freeze({
|
|
40
|
+
THINKING: 'thinking',
|
|
41
|
+
TEXT: 'text',
|
|
42
|
+
PLAN: 'plan',
|
|
43
|
+
TOOL_CALL: 'tool_call',
|
|
44
|
+
TOOL_UPDATE: 'tool_update',
|
|
45
|
+
USAGE: 'usage',
|
|
46
|
+
SPECIALIST: 'specialist',
|
|
47
|
+
PERMISSION: 'permission',
|
|
48
|
+
NOTICE: 'notice',
|
|
49
|
+
CONTEXT_CONTINUATION: 'context_continuation',
|
|
50
|
+
ERROR: 'error',
|
|
51
|
+
DONE: 'done',
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// A single-producer/single-consumer queue that is also an async iterable. The
|
|
55
|
+
// line-router pushes normalized events as they arrive; the turn loop awaits them
|
|
56
|
+
// until the producer closes the queue on stop reason or error.
|
|
57
|
+
|
|
58
|
+
export class AsyncEventQueue {
|
|
59
|
+
constructor() {
|
|
60
|
+
this.buffer = [];
|
|
61
|
+
this.waiters = [];
|
|
62
|
+
this.closed = false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
push(value) {
|
|
66
|
+
if (this.closed) return;
|
|
67
|
+
const waiter = this.waiters.shift();
|
|
68
|
+
if (waiter) waiter({ value, done: false });
|
|
69
|
+
else this.buffer.push(value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
close() {
|
|
73
|
+
if (this.closed) return;
|
|
74
|
+
this.closed = true;
|
|
75
|
+
while (this.waiters.length) this.waiters.shift()({ value: undefined, done: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
[Symbol.asyncIterator]() {
|
|
79
|
+
return {
|
|
80
|
+
next: () => {
|
|
81
|
+
if (this.buffer.length) return Promise.resolve({ value: this.buffer.shift(), done: false });
|
|
82
|
+
if (this.closed) return Promise.resolve({ value: undefined, done: true });
|
|
83
|
+
return new Promise((resolve) => this.waiters.push(resolve));
|
|
84
|
+
},
|
|
85
|
+
return: () => {
|
|
86
|
+
this.close();
|
|
87
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chat/list-picker.mjs — searchable list picker for construct chat Ink.
|
|
3
|
+
*
|
|
4
|
+
* Pure state/helpers for arrow-key overlays: type-to-filter, windowed viewport,
|
|
5
|
+
* and selection index clamping for model, setting, and permission pickers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export function filterPickerItems(items = [], query = '') {
|
|
9
|
+
const q = String(query || '').trim().toLowerCase();
|
|
10
|
+
if (!q) return items;
|
|
11
|
+
return items.filter((item) => {
|
|
12
|
+
const hay = [item.id, item.label, item.detail, item.tag]
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
.join(' ')
|
|
15
|
+
.toLowerCase();
|
|
16
|
+
return hay.includes(q);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function pickerStartIndex(items, selectedId, idKey = 'id') {
|
|
21
|
+
if (!items?.length) return 0;
|
|
22
|
+
const idx = items.findIndex((item) => item[idKey] === selectedId);
|
|
23
|
+
if (idx >= 0 && !items[idx]?.disabled) return idx;
|
|
24
|
+
const firstEnabled = items.findIndex((item) => !item.disabled);
|
|
25
|
+
if (firstEnabled >= 0) return firstEnabled;
|
|
26
|
+
return idx >= 0 ? idx : 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function windowPickerItems(items, index, windowSize = 12) {
|
|
30
|
+
if (!items?.length) return { items: [], offset: 0 };
|
|
31
|
+
const size = Math.max(5, windowSize);
|
|
32
|
+
let start = Math.max(0, index - Math.floor(size / 2));
|
|
33
|
+
if (start + size > items.length) start = Math.max(0, items.length - size);
|
|
34
|
+
return { items: items.slice(start, start + size), offset: start };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function clampPickerIndex(state) {
|
|
38
|
+
const visible = filterPickerItems(state.items, state.query);
|
|
39
|
+
const max = Math.max(0, visible.length - 1);
|
|
40
|
+
return { ...state, index: Math.min(state.index, max) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {{
|
|
45
|
+
* kind?: string,
|
|
46
|
+
* title?: string,
|
|
47
|
+
* items?: Array<Record<string, unknown>>,
|
|
48
|
+
* selectedId?: string | null,
|
|
49
|
+
* context?: string | null,
|
|
50
|
+
* query?: string,
|
|
51
|
+
* }} [opts]
|
|
52
|
+
*/
|
|
53
|
+
export function createListPickerState({
|
|
54
|
+
kind, title, items, selectedId = null, context = null, query = '',
|
|
55
|
+
} = {}) {
|
|
56
|
+
const visible = filterPickerItems(items, query);
|
|
57
|
+
return {
|
|
58
|
+
kind,
|
|
59
|
+
title,
|
|
60
|
+
items,
|
|
61
|
+
query,
|
|
62
|
+
index: pickerStartIndex(visible, selectedId),
|
|
63
|
+
context,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getPickerVisibleItems(state) {
|
|
68
|
+
return filterPickerItems(state?.items || [], state?.query || '');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getPickerSelectedItem(state) {
|
|
72
|
+
const visible = getPickerVisibleItems(state);
|
|
73
|
+
return visible[state?.index ?? 0] || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function movePickerIndex(state, delta) {
|
|
77
|
+
const visible = getPickerVisibleItems(state);
|
|
78
|
+
if (!visible.length) return state;
|
|
79
|
+
let idx = state.index ?? 0;
|
|
80
|
+
for (let step = 0; step < visible.length; step++) {
|
|
81
|
+
idx = Math.max(0, Math.min(visible.length - 1, idx + delta));
|
|
82
|
+
if (!visible[idx]?.disabled) return { ...state, index: idx };
|
|
83
|
+
if ((delta < 0 && idx === 0) || (delta > 0 && idx === visible.length - 1)) break;
|
|
84
|
+
}
|
|
85
|
+
return { ...state, index: idx };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function appendPickerQuery(state, char) {
|
|
89
|
+
if (!char) return state;
|
|
90
|
+
return clampPickerIndex({ ...state, query: `${state.query || ''}${char}`, index: 0 });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function backspacePickerQuery(state) {
|
|
94
|
+
return clampPickerIndex({ ...state, query: (state.query || '').slice(0, -1), index: 0 });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function reducePickerKey(state, { char, key } = {}) {
|
|
98
|
+
if (!state) return { state: null, action: 'none' };
|
|
99
|
+
if (key?.escape) return { state: null, action: 'cancel' };
|
|
100
|
+
if (key?.return) return { state, action: 'commit' };
|
|
101
|
+
if (key?.upArrow) return { state: movePickerIndex(state, -1), action: 'none' };
|
|
102
|
+
if (key?.downArrow) return { state: movePickerIndex(state, 1), action: 'none' };
|
|
103
|
+
if (key?.backspace || key?.delete) return { state: backspacePickerQuery(state), action: 'none' };
|
|
104
|
+
if (char && !key?.ctrl && !key?.meta) return { state: appendPickerQuery(state, char), action: 'none' };
|
|
105
|
+
return { state, action: 'none' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function pickerViewport(state, windowSize = 14) {
|
|
109
|
+
const visible = getPickerVisibleItems(state);
|
|
110
|
+
const clamped = clampPickerIndex({ ...state, items: visible });
|
|
111
|
+
return windowPickerItems(visible, clamped.index, windowSize);
|
|
112
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/chat/model-picker.mjs — model list data for the construct chat picker.
|
|
3
|
+
*
|
|
4
|
+
* Curates a short list: free-router auto-pick, live OpenRouter free models,
|
|
5
|
+
* configured provider tier defaults, and the active session model. Persistence
|
|
6
|
+
* reuses saveChatConfig from lib/chat/config.mjs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { saveChatConfig } from './config.mjs';
|
|
10
|
+
import { hasAnySecret } from '../providers/secret-resolver.mjs';
|
|
11
|
+
import { getProviderModelCatalog, isChatModelAvailable } from '../model-router.mjs';
|
|
12
|
+
import {
|
|
13
|
+
pollConfiguredProviders,
|
|
14
|
+
readProviderCatalogCache,
|
|
15
|
+
} from '../models/provider-poll.mjs';
|
|
16
|
+
import { resolveFreeOpenRouterModel } from '../../apps/chat/engine/models.mjs';
|
|
17
|
+
import { filterPickerItems, pickerStartIndex, windowPickerItems } from './list-picker.mjs';
|
|
18
|
+
|
|
19
|
+
export { filterPickerItems, pickerStartIndex, windowPickerItems };
|
|
20
|
+
|
|
21
|
+
export const FREE_ROUTER_ITEM_ID = '__free_router__';
|
|
22
|
+
|
|
23
|
+
export function sortModelsForPicker(models = []) {
|
|
24
|
+
return [...models].sort(
|
|
25
|
+
(a, b) => Number(Boolean(b.action === 'free-router')) - Number(Boolean(a.action === 'free-router'))
|
|
26
|
+
|| Number(Boolean(b.isFree)) - Number(Boolean(a.isFree))
|
|
27
|
+
|| Number(b.suitable !== false) - Number(a.suitable !== false)
|
|
28
|
+
|| Number(Boolean(b.isProviderDefault)) - Number(Boolean(a.isProviderDefault))
|
|
29
|
+
|| String(a.label || a.id).localeCompare(String(b.label || b.id)),
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function shortModelLabel(id) {
|
|
34
|
+
if (!id) return id;
|
|
35
|
+
const name = id.replace(/^openrouter\//, '');
|
|
36
|
+
const slash = name.lastIndexOf('/');
|
|
37
|
+
return slash >= 0 ? name.slice(slash + 1) : name;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function catalogItem(model) {
|
|
41
|
+
const id = model.id;
|
|
42
|
+
const isFree = id.includes(':free') || model.isFree === true;
|
|
43
|
+
return {
|
|
44
|
+
id,
|
|
45
|
+
label: model.label || (isFree ? shortModelLabel(id) : id),
|
|
46
|
+
tag: model.action === 'free-router' ? 'router' : isFree ? 'free' : model.local ? 'local' : model.tier || null,
|
|
47
|
+
detail: model.detail || model.name || (model.configured === false ? 'not configured' : null),
|
|
48
|
+
isFree,
|
|
49
|
+
action: model.action || null,
|
|
50
|
+
suitable: model.suitable,
|
|
51
|
+
disabled: model.disabled === true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function configuredTierPickerItems({ env = process.env } = {}) {
|
|
56
|
+
const { providers } = getProviderModelCatalog({ env });
|
|
57
|
+
const items = [];
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
for (const provider of providers) {
|
|
60
|
+
if (!provider.configured) continue;
|
|
61
|
+
for (const tier of ['reasoning', 'standard', 'fast']) {
|
|
62
|
+
const id = provider.tiers?.[tier];
|
|
63
|
+
if (!id || seen.has(id)) continue;
|
|
64
|
+
seen.add(id);
|
|
65
|
+
const availability = isChatModelAvailable(id, { env });
|
|
66
|
+
const notPulled = availability.reason === 'model_not_pulled';
|
|
67
|
+
items.push(catalogItem({
|
|
68
|
+
id,
|
|
69
|
+
label: id,
|
|
70
|
+
name: `${provider.label} · ${tier}`,
|
|
71
|
+
tier,
|
|
72
|
+
configured: availability.ok,
|
|
73
|
+
suitable: availability.ok,
|
|
74
|
+
isProviderDefault: tier === 'standard',
|
|
75
|
+
disabled: !availability.ok,
|
|
76
|
+
detail: notPulled
|
|
77
|
+
? `not installed — run ollama pull ${availability.nativeModel || id.replace(/^ollama\//, '')}`
|
|
78
|
+
: availability.ok ? null : availability.reason?.replace(/_/g, ' ') || 'unavailable',
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { items, seen };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function capabilityBadges(model) {
|
|
86
|
+
const badges = [];
|
|
87
|
+
if (model.reasoning) badges.push('reasoning');
|
|
88
|
+
if (model.vision) badges.push('vision');
|
|
89
|
+
if (model.tools) badges.push('tools');
|
|
90
|
+
return badges;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function priceLabel(pricing) {
|
|
94
|
+
if (!pricing) return null;
|
|
95
|
+
if (pricing.input === 0 && pricing.output === 0) return null;
|
|
96
|
+
const fmt = (n) => (n >= 1 ? n.toFixed(2) : n.toFixed(3));
|
|
97
|
+
return `$${fmt(pricing.input)} in · $${fmt(pricing.output)} out / 1M`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function contextLabel(context) {
|
|
101
|
+
if (!context) return null;
|
|
102
|
+
if (context >= 1_000_000) return `${(context / 1_000_000).toFixed(1).replace(/\.0$/, '')}M ctx`;
|
|
103
|
+
if (context >= 1000) return `${Math.round(context / 1000)}K ctx`;
|
|
104
|
+
return `${context} ctx`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function pickerItemFromModel(model, groupLabel) {
|
|
108
|
+
if (model.source === 'hint' || model.disabled) {
|
|
109
|
+
return {
|
|
110
|
+
id: model.id,
|
|
111
|
+
label: model.label || model.id,
|
|
112
|
+
group: groupLabel,
|
|
113
|
+
tag: null,
|
|
114
|
+
badges: [],
|
|
115
|
+
detail: null,
|
|
116
|
+
isFree: false,
|
|
117
|
+
suitable: false,
|
|
118
|
+
disabled: true,
|
|
119
|
+
source: model.source || 'hint',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const badges = capabilityBadges(model);
|
|
123
|
+
const price = priceLabel(model.pricing);
|
|
124
|
+
const detailParts = [];
|
|
125
|
+
if (model.free) detailParts.push(model.provider === 'ollama' ? 'free · runs locally' : 'free');
|
|
126
|
+
else if (price) detailParts.push(price);
|
|
127
|
+
const ctx = contextLabel(model.context);
|
|
128
|
+
if (ctx) detailParts.push(ctx);
|
|
129
|
+
if (model.source === 'cached') detailParts.push('cached — provider offline');
|
|
130
|
+
|
|
131
|
+
// The OpenRouter free router answers from whatever free model is available, so
|
|
132
|
+
// quality and behavior vary turn to turn — say so before it is selected.
|
|
133
|
+
if (/^openrouter\/openrouter\/(free|auto)\b/.test(String(model.id))) {
|
|
134
|
+
detailParts.push('variable quality — routes to a random free model');
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
id: model.id,
|
|
138
|
+
label: model.label || shortModelLabel(model.id),
|
|
139
|
+
group: groupLabel,
|
|
140
|
+
tag: model.free ? 'free' : null,
|
|
141
|
+
badges,
|
|
142
|
+
price: model.free ? null : price,
|
|
143
|
+
context: model.context || null,
|
|
144
|
+
detail: detailParts.join(' · ') || null,
|
|
145
|
+
isFree: Boolean(model.free),
|
|
146
|
+
suitable: true,
|
|
147
|
+
disabled: false,
|
|
148
|
+
source: model.source || 'live',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// One picker payload: the free-router auto-pick on top, then every configured
|
|
153
|
+
// provider's live catalog grouped under its label. Polling is best-effort; on a
|
|
154
|
+
// total failure the cached groups (or nothing beyond free-router) are used so the
|
|
155
|
+
// picker still opens.
|
|
156
|
+
export async function loadModelPickerItems(_driver, { env = process.env, cwd = process.cwd(), currentModel = null, modelMode = 'pinned', pollProviders = pollConfiguredProviders } = {}) {
|
|
157
|
+
const items = [];
|
|
158
|
+
const seen = new Set();
|
|
159
|
+
|
|
160
|
+
const hasOpenRouter = hasAnySecret(['OPENROUTER_API_KEY', 'OPEN_ROUTER_API_KEY'], { env });
|
|
161
|
+
items.push({
|
|
162
|
+
id: FREE_ROUTER_ITEM_ID,
|
|
163
|
+
label: 'OpenRouter free router — auto-pick best standard model',
|
|
164
|
+
group: 'Smart routing',
|
|
165
|
+
tag: 'router',
|
|
166
|
+
badges: [],
|
|
167
|
+
detail: hasOpenRouter
|
|
168
|
+
? 'auto-picks a free model + retries on failure; not pinned — quality varies'
|
|
169
|
+
: 'needs OPENROUTER_API_KEY in ~/.construct/config.env',
|
|
170
|
+
action: 'free-router',
|
|
171
|
+
configured: hasOpenRouter,
|
|
172
|
+
disabled: !hasOpenRouter,
|
|
173
|
+
});
|
|
174
|
+
seen.add(FREE_ROUTER_ITEM_ID);
|
|
175
|
+
|
|
176
|
+
let groups = [];
|
|
177
|
+
try {
|
|
178
|
+
groups = await pollProviders({ env, cwd });
|
|
179
|
+
} catch {
|
|
180
|
+
groups = readProviderCatalogCache() || [];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
for (const group of groups) {
|
|
184
|
+
const sorted = [...group.models].sort(
|
|
185
|
+
(a, b) => Number(Boolean(b.free)) - Number(Boolean(a.free))
|
|
186
|
+
|| String(a.label || a.id).localeCompare(String(b.label || b.id)),
|
|
187
|
+
);
|
|
188
|
+
for (const model of sorted) {
|
|
189
|
+
if (seen.has(model.id)) continue;
|
|
190
|
+
|
|
191
|
+
// The chat loop always dispatches tools, so a model the provider reports as
|
|
192
|
+
// tool-incapable can only ever error out — drop it from the picker. Only
|
|
193
|
+
// filter when the provider actually told us (toolsKnown); an unknown
|
|
194
|
+
// capability (e.g. a local Ollama tag) stays selectable.
|
|
195
|
+
if (!model.disabled && model.source !== 'hint' && model.toolsKnown === true && !model.tools) {
|
|
196
|
+
seen.add(model.id);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
seen.add(model.id);
|
|
201
|
+
items.push(pickerItemFromModel(model, group.label));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (currentModel && modelMode !== 'free-router' && !seen.has(currentModel)) {
|
|
206
|
+
seen.add(currentModel);
|
|
207
|
+
items.push(pickerItemFromModel(
|
|
208
|
+
{ id: currentModel, label: currentModel, free: false, source: 'live' },
|
|
209
|
+
'Current session',
|
|
210
|
+
));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return items;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function resolveModelPickerSelection(item, { env = process.env } = {}) {
|
|
217
|
+
if (!item) return null;
|
|
218
|
+
if (item.action === 'free-router' || item.id === FREE_ROUTER_ITEM_ID) {
|
|
219
|
+
const modelId = await resolveFreeOpenRouterModel({ env, tier: 'standard' });
|
|
220
|
+
return modelId ? { mode: 'free-router', modelId } : null;
|
|
221
|
+
}
|
|
222
|
+
return { mode: 'pinned', modelId: item.id };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function commitPickerModel(session, selection, { cwd, hostId = 'construct', layers = null } = {}) {
|
|
226
|
+
const normalized = typeof selection === 'string'
|
|
227
|
+
? { mode: 'pinned', modelId: selection }
|
|
228
|
+
: selection;
|
|
229
|
+
if (!normalized?.modelId && normalized?.mode !== 'free-router') return null;
|
|
230
|
+
|
|
231
|
+
session.modelMode = normalized.mode || 'pinned';
|
|
232
|
+
session.model = normalized.modelId;
|
|
233
|
+
session.savedModel = session.modelMode === 'pinned' ? normalized.modelId : null;
|
|
234
|
+
session.modelNotice = null;
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
saveChatConfig({
|
|
238
|
+
host: hostId,
|
|
239
|
+
model: session.modelMode === 'pinned' ? session.model : null,
|
|
240
|
+
modelMode: session.modelMode,
|
|
241
|
+
layers: layers || session.layers,
|
|
242
|
+
thinking: (layers || session.layers)?.thinking,
|
|
243
|
+
permissionMode: session.permissionMode,
|
|
244
|
+
sandbox: session.sandbox,
|
|
245
|
+
ui: session.ui,
|
|
246
|
+
}, { cwd });
|
|
247
|
+
} catch { /* persistence is best-effort */ }
|
|
248
|
+
return normalized;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function pickerSelectedId(session) {
|
|
252
|
+
if (session?.modelMode === 'free-router') return FREE_ROUTER_ITEM_ID;
|
|
253
|
+
return session?.savedModel || session?.model || null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function formatModelHeader(session) {
|
|
257
|
+
if (session?.modelMode === 'free-router') {
|
|
258
|
+
const slug = session.model ? session.model.replace(/^openrouter\//, '') : '(resolving…)';
|
|
259
|
+
return { label: `free router → ${slug}`, isRouter: true };
|
|
260
|
+
}
|
|
261
|
+
const active = session?.model || '(no model)';
|
|
262
|
+
const saved = session?.savedModel;
|
|
263
|
+
if (saved && saved !== session?.model) {
|
|
264
|
+
return { label: active, savedPin: saved, isRouter: false };
|
|
265
|
+
}
|
|
266
|
+
return { label: active, isRouter: false };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function formatPickerLine(item, colors, { selected = false } = {}) {
|
|
270
|
+
const marker = selected ? `${colors.green}●${colors.reset}` : ' ';
|
|
271
|
+
const tag = item.tag ? `${colors.dim} [${item.tag}]${colors.reset}` : '';
|
|
272
|
+
const badges = item.badges?.length ? `${colors.dim} ${item.badges.map((b) => `·${b}`).join(' ')}${colors.reset}` : '';
|
|
273
|
+
const disabled = item.disabled ? `${colors.dim} (unavailable)${colors.reset}` : '';
|
|
274
|
+
const detail = item.detail ? `\n ${colors.dim}${item.detail}${colors.reset}` : '';
|
|
275
|
+
return ` ${marker} ${item.label || item.id}${tag}${badges}${disabled}${detail}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function promptModelPickerTerminal({
|
|
279
|
+
output,
|
|
280
|
+
colors,
|
|
281
|
+
session,
|
|
282
|
+
rl = null,
|
|
283
|
+
askFn = null,
|
|
284
|
+
env = process.env,
|
|
285
|
+
cwd = process.cwd(),
|
|
286
|
+
hostId = 'construct',
|
|
287
|
+
layers = session?.layers,
|
|
288
|
+
pollProviders = pollConfiguredProviders,
|
|
289
|
+
} = {}) {
|
|
290
|
+
const items = await loadModelPickerItems(null, {
|
|
291
|
+
env,
|
|
292
|
+
cwd,
|
|
293
|
+
currentModel: session?.model,
|
|
294
|
+
modelMode: session?.modelMode || 'pinned',
|
|
295
|
+
pollProviders,
|
|
296
|
+
});
|
|
297
|
+
if (!items.length) {
|
|
298
|
+
output.write(`${colors.dim}no models to pick from — set a provider key in ~/.construct/config.env or use /model <id>${colors.reset}\n`);
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const selectedId = pickerSelectedId(session);
|
|
303
|
+
output.write(`${colors.bold}select a model${colors.reset}\n`);
|
|
304
|
+
output.write(`${colors.dim}enter a number, or press enter to cancel${colors.reset}\n`);
|
|
305
|
+
const groupCounts = new Map();
|
|
306
|
+
for (const item of items) {
|
|
307
|
+
if (!item.group || item.disabled) continue;
|
|
308
|
+
groupCounts.set(item.group, (groupCounts.get(item.group) || 0) + 1);
|
|
309
|
+
}
|
|
310
|
+
let lastGroup = null;
|
|
311
|
+
items.forEach((item, i) => {
|
|
312
|
+
if (item.group && item.group !== lastGroup) {
|
|
313
|
+
lastGroup = item.group;
|
|
314
|
+
const count = groupCounts.get(item.group);
|
|
315
|
+
const suffix = count ? ` (${count})` : '';
|
|
316
|
+
output.write(`\n${colors.dim}── ${item.group}${suffix} ──${colors.reset}\n`);
|
|
317
|
+
}
|
|
318
|
+
output.write(`${String(i + 1).padStart(2)}.${formatPickerLine(item, colors, { selected: item.id === selectedId })}\n`);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const prompt = `${colors.green}model #${colors.reset} `;
|
|
322
|
+
const answer = rl
|
|
323
|
+
? (await new Promise((resolve) => rl.question(prompt, resolve))).trim()
|
|
324
|
+
: askFn
|
|
325
|
+
? String(await askFn(prompt)).trim()
|
|
326
|
+
: '';
|
|
327
|
+
if (!answer) {
|
|
328
|
+
output.write(`${colors.dim}${rl || askFn ? 'cancelled' : 'pick one with /model <id> or use the Ink UI for search'}${colors.reset}\n`);
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const idx = Number(answer) - 1;
|
|
333
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= items.length) {
|
|
334
|
+
output.write(`${colors.red}invalid selection${colors.reset}\n`);
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const item = items[idx];
|
|
339
|
+
if (item.disabled) {
|
|
340
|
+
output.write(`${colors.red}${item.detail || 'model not available'}${colors.reset}\n`);
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const selection = await resolveModelPickerSelection(item, { env });
|
|
345
|
+
if (!selection?.modelId && selection?.mode !== 'free-router') {
|
|
346
|
+
output.write(`${colors.red}could not resolve model${colors.reset}\n`);
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
commitPickerModel(session, selection, { cwd, hostId, layers });
|
|
351
|
+
const label = selection.mode === 'free-router'
|
|
352
|
+
? `free-router → ${selection.modelId}`
|
|
353
|
+
: selection.modelId;
|
|
354
|
+
output.write(`${colors.green}model set:${colors.reset} ${label} ${colors.dim}(saved)${colors.reset}\n`);
|
|
355
|
+
return selection;
|
|
356
|
+
}
|