@vecteur/cli 0.9.0 → 0.10.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 +2 -3
- package/package.json +2 -1
- package/src/contract.js +2 -2
- package/src/input.js +15 -0
- package/src/session.js +4 -1
- package/src/terminal.js +131 -29
- package/src/tui/view.js +252 -107
- package/src/update.js +3 -3
package/README.md
CHANGED
|
@@ -154,9 +154,8 @@ discovery or local execution capability ships in this package.
|
|
|
154
154
|
The intended platform matrix includes Linux, macOS and Windows Terminal, plus screen-reader
|
|
155
155
|
qualification. Current evidence and unresolved cases belong in the stage-1 WBS/backlog; Linux
|
|
156
156
|
tests alone do not qualify other platforms. Standalone binaries are a later packaging option using
|
|
157
|
-
the same session owner. This
|
|
158
|
-
|
|
159
|
-
`stable` before freezing the published bytes. Updates across channels always ask explicitly.
|
|
157
|
+
the same session owner. This package uses the `stable` channel, visible even offline. Publication
|
|
158
|
+
freezes its reviewed package bytes and release identity. Updates across channels always ask explicitly.
|
|
160
159
|
|
|
161
160
|
## Drafting, recovery and updates
|
|
162
161
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vecteur/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"vecteur": "bin/vecteur.js"
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"description": "The Vecteur command line \u2014 one login, the same product as the web app, from any directory.",
|
|
53
53
|
"license": "UNLICENSED",
|
|
54
|
+
"repository": "git+https://github.com/vecteurspace/vecteur-cli.git",
|
|
54
55
|
"publishConfig": {
|
|
55
56
|
"access": "public"
|
|
56
57
|
},
|
package/src/contract.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// GENERATED by scripts/gen-contract.mjs from owner TypeScript projections.
|
|
2
2
|
export const OWNER_CONTRACT_IDENTITY = "api-sha256:b78084db722fba9eb9c159fcc93b29d12db0760ded46263aca2ad2fcf97d6a3f;run-wire-sha256:7bc5c77a90d6093cff5179533d4f4b6326056057dd677740cac097ebc0c8fb54";
|
|
3
|
-
export const MODEL_CATALOGUE = Object.freeze([{"profile_id":"glm","label":"GLM 5.3","model":"z-ai/glm-5.3","provider":"tensorx","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://tensorx.ai/trust-security/","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":null,"eligibility_sha256":"9b77e192a34bc1e2dc909a84a736d4e21f5d5835f25aae763380ac6f7c7907a7","hosting":"provider_api","reasoning":null,"image_input":false,"training_permitted":false,"retention_mode":"zero_retention","retention_stated_days":null,"retention_fact":"Prompts and completions are processed with zero data retention.","zero_retention_endpoint":true,"policy_source_url":"https://tensorx.ai/terms-of-service/","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"TensorX Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://tensorx.ai/terms-of-service/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"flash","label":"DeepSeek V4.1 Flash 1017","model":"deepseek/deepseek-v4.1-flash-1017","provider":"tensorx","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://tensorx.ai/trust-security/","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":null,"eligibility_sha256":"0bb3844773a56a555dde5f20b2c8cab0271d937a246b7c3ae642fd98e49516e9","hosting":"provider_api","reasoning":"low","image_input":false,"training_permitted":false,"retention_mode":"zero_retention","retention_stated_days":null,"retention_fact":"Prompts and completions are processed with zero data retention.","zero_retention_endpoint":true,"policy_source_url":"https://tensorx.ai/terms-of-service/","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"TensorX Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://tensorx.ai/terms-of-service/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"kimi_k3","label":"Kimi K3","model":"moonshotai/kimi-k3","provider":"tensorx","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://tensorx.ai/trust-security/","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":null,"eligibility_sha256":"e69f8b077c57c5636da412ce31d2a2e2eff6210ecc8832044250ea81a6ce3d9b","hosting":"provider_api","reasoning":null,"image_input":false,"training_permitted":false,"retention_mode":"zero_retention","retention_stated_days":null,"retention_fact":"Prompts and completions are processed with zero data retention.","zero_retention_endpoint":true,"policy_source_url":"https://tensorx.ai/terms-of-service/","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"TensorX Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://tensorx.ai/terms-of-service/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"deepseek_pro","label":"DeepSeek V4 Pro","model":"deepseek/deepseek-v4-pro-0813","provider":"tensorx","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://tensorx.ai/trust-security/","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":null,"eligibility_sha256":"82cb953f9b3d7ee90d4de9b8e3abc8dcff16379ac0f446d9d194cea5d3ec2585","hosting":"provider_api","reasoning":null,"image_input":false,"training_permitted":false,"retention_mode":"zero_retention","retention_stated_days":null,"retention_fact":"Prompts and completions are processed with zero data retention.","zero_retention_endpoint":true,"policy_source_url":"https://tensorx.ai/terms-of-service/","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"TensorX Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://tensorx.ai/terms-of-service/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"glm_flash","label":"GLM 5.3 Flash","model":"z-ai/glm-5.3-flash","provider":"tensorx","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://tensorx.ai/trust-security/","admitted_context_window_tokens":1000000,"dialect":"openai_chat","execution_placement":null,"eligibility_sha256":"9b38c17e45100344956e5235c97ab6195de9e426108ceab63eba172b7a617592","hosting":"provider_api","reasoning":null,"image_input":false,"training_permitted":false,"retention_mode":"zero_retention","retention_stated_days":null,"retention_fact":"Prompts and completions are processed with zero data retention.","zero_retention_endpoint":true,"policy_source_url":"https://tensorx.ai/terms-of-service/","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"TensorX Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://tensorx.ai/terms-of-service/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"mistral","label":"Mistral Small 4","model":"mistral-small-2603","provider":"mistral","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://docs.mistral.ai/inference/regional-inference","admitted_context_window_tokens":262144,"dialect":"mistral_conversations","execution_placement":null,"eligibility_sha256":"8bf0ba9bc0ef536214e0248d8fc918f302f38b4e406883c4b9854ec9594aa77a","hosting":"provider_api","reasoning":null,"image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"Commercial API data has published privacy and retention controls with documented exceptions; zero data retention requires separate account approval and enablement.","zero_retention_endpoint":false,"policy_source_url":"https://docs.mistral.ai/admin/monitor-comply/privacy-data-controls","policy_checked_at":"2026-08-28","policy_expires_at":"2026-11-26","operator_entity":"Mistral AI","operator_country":"France","governing_law":"France","operator_source_url":"https://legal.mistral.ai/terms/commercial-terms-of-service","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"mistral_medium","label":"Mistral Medium 3.5","model":"mistral-medium-2604","provider":"mistral","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://docs.mistral.ai/inference/regional-inference","admitted_context_window_tokens":262144,"dialect":"mistral_conversations","execution_placement":null,"eligibility_sha256":"8c52e7ab13e7ec0c21777e9742b675ced4ef07b3166d3bdec6a129b0200f67d7","hosting":"provider_api","reasoning":null,"image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"Commercial API data has published privacy and retention controls with documented exceptions; zero data retention requires separate account approval and enablement.","zero_retention_endpoint":false,"policy_source_url":"https://docs.mistral.ai/admin/monitor-comply/privacy-data-controls","policy_checked_at":"2026-08-28","policy_expires_at":"2026-11-26","operator_entity":"Mistral AI","operator_country":"France","governing_law":"France","operator_source_url":"https://legal.mistral.ai/terms/commercial-terms-of-service","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"luna","label":"GPT-6 Luna","model":"gpt-6-luna","provider":"openai","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://developers.openai.com/api/docs/guides/your-data","admitted_context_window_tokens":131072,"dialect":"openai_responses","execution_placement":null,"eligibility_sha256":"5aad437ed7ad769d6e824d6a24e0ce7253a2cf65483f9af998257ba4b3a62210","hosting":"provider_api","reasoning":"medium","image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":30,"retention_fact":"Chat Completions abuse-monitoring logs are normally retained up to 30 days, with documented legal and safety exceptions.","zero_retention_endpoint":false,"policy_source_url":"https://platform.openai.com/docs/models/default-usage-policies-by-endpoint","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"OpenAI Ireland Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://openai.com/policies/services-agreement/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"sol","label":"GPT-6.1 Sol","model":"gpt-6.1-sol","provider":"openai","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://developers.openai.com/api/docs/guides/your-data","admitted_context_window_tokens":131072,"dialect":"openai_responses","execution_placement":null,"eligibility_sha256":"d8d6c3f90abcce5c33a0fbaca1e20c01be408460a5a0ade3ed676e31a09bf128","hosting":"provider_api","reasoning":"medium","image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":30,"retention_fact":"Chat Completions abuse-monitoring logs are normally retained up to 30 days, with documented legal and safety exceptions.","zero_retention_endpoint":false,"policy_source_url":"https://platform.openai.com/docs/models/default-usage-policies-by-endpoint","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"OpenAI Ireland Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://openai.com/policies/services-agreement/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"astra","label":"GPT-6 Astra","model":"gpt-6-astra","provider":"openai","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://developers.openai.com/api/docs/guides/your-data","admitted_context_window_tokens":131072,"dialect":"openai_responses","execution_placement":null,"eligibility_sha256":"166b94ca70d14249e14bcf080b92643772d3ebdb75fd3ab81c15a6a7fb2d6e5a","hosting":"provider_api","reasoning":"high","image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":30,"retention_fact":"Chat Completions abuse-monitoring logs are normally retained up to 30 days, with documented legal and safety exceptions.","zero_retention_endpoint":false,"policy_source_url":"https://platform.openai.com/docs/models/default-usage-policies-by-endpoint","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"OpenAI Ireland Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://openai.com/policies/services-agreement/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"claude_fable","label":"Claude Fable 5.1","model":"claude-fable-5-1","provider":"anthropic","data_residency":"US","data_residency_source":"vendor","data_residency_source_url":"https://privacy.claude.com/en/articles/7996890-where-are-your-servers-located-do-you-host-your-models-on-eu-servers","admitted_context_window_tokens":131072,"dialect":"anthropic_messages","execution_placement":null,"eligibility_sha256":"75224e4a7464da37d21c6b4b1ed4567a73b4430faad20221560899534404a8a7","hosting":"provider_api","reasoning":null,"image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":30,"retention_fact":"Commercial API inputs and outputs are normally retained up to 30 days, with documented legal, policy and safety exceptions.","zero_retention_endpoint":false,"policy_source_url":"https://privacy.claude.com/en/articles/7996866-how-long-do-you-store-my-organization-s-data","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"Anthropic Ireland, Limited","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://www.anthropic.com/legal/commercial-terms","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"claude_opus","label":"Claude Opus 5.5","model":"claude-opus-5-5","provider":"anthropic","data_residency":"US","data_residency_source":"vendor","data_residency_source_url":"https://privacy.claude.com/en/articles/7996890-where-are-your-servers-located-do-you-host-your-models-on-eu-servers","admitted_context_window_tokens":131072,"dialect":"anthropic_messages","execution_placement":null,"eligibility_sha256":"70fcb6021cb82980290ea4d431106bdf5dd0ccace8b012e44e83f07c61019e4d","hosting":"provider_api","reasoning":null,"image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":30,"retention_fact":"Commercial API inputs and outputs are normally retained up to 30 days, with documented legal, policy and safety exceptions.","zero_retention_endpoint":false,"policy_source_url":"https://privacy.claude.com/en/articles/7996866-how-long-do-you-store-my-organization-s-data","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"Anthropic Ireland, Limited","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://www.anthropic.com/legal/commercial-terms","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"claude_sonnet","label":"Claude Sonnet 5.5","model":"claude-sonnet-5-5","provider":"anthropic","data_residency":"US","data_residency_source":"vendor","data_residency_source_url":"https://privacy.claude.com/en/articles/7996890-where-are-your-servers-located-do-you-host-your-models-on-eu-servers","admitted_context_window_tokens":131072,"dialect":"anthropic_messages","execution_placement":null,"eligibility_sha256":"8892913a3ed87bae501dcfc300c51cb1342ede2956ef3a40d44ce30602f5d939","hosting":"provider_api","reasoning":null,"image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":30,"retention_fact":"Commercial API inputs and outputs are normally retained up to 30 days, with documented legal, policy and safety exceptions.","zero_retention_endpoint":false,"policy_source_url":"https://privacy.claude.com/en/articles/7996866-how-long-do-you-store-my-organization-s-data","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"Anthropic Ireland, Limited","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://www.anthropic.com/legal/commercial-terms","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"claude_haiku","label":"Claude Haiku 5.5","model":"claude-haiku-5-5","provider":"anthropic","data_residency":"US","data_residency_source":"vendor","data_residency_source_url":"https://privacy.claude.com/en/articles/7996890-where-are-your-servers-located-do-you-host-your-models-on-eu-servers","admitted_context_window_tokens":131072,"dialect":"anthropic_messages","execution_placement":null,"eligibility_sha256":"32647fa178a427705d04b7ec6447810da7016d36725c8625bbdc8ea5677e24f7","hosting":"provider_api","reasoning":null,"image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":30,"retention_fact":"Commercial API inputs and outputs are normally retained up to 30 days, with documented legal, policy and safety exceptions.","zero_retention_endpoint":false,"policy_source_url":"https://privacy.claude.com/en/articles/7996866-how-long-do-you-store-my-organization-s-data","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"Anthropic Ireland, Limited","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://www.anthropic.com/legal/commercial-terms","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"nebius_nemotron","label":"Nemotron 3.5 Lightning","model":"nvidia/Nemotron-3_5-Lightning","provider":"nebius","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://docs.tokenfactory.nebius.com/legal/legal-quick-guide","admitted_context_window_tokens":1048576,"dialect":"openai_chat","execution_placement":null,"eligibility_sha256":"f0586d343f97c23ed000e00ed86236c3c0c91b3300b2763079e8f6879c081e16","hosting":"provider_api","reasoning":null,"image_input":false,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"Without organization-level zero data retention, inputs and outputs may be retained solely for speculative decoding; retained data is stored in Finland in the European Union.","zero_retention_endpoint":false,"policy_source_url":"https://docs.tokenfactory.nebius.com/legal/legal-quick-guide","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"Nebius B.V.","operator_country":"Netherlands","governing_law":"Netherlands","operator_source_url":"https://docs.nebius.com/legal/agreement","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"openrouter_glm","label":"GLM 5.3","model":"z-ai/glm-5.3","provider":"openrouter","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":"GLOBAL","eligibility_sha256":"99d39ebcf0661d9556db669cfe118c59f94fb253476fa1a23bafb42938cc9a4b","hosting":"provider_api","reasoning":"low","image_input":false,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"OpenRouter retains prompts only on opt-in or for CSAM review, reporting and legal preservation, ZDR traffic included; downstream commercial policies have documented retention exceptions. This route makes no platform-wide zero-retention guarantee.","zero_retention_endpoint":true,"policy_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","policy_checked_at":"2026-10-30","policy_expires_at":"2026-11-29","operator_entity":"OpenRouter, Inc.","operator_country":"United States","governing_law":"New York, United States","operator_source_url":"https://openrouter.ai/terms","downstream_provider":"DeepInfra","downstream_policy_fact":"DeepInfra does not train on API inputs for these models; in-memory processing has documented debugging and security logging exceptions."},{"profile_id":"openrouter_ling","label":"Ling 3.1 Flash","model":"inclusionai/ling-3.1-flash","provider":"openrouter","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":"GLOBAL","eligibility_sha256":"713b2ca20ed060398e1589baa4f2d9759a462e7985c2c4ec8456e8216ed949d0","hosting":"provider_api","reasoning":"off","image_input":false,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"OpenRouter retains prompts only on opt-in or for CSAM review, reporting and legal preservation, ZDR traffic included; downstream commercial policies have documented retention exceptions. This route makes no platform-wide zero-retention guarantee.","zero_retention_endpoint":true,"policy_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","policy_checked_at":"2026-10-30","policy_expires_at":"2026-11-29","operator_entity":"OpenRouter, Inc.","operator_country":"United States","governing_law":"New York, United States","operator_source_url":"https://openrouter.ai/terms","downstream_provider":"DeepInfra","downstream_policy_fact":"DeepInfra does not train on API inputs for these models and does not store submitted data on disk; it holds it in memory during inference."},{"profile_id":"gpt_terra","label":"GPT-6.1 Terra","model":"gpt-6.1-terra","provider":"openai","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://developers.openai.com/api/docs/guides/your-data","admitted_context_window_tokens":131072,"dialect":"openai_responses","execution_placement":null,"eligibility_sha256":"09e58e0478782db24bd9cdca7a8d04805716045e622830c9770fd823a083c6f7","hosting":"provider_api","reasoning":"medium","image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":30,"retention_fact":"Chat Completions abuse-monitoring logs are normally retained up to 30 days, with documented legal and safety exceptions.","zero_retention_endpoint":false,"policy_source_url":"https://platform.openai.com/docs/models/default-usage-policies-by-endpoint","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"OpenAI Ireland Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://openai.com/policies/services-agreement/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"openrouter_gemini_flash","label":"Gemini 3.8 Flash","model":"google/gemini-3.8-flash","provider":"openrouter","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":"GLOBAL","eligibility_sha256":"fe98eada28b9f5521600959ed87e6634923e1b10136c053ceae485919fb85ba8","hosting":"provider_api","reasoning":"medium","image_input":false,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"OpenRouter retains prompts only on opt-in or for CSAM review, reporting and legal preservation, ZDR traffic included; downstream commercial policies have documented retention exceptions. This route makes no platform-wide zero-retention guarantee.","zero_retention_endpoint":false,"policy_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","policy_checked_at":"2026-10-30","policy_expires_at":"2026-11-29","operator_entity":"OpenRouter, Inc.","operator_country":"United States","governing_law":"New York, United States","operator_source_url":"https://openrouter.ai/terms","downstream_provider":"Google","downstream_policy_fact":"Google does not train on Vertex AI customer data without permission; prompts its abuse classifiers flag may be stored up to 90 days (OpenRouter Safety Retention on every Vertex endpoint since 2026-10-18), so this endpoint is not strict zero-retention."},{"profile_id":"openrouter_gemini_argon","label":"Gemini 4 Argon","model":"google/gemini-4-argon","provider":"openrouter","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":"GLOBAL","eligibility_sha256":"80efc4df5597ab3e9d4584bb065e381a2ae91b4f5d3f9187d6de7e3e568035d2","hosting":"provider_api","reasoning":"high","image_input":false,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"OpenRouter retains prompts only on opt-in or for CSAM review, reporting and legal preservation, ZDR traffic included; downstream commercial policies have documented retention exceptions. This route makes no platform-wide zero-retention guarantee.","zero_retention_endpoint":false,"policy_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","policy_checked_at":"2026-10-30","policy_expires_at":"2026-11-29","operator_entity":"OpenRouter, Inc.","operator_country":"United States","governing_law":"New York, United States","operator_source_url":"https://openrouter.ai/terms","downstream_provider":"Google","downstream_policy_fact":"Google does not train on Vertex AI customer data without permission; prompts its abuse classifiers flag may be stored up to 90 days (OpenRouter Safety Retention on every Vertex endpoint since 2026-10-18), so this endpoint is not strict zero-retention."},{"profile_id":"openrouter_grok","label":"Grok 4.7","model":"x-ai/grok-4.7","provider":"openrouter","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":"GLOBAL","eligibility_sha256":"bf4c95e82a447814818183288ba7d8d8673453403ff93817fe6e3a8617968715","hosting":"provider_api","reasoning":"low","image_input":false,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"OpenRouter retains prompts only on opt-in or for CSAM review, reporting and legal preservation, ZDR traffic included; downstream commercial policies have documented retention exceptions. This route makes no platform-wide zero-retention guarantee.","zero_retention_endpoint":true,"policy_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","policy_checked_at":"2026-10-30","policy_expires_at":"2026-11-29","operator_entity":"OpenRouter, Inc.","operator_country":"United States","governing_law":"New York, United States","operator_source_url":"https://openrouter.ai/terms","downstream_provider":"xAI","downstream_policy_fact":"xAI never trains on API inputs or outputs without permission; under ZDR inputs and outputs are never persisted to disk."}]);
|
|
3
|
+
export const MODEL_CATALOGUE = Object.freeze([{"profile_id":"glm","label":"GLM 5.3","model":"z-ai/glm-5.3","provider":"tensorx","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://tensorx.ai/trust-security/","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":null,"eligibility_sha256":"9b77e192a34bc1e2dc909a84a736d4e21f5d5835f25aae763380ac6f7c7907a7","hosting":"provider_api","reasoning":null,"image_input":false,"training_permitted":false,"retention_mode":"zero_retention","retention_stated_days":null,"retention_fact":"Prompts and completions are processed with zero data retention.","zero_retention_endpoint":true,"policy_source_url":"https://tensorx.ai/terms-of-service/","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"TensorX Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://tensorx.ai/terms-of-service/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"flash","label":"DeepSeek V4.1 Flash 1017","model":"deepseek/deepseek-v4.1-flash-1017","provider":"tensorx","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://tensorx.ai/trust-security/","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":null,"eligibility_sha256":"0bb3844773a56a555dde5f20b2c8cab0271d937a246b7c3ae642fd98e49516e9","hosting":"provider_api","reasoning":"low","image_input":false,"training_permitted":false,"retention_mode":"zero_retention","retention_stated_days":null,"retention_fact":"Prompts and completions are processed with zero data retention.","zero_retention_endpoint":true,"policy_source_url":"https://tensorx.ai/terms-of-service/","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"TensorX Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://tensorx.ai/terms-of-service/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"kimi_k3","label":"Kimi K3","model":"moonshotai/kimi-k3","provider":"tensorx","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://tensorx.ai/trust-security/","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":null,"eligibility_sha256":"e69f8b077c57c5636da412ce31d2a2e2eff6210ecc8832044250ea81a6ce3d9b","hosting":"provider_api","reasoning":null,"image_input":false,"training_permitted":false,"retention_mode":"zero_retention","retention_stated_days":null,"retention_fact":"Prompts and completions are processed with zero data retention.","zero_retention_endpoint":true,"policy_source_url":"https://tensorx.ai/terms-of-service/","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"TensorX Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://tensorx.ai/terms-of-service/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"deepseek_pro","label":"DeepSeek V4 Pro","model":"deepseek/deepseek-v4-pro-0813","provider":"tensorx","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://tensorx.ai/trust-security/","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":null,"eligibility_sha256":"82cb953f9b3d7ee90d4de9b8e3abc8dcff16379ac0f446d9d194cea5d3ec2585","hosting":"provider_api","reasoning":null,"image_input":false,"training_permitted":false,"retention_mode":"zero_retention","retention_stated_days":null,"retention_fact":"Prompts and completions are processed with zero data retention.","zero_retention_endpoint":true,"policy_source_url":"https://tensorx.ai/terms-of-service/","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"TensorX Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://tensorx.ai/terms-of-service/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"glm_flash","label":"GLM 5.3 Flash","model":"z-ai/glm-5.3-flash","provider":"tensorx","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://tensorx.ai/trust-security/","admitted_context_window_tokens":1000000,"dialect":"openai_chat","execution_placement":null,"eligibility_sha256":"9b38c17e45100344956e5235c97ab6195de9e426108ceab63eba172b7a617592","hosting":"provider_api","reasoning":null,"image_input":false,"training_permitted":false,"retention_mode":"zero_retention","retention_stated_days":null,"retention_fact":"Prompts and completions are processed with zero data retention.","zero_retention_endpoint":true,"policy_source_url":"https://tensorx.ai/terms-of-service/","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"TensorX Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://tensorx.ai/terms-of-service/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"mistral","label":"Mistral Small 4","model":"mistral-small-2603","provider":"mistral","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://docs.mistral.ai/inference/regional-inference","admitted_context_window_tokens":262144,"dialect":"mistral_conversations","execution_placement":null,"eligibility_sha256":"8bf0ba9bc0ef536214e0248d8fc918f302f38b4e406883c4b9854ec9594aa77a","hosting":"provider_api","reasoning":null,"image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"Commercial API data has published privacy and retention controls with documented exceptions; zero data retention requires separate account approval and enablement.","zero_retention_endpoint":false,"policy_source_url":"https://docs.mistral.ai/admin/monitor-comply/privacy-data-controls","policy_checked_at":"2026-08-28","policy_expires_at":"2026-11-26","operator_entity":"Mistral AI","operator_country":"France","governing_law":"France","operator_source_url":"https://legal.mistral.ai/terms/commercial-terms-of-service","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"mistral_medium","label":"Mistral Medium 3.5","model":"mistral-medium-2604","provider":"mistral","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://docs.mistral.ai/inference/regional-inference","admitted_context_window_tokens":262144,"dialect":"mistral_conversations","execution_placement":null,"eligibility_sha256":"8c52e7ab13e7ec0c21777e9742b675ced4ef07b3166d3bdec6a129b0200f67d7","hosting":"provider_api","reasoning":null,"image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"Commercial API data has published privacy and retention controls with documented exceptions; zero data retention requires separate account approval and enablement.","zero_retention_endpoint":false,"policy_source_url":"https://docs.mistral.ai/admin/monitor-comply/privacy-data-controls","policy_checked_at":"2026-08-28","policy_expires_at":"2026-11-26","operator_entity":"Mistral AI","operator_country":"France","governing_law":"France","operator_source_url":"https://legal.mistral.ai/terms/commercial-terms-of-service","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"mistral_large","label":"Mistral Large 4","model":"mistral-large-4","provider":"mistral","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://docs.mistral.ai/inference/regional-inference","admitted_context_window_tokens":1048576,"dialect":"mistral_conversations","execution_placement":null,"eligibility_sha256":"fd34bc87a87f1cbe9345e3a961fa0938fb51d51d7799bb6173e810df23895ebc","hosting":"provider_api","reasoning":"medium","image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"Commercial API data has published privacy and retention controls with documented exceptions; zero data retention requires separate account approval and enablement.","zero_retention_endpoint":false,"policy_source_url":"https://docs.mistral.ai/admin/monitor-comply/privacy-data-controls","policy_checked_at":"2026-08-28","policy_expires_at":"2026-11-26","operator_entity":"Mistral AI","operator_country":"France","governing_law":"France","operator_source_url":"https://legal.mistral.ai/terms/commercial-terms-of-service","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"luna","label":"GPT-6 Luna","model":"gpt-6-luna","provider":"openai","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://developers.openai.com/api/docs/guides/your-data","admitted_context_window_tokens":131072,"dialect":"openai_responses","execution_placement":null,"eligibility_sha256":"5aad437ed7ad769d6e824d6a24e0ce7253a2cf65483f9af998257ba4b3a62210","hosting":"provider_api","reasoning":"medium","image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":30,"retention_fact":"Chat Completions abuse-monitoring logs are normally retained up to 30 days, with documented legal and safety exceptions.","zero_retention_endpoint":false,"policy_source_url":"https://platform.openai.com/docs/models/default-usage-policies-by-endpoint","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"OpenAI Ireland Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://openai.com/policies/services-agreement/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"sol","label":"GPT-6.1 Sol","model":"gpt-6.1-sol","provider":"openai","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://developers.openai.com/api/docs/guides/your-data","admitted_context_window_tokens":131072,"dialect":"openai_responses","execution_placement":null,"eligibility_sha256":"d8d6c3f90abcce5c33a0fbaca1e20c01be408460a5a0ade3ed676e31a09bf128","hosting":"provider_api","reasoning":"medium","image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":30,"retention_fact":"Chat Completions abuse-monitoring logs are normally retained up to 30 days, with documented legal and safety exceptions.","zero_retention_endpoint":false,"policy_source_url":"https://platform.openai.com/docs/models/default-usage-policies-by-endpoint","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"OpenAI Ireland Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://openai.com/policies/services-agreement/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"astra","label":"GPT-6 Astra","model":"gpt-6-astra","provider":"openai","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://developers.openai.com/api/docs/guides/your-data","admitted_context_window_tokens":131072,"dialect":"openai_responses","execution_placement":null,"eligibility_sha256":"166b94ca70d14249e14bcf080b92643772d3ebdb75fd3ab81c15a6a7fb2d6e5a","hosting":"provider_api","reasoning":"high","image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":30,"retention_fact":"Chat Completions abuse-monitoring logs are normally retained up to 30 days, with documented legal and safety exceptions.","zero_retention_endpoint":false,"policy_source_url":"https://platform.openai.com/docs/models/default-usage-policies-by-endpoint","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"OpenAI Ireland Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://openai.com/policies/services-agreement/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"claude_fable","label":"Claude Fable 5.1","model":"claude-fable-5-1","provider":"anthropic","data_residency":"US","data_residency_source":"vendor","data_residency_source_url":"https://privacy.claude.com/en/articles/7996890-where-are-your-servers-located-do-you-host-your-models-on-eu-servers","admitted_context_window_tokens":131072,"dialect":"anthropic_messages","execution_placement":null,"eligibility_sha256":"75224e4a7464da37d21c6b4b1ed4567a73b4430faad20221560899534404a8a7","hosting":"provider_api","reasoning":null,"image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":30,"retention_fact":"Commercial API inputs and outputs are normally retained up to 30 days, with documented legal, policy and safety exceptions.","zero_retention_endpoint":false,"policy_source_url":"https://privacy.claude.com/en/articles/7996866-how-long-do-you-store-my-organization-s-data","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"Anthropic Ireland, Limited","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://www.anthropic.com/legal/commercial-terms","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"claude_opus","label":"Claude Opus 5.5","model":"claude-opus-5-5","provider":"anthropic","data_residency":"US","data_residency_source":"vendor","data_residency_source_url":"https://privacy.claude.com/en/articles/7996890-where-are-your-servers-located-do-you-host-your-models-on-eu-servers","admitted_context_window_tokens":131072,"dialect":"anthropic_messages","execution_placement":null,"eligibility_sha256":"70fcb6021cb82980290ea4d431106bdf5dd0ccace8b012e44e83f07c61019e4d","hosting":"provider_api","reasoning":null,"image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":30,"retention_fact":"Commercial API inputs and outputs are normally retained up to 30 days, with documented legal, policy and safety exceptions.","zero_retention_endpoint":false,"policy_source_url":"https://privacy.claude.com/en/articles/7996866-how-long-do-you-store-my-organization-s-data","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"Anthropic Ireland, Limited","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://www.anthropic.com/legal/commercial-terms","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"claude_sonnet","label":"Claude Sonnet 5.5","model":"claude-sonnet-5-5","provider":"anthropic","data_residency":"US","data_residency_source":"vendor","data_residency_source_url":"https://privacy.claude.com/en/articles/7996890-where-are-your-servers-located-do-you-host-your-models-on-eu-servers","admitted_context_window_tokens":131072,"dialect":"anthropic_messages","execution_placement":null,"eligibility_sha256":"8892913a3ed87bae501dcfc300c51cb1342ede2956ef3a40d44ce30602f5d939","hosting":"provider_api","reasoning":null,"image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":30,"retention_fact":"Commercial API inputs and outputs are normally retained up to 30 days, with documented legal, policy and safety exceptions.","zero_retention_endpoint":false,"policy_source_url":"https://privacy.claude.com/en/articles/7996866-how-long-do-you-store-my-organization-s-data","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"Anthropic Ireland, Limited","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://www.anthropic.com/legal/commercial-terms","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"claude_haiku","label":"Claude Haiku 5.5","model":"claude-haiku-5-5","provider":"anthropic","data_residency":"US","data_residency_source":"vendor","data_residency_source_url":"https://privacy.claude.com/en/articles/7996890-where-are-your-servers-located-do-you-host-your-models-on-eu-servers","admitted_context_window_tokens":131072,"dialect":"anthropic_messages","execution_placement":null,"eligibility_sha256":"32647fa178a427705d04b7ec6447810da7016d36725c8625bbdc8ea5677e24f7","hosting":"provider_api","reasoning":null,"image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":30,"retention_fact":"Commercial API inputs and outputs are normally retained up to 30 days, with documented legal, policy and safety exceptions.","zero_retention_endpoint":false,"policy_source_url":"https://privacy.claude.com/en/articles/7996866-how-long-do-you-store-my-organization-s-data","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"Anthropic Ireland, Limited","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://www.anthropic.com/legal/commercial-terms","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"nebius_nemotron","label":"Nemotron 3.5 Lightning","model":"nvidia/Nemotron-3_5-Lightning","provider":"nebius","data_residency":"EU","data_residency_source":"vendor","data_residency_source_url":"https://docs.tokenfactory.nebius.com/legal/legal-quick-guide","admitted_context_window_tokens":1048576,"dialect":"openai_chat","execution_placement":null,"eligibility_sha256":"f0586d343f97c23ed000e00ed86236c3c0c91b3300b2763079e8f6879c081e16","hosting":"provider_api","reasoning":null,"image_input":false,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"Without organization-level zero data retention, inputs and outputs may be retained solely for speculative decoding; retained data is stored in Finland in the European Union.","zero_retention_endpoint":false,"policy_source_url":"https://docs.tokenfactory.nebius.com/legal/legal-quick-guide","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"Nebius B.V.","operator_country":"Netherlands","governing_law":"Netherlands","operator_source_url":"https://docs.nebius.com/legal/agreement","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"openrouter_glm","label":"GLM 5.3","model":"z-ai/glm-5.3","provider":"openrouter","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":"GLOBAL","eligibility_sha256":"99d39ebcf0661d9556db669cfe118c59f94fb253476fa1a23bafb42938cc9a4b","hosting":"provider_api","reasoning":"low","image_input":false,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"OpenRouter retains prompts only on opt-in or for CSAM review, reporting and legal preservation, ZDR traffic included; downstream commercial policies have documented retention exceptions. This route makes no platform-wide zero-retention guarantee.","zero_retention_endpoint":true,"policy_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","policy_checked_at":"2026-10-30","policy_expires_at":"2026-11-29","operator_entity":"OpenRouter, Inc.","operator_country":"United States","governing_law":"New York, United States","operator_source_url":"https://openrouter.ai/terms","downstream_provider":"DeepInfra","downstream_policy_fact":"DeepInfra does not train on API inputs for these models; in-memory processing has documented debugging and security logging exceptions."},{"profile_id":"openrouter_ling","label":"Ling 3.1 Flash","model":"inclusionai/ling-3.1-flash","provider":"openrouter","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":"GLOBAL","eligibility_sha256":"713b2ca20ed060398e1589baa4f2d9759a462e7985c2c4ec8456e8216ed949d0","hosting":"provider_api","reasoning":"off","image_input":false,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"OpenRouter retains prompts only on opt-in or for CSAM review, reporting and legal preservation, ZDR traffic included; downstream commercial policies have documented retention exceptions. This route makes no platform-wide zero-retention guarantee.","zero_retention_endpoint":true,"policy_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","policy_checked_at":"2026-10-30","policy_expires_at":"2026-11-29","operator_entity":"OpenRouter, Inc.","operator_country":"United States","governing_law":"New York, United States","operator_source_url":"https://openrouter.ai/terms","downstream_provider":"DeepInfra","downstream_policy_fact":"DeepInfra does not train on API inputs for these models and does not store submitted data on disk; it holds it in memory during inference."},{"profile_id":"gpt_terra","label":"GPT-6.1 Terra","model":"gpt-6.1-terra","provider":"openai","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://developers.openai.com/api/docs/guides/your-data","admitted_context_window_tokens":131072,"dialect":"openai_responses","execution_placement":null,"eligibility_sha256":"09e58e0478782db24bd9cdca7a8d04805716045e622830c9770fd823a083c6f7","hosting":"provider_api","reasoning":"medium","image_input":true,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":30,"retention_fact":"Chat Completions abuse-monitoring logs are normally retained up to 30 days, with documented legal and safety exceptions.","zero_retention_endpoint":false,"policy_source_url":"https://platform.openai.com/docs/models/default-usage-policies-by-endpoint","policy_checked_at":"2026-10-30","policy_expires_at":"2027-01-28","operator_entity":"OpenAI Ireland Ltd.","operator_country":"Ireland","governing_law":"Ireland","operator_source_url":"https://openai.com/policies/services-agreement/","downstream_provider":null,"downstream_policy_fact":null},{"profile_id":"openrouter_gemini_flash","label":"Gemini 3.8 Flash","model":"google/gemini-3.8-flash","provider":"openrouter","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":"GLOBAL","eligibility_sha256":"fe98eada28b9f5521600959ed87e6634923e1b10136c053ceae485919fb85ba8","hosting":"provider_api","reasoning":"medium","image_input":false,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"OpenRouter retains prompts only on opt-in or for CSAM review, reporting and legal preservation, ZDR traffic included; downstream commercial policies have documented retention exceptions. This route makes no platform-wide zero-retention guarantee.","zero_retention_endpoint":false,"policy_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","policy_checked_at":"2026-10-30","policy_expires_at":"2026-11-29","operator_entity":"OpenRouter, Inc.","operator_country":"United States","governing_law":"New York, United States","operator_source_url":"https://openrouter.ai/terms","downstream_provider":"Google","downstream_policy_fact":"Google does not train on Vertex AI customer data without permission; prompts its abuse classifiers flag may be stored up to 90 days (OpenRouter Safety Retention on every Vertex endpoint since 2026-10-18), so this endpoint is not strict zero-retention."},{"profile_id":"openrouter_gemini_argon","label":"Gemini 4 Argon","model":"google/gemini-4-argon","provider":"openrouter","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":"GLOBAL","eligibility_sha256":"80efc4df5597ab3e9d4584bb065e381a2ae91b4f5d3f9187d6de7e3e568035d2","hosting":"provider_api","reasoning":"high","image_input":false,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"OpenRouter retains prompts only on opt-in or for CSAM review, reporting and legal preservation, ZDR traffic included; downstream commercial policies have documented retention exceptions. This route makes no platform-wide zero-retention guarantee.","zero_retention_endpoint":false,"policy_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","policy_checked_at":"2026-10-30","policy_expires_at":"2026-11-29","operator_entity":"OpenRouter, Inc.","operator_country":"United States","governing_law":"New York, United States","operator_source_url":"https://openrouter.ai/terms","downstream_provider":"Google","downstream_policy_fact":"Google does not train on Vertex AI customer data without permission; prompts its abuse classifiers flag may be stored up to 90 days (OpenRouter Safety Retention on every Vertex endpoint since 2026-10-18), so this endpoint is not strict zero-retention."},{"profile_id":"openrouter_grok","label":"Grok 4.7","model":"x-ai/grok-4.7","provider":"openrouter","data_residency":"GLOBAL","data_residency_source":"vendor","data_residency_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","admitted_context_window_tokens":131072,"dialect":"openai_chat","execution_placement":"GLOBAL","eligibility_sha256":"bf4c95e82a447814818183288ba7d8d8673453403ff93817fe6e3a8617968715","hosting":"provider_api","reasoning":"low","image_input":false,"training_permitted":false,"retention_mode":"published_with_exceptions","retention_stated_days":null,"retention_fact":"OpenRouter retains prompts only on opt-in or for CSAM review, reporting and legal preservation, ZDR traffic included; downstream commercial policies have documented retention exceptions. This route makes no platform-wide zero-retention guarantee.","zero_retention_endpoint":true,"policy_source_url":"https://openrouter.ai/docs/guides/privacy/provider-logging","policy_checked_at":"2026-10-30","policy_expires_at":"2026-11-29","operator_entity":"OpenRouter, Inc.","operator_country":"United States","governing_law":"New York, United States","operator_source_url":"https://openrouter.ai/terms","downstream_provider":"xAI","downstream_policy_fact":"xAI never trains on API inputs or outputs without permission; under ZDR inputs and outputs are never persisted to disk."}]);
|
|
4
4
|
export const PRODUCT_ROUTES = Object.freeze([{"route_id":"auto","label":"Auto","level":"balanced","region":"GLOBAL","route_sha256":"3d76e34a3fc43eddafb9d16d45ffbb85ce0d7cad72add85101e83322eabb383a","max_provider_switches":1,"candidates":[{"profile_id":"gpt_terra","provider_id":"openai","model_id":"gpt-6.1-terra","jurisdiction":"GLOBAL"},{"profile_id":"flash","provider_id":"tensorx","model_id":"deepseek/deepseek-v4.1-flash-1017","jurisdiction":"EU"}]},{"route_id":"auto_eu","label":"Auto EU","level":"balanced","region":"EU","route_sha256":"a1d9f41cb4ce71bf1925e33a838c78c4009416fe04d05795b3fbb7d4ea482162","max_provider_switches":1,"candidates":[{"profile_id":"flash","provider_id":"tensorx","model_id":"deepseek/deepseek-v4.1-flash-1017","jurisdiction":"EU"},{"profile_id":"glm_flash","provider_id":"tensorx","model_id":"z-ai/glm-5.3-flash","jurisdiction":"EU"}]},{"route_id":"quality","label":"Best quality","level":"quality","region":"GLOBAL","route_sha256":"88474569b15517cbc3b8b004588b41622b16b88529b5178f7a5159d4c535d2fd","max_provider_switches":1,"candidates":[{"profile_id":"astra","provider_id":"openai","model_id":"gpt-6-astra","jurisdiction":"GLOBAL"},{"profile_id":"claude_opus","provider_id":"anthropic","model_id":"claude-opus-5-5","jurisdiction":"US"}]},{"route_id":"quality_eu","label":"Best quality EU","level":"quality","region":"EU","route_sha256":"00fd959a8d8106119969bdb21f19b23edefa772642b34cb4b7d4107aa36c22dc","max_provider_switches":1,"candidates":[{"profile_id":"flash","provider_id":"tensorx","model_id":"deepseek/deepseek-v4.1-flash-1017","jurisdiction":"EU"},{"profile_id":"glm","provider_id":"tensorx","model_id":"z-ai/glm-5.3","jurisdiction":"EU"}]},{"route_id":"economy","label":"Lowest cost","level":"economy","region":"GLOBAL","route_sha256":"f9a74312bb237c9f5fc1af1defa3ed1711d10dca8cd1c0ef0c44ab5ac5b27501","max_provider_switches":1,"candidates":[{"profile_id":"luna","provider_id":"openai","model_id":"gpt-6-luna","jurisdiction":"GLOBAL"},{"profile_id":"claude_haiku","provider_id":"anthropic","model_id":"claude-haiku-5-5","jurisdiction":"US"}]},{"route_id":"economy_eu","label":"Lowest cost EU","level":"economy","region":"EU","route_sha256":"e56851dc6469526a05132b82bc1905d27b4fe4a9143c31c7f3e9df7f1265dcad","max_provider_switches":1,"candidates":[{"profile_id":"glm_flash","provider_id":"tensorx","model_id":"z-ai/glm-5.3-flash","jurisdiction":"EU"},{"profile_id":"flash","provider_id":"tensorx","model_id":"deepseek/deepseek-v4.1-flash-1017","jurisdiction":"EU"}]}]);
|
|
5
5
|
export const EXECUTION_HORIZONS = Object.freeze([{"tier":"free","max_steps":30},{"tier":"pro","max_steps":50},{"tier":"enterprise","max_steps":null}]);
|
|
6
|
-
export const CATALOGUE_ENVELOPE_SHA256 = Object.freeze("
|
|
6
|
+
export const CATALOGUE_ENVELOPE_SHA256 = Object.freeze("39127ad6b8c5fe026872dd15d42e2a052575a1fde28f7a103134e47a899da6d0");
|
|
7
7
|
export const RECOVERY_ACTIONS = Object.freeze({"delete_project":{"label":"Delete a project","href":"/projects"},"upgrade_plan":{"label":"Upgrade your plan","href":"/dashboard/billing"},"wait_for_reset":{"label":"Wait until the UTC reset","href":null}});
|
|
8
8
|
export const EVENT_KINDS = Object.freeze(["source","step_started","step_finished","tool_call","tool_result","synthesis","artifact","terminal","heartbeat","assistant_delta","knowledge_binding","engineering_projection","provider_attempt","clarification","clarification_evidence","input_rows"]);
|
|
9
9
|
export const RETIRED_EVENT_KINDS = Object.freeze(["context_frame","cta","step_label"]);
|
package/src/input.js
CHANGED
|
@@ -80,6 +80,20 @@ export class ComposerInput extends EventEmitter {
|
|
|
80
80
|
this.pasted = ""; this.pasting = false; this.pasteOverflow = false; continue;
|
|
81
81
|
}
|
|
82
82
|
if (this.pending.startsWith(START)) { this.pasting = true; this.pending = this.pending.slice(START.length); continue; }
|
|
83
|
+
// Terminal replies to the writer's own queries are never keys, early or late: a colour
|
|
84
|
+
// report (OSC 10/11) is recognised by its own form, any OSC while the writer awaits one.
|
|
85
|
+
// A bare Alt+] keeps its meaning.
|
|
86
|
+
const report = /^\x1b\](?:1(?:[01](?:;(?:r(?:g(?:b(?::[0-9a-f/]*)?)?)?)?)?)?)?/i.exec(this.pending)?.[0] ?? "";
|
|
87
|
+
if (this.pending.startsWith("\x1b]") && (this.expectReports || report.length > 2)) {
|
|
88
|
+
const end = /\x07|\x1b\\/.exec(this.pending);
|
|
89
|
+
if (!end && this.pending.length < 256 && (this.expectReports || report.length === this.pending.length)) {
|
|
90
|
+
this.escapeTimer = setTimeout(() => { this.pending = ""; }, 300); return;
|
|
91
|
+
}
|
|
92
|
+
const length = end ? end.index + end[0].length : 2;
|
|
93
|
+
const reply = this.pending.slice(0, length); this.pending = this.pending.slice(length);
|
|
94
|
+
if (end) this.emit("report", reply);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
83
97
|
if (this.pending.startsWith("\x1b")) {
|
|
84
98
|
if (this.pending.startsWith("\x1b\x1b")) {
|
|
85
99
|
// Escape followed by an arrow/Alt-Enter keeps that encoded key intact.
|
|
@@ -100,6 +114,7 @@ export class ComposerInput extends EventEmitter {
|
|
|
100
114
|
const escape = /^\x1b\[[0-?]*[ -/]*[@-~]/.exec(this.pending)?.[0];
|
|
101
115
|
if (escape) {
|
|
102
116
|
this.pending = this.pending.slice(escape.length);
|
|
117
|
+
if (/^\x1b\[\d+;\d+R$/.test(escape)) { this.emit("report", escape); continue; }
|
|
103
118
|
if (this.handleKey?.(escape)) continue;
|
|
104
119
|
if (escape === "\x1b[D") this.cursor = this.boundary(-1);
|
|
105
120
|
else if (escape === "\x1b[C") this.cursor = this.boundary(1);
|
package/src/session.js
CHANGED
|
@@ -13,6 +13,7 @@ import { humanCost, runSections } from "./format.js";
|
|
|
13
13
|
import { CATALOGUE_ENVELOPE_SHA256, MODEL_CATALOGUE, PRODUCT_ROUTES } from "./contract.js";
|
|
14
14
|
import { openBrowser } from "./browser.js";
|
|
15
15
|
|
|
16
|
+
export const NEW_FOLDER = "New folder — send an ask or /link an existing Project";
|
|
16
17
|
const AUTH_ERRORS = new Set(["pat_expired", "pat_invalid", "pat_revoked"]);
|
|
17
18
|
const TRANSIENT = new Set(["request_failed", "request_timeout", "terminal_invalid"]);
|
|
18
19
|
const MEDIA = { ".txt": "text/plain", ".md": "text/markdown", ".csv": "text/csv", ".json": "application/json",
|
|
@@ -45,6 +46,8 @@ export class Session extends EventEmitter {
|
|
|
45
46
|
projectLabel: this.project?.name ?? (this.binding ? "Linked Project unavailable" : "New Project on first send"),
|
|
46
47
|
status: activity + (canEdit ? "" : " · Read-only — Viewer") + (projection.terminal ? ` · ${projection.elapsed}` : ""), draft: this.draft, prompt: this.prompt, busy: this.busy,
|
|
47
48
|
offline: this.offline, issue: this.issue, login: this.loginChallenge, release: this.release,
|
|
49
|
+
// First use: signed in and online, the folder resolved to no binding/Project/Run, nothing pending.
|
|
50
|
+
firstUse: !!this.account && !this.offline && !this.busy && !this.project && !this.run && this.status === NEW_FOLDER,
|
|
48
51
|
attachments: this.attachments.map(item => ({ path: item.path, size: item.bytes.length })),
|
|
49
52
|
lead: "Laplace", location: "Vecteur cloud", model: modelLabel,
|
|
50
53
|
run: projection, available: { submit: !this.offline && !!this.account && canEdit && !this.busy && !this.run?.open,
|
|
@@ -252,7 +255,7 @@ export class Session extends EventEmitter {
|
|
|
252
255
|
this.project = this.projects.find(project => project.id === binding.projectId) ?? null;
|
|
253
256
|
if (!this.project) { this.status = "Linked Project unavailable — /project or /unlink"; this.change(); return; }
|
|
254
257
|
}
|
|
255
|
-
this.status = this.project ? "Ready" :
|
|
258
|
+
this.status = this.project ? "Ready" : NEW_FOLDER;
|
|
256
259
|
const pending = this.registry.state();
|
|
257
260
|
if (pending.pendingRun || pending.pendingCreate) {
|
|
258
261
|
this.followController?.abort(); this.generation++; this.run = null;
|
package/src/terminal.js
CHANGED
|
@@ -6,17 +6,36 @@ import { preferences } from "./preferences.js";
|
|
|
6
6
|
import { openBrowser } from "./browser.js";
|
|
7
7
|
import { readFileSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
|
+
import { stripVTControlCharacters } from "node:util";
|
|
10
|
+
import stringWidth from "string-width";
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
const measure = value => /^[\x20-\x7e]*$/.test(value) ? value.length : stringWidth(value);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The only stdout writer. It never clears the screen/scrollback or enters alternate screen.
|
|
16
|
+
* Rich mode parks the cursor (hidden) at column 1 of the first live row: everything from there
|
|
17
|
+
* down is the live region, so one erase-below removes exactly it, even after the terminal has
|
|
18
|
+
* reflowed it on resize. A cursor-position report places that row; blank rows between the
|
|
19
|
+
* transcript and the bottom-anchored composer are cursor movement, never printed history.
|
|
20
|
+
*/
|
|
11
21
|
export class TerminalWriter {
|
|
12
|
-
constructor(stdout, plain) {
|
|
22
|
+
constructor(stdout, plain) {
|
|
23
|
+
this.stdout = stdout; this.plain = plain; this.drawn = false; this.frame = null;
|
|
24
|
+
this.row = null; this.awaiting = 0; this.since = 0;
|
|
25
|
+
}
|
|
13
26
|
erase() {
|
|
14
|
-
if (!this.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
if (!this.drawn) return "";
|
|
28
|
+
this.drawn = false;
|
|
29
|
+
return "\r\x1b[J";
|
|
30
|
+
}
|
|
31
|
+
clearLive() { this.frame = null; const bytes = this.erase(); if (bytes) this.stdout.write(bytes); }
|
|
32
|
+
// Display rows the terminal used for appended text; rich lines are narrower than the window.
|
|
33
|
+
advance(text) {
|
|
34
|
+
const columns = Math.max(1, this.stdout.columns ?? 80);
|
|
35
|
+
const rows = text.split("\n").reduce((sum, line) => sum + Math.max(1, Math.ceil(measure(stripVTControlCharacters(line)) / columns)), 0);
|
|
36
|
+
if (this.awaiting) this.since += rows;
|
|
37
|
+
else if (this.row) this.row = Math.min(this.stdout.rows ?? 24, this.row + rows);
|
|
18
38
|
}
|
|
19
|
-
clearLive() { const bytes = this.erase(); if (bytes) this.stdout.write(bytes); }
|
|
20
39
|
progress(view) {
|
|
21
40
|
const subject = view.run?.subject?.id;
|
|
22
41
|
if (subject !== this.subject) { this.subject = subject; this.lastProgress = null; this.lastQuestion = null; }
|
|
@@ -30,17 +49,59 @@ export class TerminalWriter {
|
|
|
30
49
|
if (question) this.append(safe(`Vecteur needs your answer — /answer\n${question.questions.map(row => row.question).join("\n")}`));
|
|
31
50
|
}
|
|
32
51
|
}
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
/** Scrollback text, then the given live frame in the same write: the composer never blinks out. */
|
|
53
|
+
append(text, frame = null) {
|
|
54
|
+
const body = text.replace(/\n+$/, "");
|
|
55
|
+
let bytes = `${this.erase()}${body}\n`;
|
|
56
|
+
if (!this.plain) this.advance(body);
|
|
57
|
+
if (frame) { this.frame = frame; bytes += this.render(); }
|
|
58
|
+
this.stdout.write(bytes);
|
|
59
|
+
}
|
|
60
|
+
draw(frame) {
|
|
35
61
|
if (this.plain) return;
|
|
62
|
+
this.frame = typeof frame === "string" ? { top: [], bottom: frame.replace(/\n+$/, "").split("\n") } : frame;
|
|
36
63
|
// One write per frame: erase and redraw land together, so the spinner never flickers.
|
|
37
|
-
const
|
|
38
|
-
|
|
64
|
+
const bytes = this.render();
|
|
65
|
+
if (bytes) this.stdout.write(bytes);
|
|
66
|
+
}
|
|
67
|
+
render() {
|
|
68
|
+
if (this.plain || !this.frame || this.awaiting) return "";
|
|
69
|
+
const rows = this.stdout.rows ?? 24;
|
|
70
|
+
const lines = [...this.frame.top, ...this.frame.bottom].slice(-Math.max(1, rows - 1));
|
|
71
|
+
const gap = this.row ? Math.max(0, rows - this.row + 1 - lines.length) : 0;
|
|
72
|
+
const at = Math.max(0, lines.length - this.frame.bottom.length);
|
|
73
|
+
const body = [...lines.slice(0, at), ...Array(gap).fill(""), ...lines.slice(at)];
|
|
74
|
+
const up = body.length - 1;
|
|
75
|
+
if (this.row) this.row = Math.max(1, Math.min(this.row, rows - body.length + 1));
|
|
76
|
+
const erase = this.erase(); this.drawn = true;
|
|
77
|
+
return `${erase}${body.join("\n")}${up ? `\x1b[${up}A` : ""}\r`;
|
|
78
|
+
}
|
|
79
|
+
/** Ask the terminal where the live region starts; frames wait for the answer (or 300 ms). */
|
|
80
|
+
locate(query = "") {
|
|
81
|
+
if (this.plain) return;
|
|
82
|
+
this.awaiting++; this.since = 0;
|
|
83
|
+
this.stdout.write(`${query}\x1b[6n`);
|
|
84
|
+
clearTimeout(this.locateTimer);
|
|
85
|
+
this.locateTimer = setTimeout(() => { this.awaiting = 0; this.row = null; this.onLocated?.(); }, 300);
|
|
86
|
+
}
|
|
87
|
+
located(row, column) {
|
|
88
|
+
if (!this.awaiting || --this.awaiting) return;
|
|
89
|
+
clearTimeout(this.locateTimer);
|
|
90
|
+
const rows = this.stdout.rows ?? 24;
|
|
91
|
+
let bytes = "";
|
|
92
|
+
// Another program left the cursor mid-line: start the live region on the next row.
|
|
93
|
+
if (column > 1 && !this.since) { bytes = "\r\n"; row++; }
|
|
94
|
+
this.row = Math.max(1, Math.min(rows, row + this.since));
|
|
95
|
+
if (bytes) this.stdout.write(bytes);
|
|
96
|
+
this.onLocated?.();
|
|
39
97
|
}
|
|
40
98
|
resized(plain) {
|
|
41
|
-
// Reflow
|
|
42
|
-
|
|
99
|
+
// Reflow moved the old frame but not the parked cursor: erase below it, then re-locate.
|
|
100
|
+
const bytes = this.drawn ? "\r\x1b[J" : "";
|
|
101
|
+
this.drawn = false; this.frame = null; this.row = null; this.plain = plain;
|
|
102
|
+
if (bytes) this.stdout.write(bytes);
|
|
43
103
|
}
|
|
104
|
+
close() { clearTimeout(this.locateTimer); this.awaiting = 0; }
|
|
44
105
|
}
|
|
45
106
|
|
|
46
107
|
export async function startTerminal(options = {}, { stdin = process.stdin, stdout = process.stdout, env = process.env } = {}) {
|
|
@@ -51,6 +112,8 @@ export async function startTerminal(options = {}, { stdin = process.stdin, stdou
|
|
|
51
112
|
}
|
|
52
113
|
let rich = !options.plain && !prefs.plain && env.TERM !== "dumb";
|
|
53
114
|
const color = !options.noColor && env.NO_COLOR === undefined;
|
|
115
|
+
// Colour roles start palette-relative; a terminal that reports its own colours gets exact blends.
|
|
116
|
+
let theme = color; const reported = {};
|
|
54
117
|
const layout = await import("./tui/view.js");
|
|
55
118
|
const isPlain = () => !rich || ((stdout.columns ?? 80) < 40 || (stdout.rows ?? 24) < 8);
|
|
56
119
|
const writer = new TerminalWriter(stdout, isPlain());
|
|
@@ -67,6 +130,9 @@ export async function startTerminal(options = {}, { stdin = process.stdin, stdou
|
|
|
67
130
|
let vendorOwnsTerminal = false, resizedDuringHandoff = false; const heldMessages = [];
|
|
68
131
|
// Rich scrollback streams each finished step as one line; the answer block then skips them.
|
|
69
132
|
const streamed = { run: null, events: 0, steps: 0 };
|
|
133
|
+
// The identity header is live until the first transcript block, then committed once.
|
|
134
|
+
let headerLive = true, cursorHidden = false;
|
|
135
|
+
const directory = () => session.cwd === homedir() || session.cwd.startsWith(homedir() + "/") ? "~" + session.cwd.slice(homedir().length) : session.cwd;
|
|
70
136
|
const history = []; let historyIndex = 0, historyDraft = "", historyAccount;
|
|
71
137
|
const syncInput = (value, cursor = value === input.text ? input.cursor : value.length) => { suppressed = true; input.set(value); input.cursor = cursor; suppressed = false; };
|
|
72
138
|
const focusChanged = () => {
|
|
@@ -82,15 +148,41 @@ export async function startTerminal(options = {}, { stdin = process.stdin, stdou
|
|
|
82
148
|
if (!input.text || /^[\d, ]+$/.test(input.text)) return prompt.options;
|
|
83
149
|
return prompt.options.filter(item => item.label.toLowerCase().includes(input.text.toLowerCase()));
|
|
84
150
|
};
|
|
85
|
-
const frameView = () => ({ ...session.view(), version, channel,
|
|
151
|
+
const frameView = () => ({ ...session.view(), version, channel, header: headerLive, directory: directory(),
|
|
86
152
|
focus: localPicker ?? session.prompt, choices: choices(), selected,
|
|
87
153
|
palette: palette(), paletteSelected, loginAction });
|
|
154
|
+
const live = () => layout.richLayout(frameView(), input.text, input.cursor,
|
|
155
|
+
{ columns: stdout.columns ?? 80, rows: stdout.rows ?? 24, color: theme, tick: prefs.reducedMotion ? null : tick });
|
|
88
156
|
const draw = () => {
|
|
89
157
|
clearTimeout(frameTimer); frameTimer = null;
|
|
90
158
|
if (vendorOwnsTerminal) return;
|
|
91
|
-
if (!session.closed && !writer.plain) writer.draw(
|
|
92
|
-
{ columns: stdout.columns ?? 80, rows: stdout.rows ?? 24, color, tick: prefs.reducedMotion ? null : tick }));
|
|
159
|
+
if (!session.closed && !writer.plain) writer.draw(live());
|
|
93
160
|
};
|
|
161
|
+
// The real cursor stays hidden in rich mode (the composer draws its own caret) and always
|
|
162
|
+
// returns on exit, error, plain fallback and vendor handoff.
|
|
163
|
+
const showCursor = visible => {
|
|
164
|
+
if (env.TERM === "dumb" || visible !== cursorHidden) return;
|
|
165
|
+
cursorHidden = !visible; stdout.write(visible ? "\x1b[0m\x1b[?25h" : "\x1b[?25l");
|
|
166
|
+
};
|
|
167
|
+
const restoreCursor = () => showCursor(true);
|
|
168
|
+
const commitHeader = () => {
|
|
169
|
+
if (writer.plain || !headerLive) return;
|
|
170
|
+
headerLive = false; writer.append(layout.richHeader(frameView(), stdout.columns ?? 80, theme).join("\n"));
|
|
171
|
+
};
|
|
172
|
+
// Transcript blocks (asks, answers, steps, usage) commit the header above them; notices printed
|
|
173
|
+
// before the first ask stay above the live identity, which keeps owning the current facts.
|
|
174
|
+
const print = (text, transcript = true) => {
|
|
175
|
+
if (transcript) commitHeader();
|
|
176
|
+
writer.append(text, writer.plain || session.closed || vendorOwnsTerminal ? null : live());
|
|
177
|
+
};
|
|
178
|
+
writer.onLocated = () => {
|
|
179
|
+
input.expectReports = false;
|
|
180
|
+
if (theme && reported.fg && reported.bg && theme === color) {
|
|
181
|
+
theme = layout.paletteTheme(reported, ["truecolor", "24bit"].includes(env.COLORTERM));
|
|
182
|
+
}
|
|
183
|
+
draw();
|
|
184
|
+
};
|
|
185
|
+
const locate = (query = "") => { if (!writer.plain) { input.expectReports ||= !!query; writer.locate(query); } };
|
|
94
186
|
const schedule = () => { if (!frameTimer && !vendorOwnsTerminal) frameTimer = setTimeout(() => { frameTimer = null; draw(); }, 40); };
|
|
95
187
|
const printPlainPrompt = prompt => writer.append(safe(`${prompt.title}\n${prompt.options.map((row, i) => `${i + 1}. ${row.label}`).join("\n")}\nEnter a choice; Esc returns to your draft.`));
|
|
96
188
|
const maybeUpdate = () => {
|
|
@@ -111,8 +203,8 @@ export async function startTerminal(options = {}, { stdin = process.stdin, stdou
|
|
|
111
203
|
if (event?.kind !== "step_finished") continue;
|
|
112
204
|
const [row] = taskLines([event]);
|
|
113
205
|
if (!row) continue;
|
|
114
|
-
if (!streamed.steps++)
|
|
115
|
-
|
|
206
|
+
if (!streamed.steps++) print("");
|
|
207
|
+
print(layout.richMessage({ kind: "step", text: row.split("\n").map(part => part.trim().replace(/^[▸·]\s*/, "")).filter(Boolean).join(" · ") }, stdout.columns ?? 80, theme));
|
|
116
208
|
}
|
|
117
209
|
}
|
|
118
210
|
if (process.connected && process.send && env.VECTEUR_UPDATE_SESSION && session.release?.compatible
|
|
@@ -146,13 +238,13 @@ export async function startTerminal(options = {}, { stdin = process.stdin, stdou
|
|
|
146
238
|
});
|
|
147
239
|
session.on("preferences", value => {
|
|
148
240
|
prefs = value; rich = !options.plain && !prefs.plain && env.TERM !== "dumb";
|
|
149
|
-
writer.resized(isPlain()); session.change(); draw();
|
|
241
|
+
writer.resized(isPlain()); showCursor(writer.plain); locate(); session.change(); draw();
|
|
150
242
|
});
|
|
151
243
|
session.on("message", message => {
|
|
152
244
|
if (vendorOwnsTerminal) { heldMessages.push(message); return; }
|
|
153
|
-
|
|
154
|
-
{ stepsShown: message.runId && message.runId === streamed.run ? streamed.steps : 0 }));
|
|
155
|
-
plainDraft = "";
|
|
245
|
+
print(writer.plain ? safe(message.text) : layout.richMessage(message, stdout.columns ?? 80, theme,
|
|
246
|
+
{ stepsShown: message.runId && message.runId === streamed.run ? streamed.steps : 0 }), ["user", "answer", "cost"].includes(message.kind));
|
|
247
|
+
plainDraft = "";
|
|
156
248
|
});
|
|
157
249
|
session.on("copy", answer => {
|
|
158
250
|
if (vendorOwnsTerminal) return;
|
|
@@ -218,7 +310,7 @@ export async function startTerminal(options = {}, { stdin = process.stdin, stdou
|
|
|
218
310
|
clearTimeout(frameTimer); frameTimer = null; clearInterval(clockTimer); clockTimer = null;
|
|
219
311
|
stdin.off("data", onData); stdin.setRawMode(false); stdin.pause();
|
|
220
312
|
process.off("SIGINT", onSignal);
|
|
221
|
-
writer.clearLive(); if (env.TERM !== "dumb") stdout.write("\x1b[?2004l");
|
|
313
|
+
writer.clearLive(); commitHeader(); showCursor(true); if (env.TERM !== "dumb") stdout.write("\x1b[?2004l");
|
|
222
314
|
const { VENDORS, executingServer, runVendor, vendorArgv } = await import("./vendors.js");
|
|
223
315
|
writer.append(`Opening your ${VENDORS[vendor]} terminal with Vecteur attached for this session only. Exit it to return.`);
|
|
224
316
|
let note;
|
|
@@ -231,6 +323,8 @@ export async function startTerminal(options = {}, { stdin = process.stdin, stdou
|
|
|
231
323
|
if (env.TERM !== "dumb") stdout.write("\x1b[?2004h");
|
|
232
324
|
stdin.setRawMode(true); stdin.resume(); stdin.on("data", onData);
|
|
233
325
|
}
|
|
326
|
+
// The vendor's output is history now; find where it left the cursor before drawing again.
|
|
327
|
+
writer.resized(isPlain()); showCursor(writer.plain); locate();
|
|
234
328
|
if (resizedDuringHandoff) { resizedDuringHandoff = false; onResize(); }
|
|
235
329
|
for (const message of heldMessages.splice(0)) session.emit("message", message);
|
|
236
330
|
session.message(note); session.change();
|
|
@@ -295,7 +389,13 @@ export async function startTerminal(options = {}, { stdin = process.stdin, stdou
|
|
|
295
389
|
return false;
|
|
296
390
|
};
|
|
297
391
|
input.on("details", () => { if (!session.prompt) void execute("/details"); });
|
|
298
|
-
input.on("redraw", () => { writer.clearLive(); draw(); });
|
|
392
|
+
input.on("redraw", () => { writer.clearLive(); locate(); draw(); });
|
|
393
|
+
input.on("report", report => {
|
|
394
|
+
const position = /^\x1b\[(\d+);(\d+)R$/.exec(report);
|
|
395
|
+
if (position) { writer.located(Number(position[1]), Number(position[2])); return; }
|
|
396
|
+
const value = /^\x1b\]1([01]);rgb:([0-9a-f]+)\/([0-9a-f]+)\/([0-9a-f]+)/i.exec(report);
|
|
397
|
+
if (value) reported[value[1] === "0" ? "fg" : "bg"] = value.slice(2).map(hex => Math.round(parseInt(hex, 16) / (16 ** hex.length - 1) * 255));
|
|
398
|
+
});
|
|
299
399
|
input.on("exit", () => { try { session.close(); } catch (error) { session.report(error); } });
|
|
300
400
|
const onData = chunk => {
|
|
301
401
|
if (env.TERM === "dumb" && !input.pasting && (String(chunk).match(/[\r\n]/g)?.length ?? 0) > 1) {
|
|
@@ -307,8 +407,8 @@ export async function startTerminal(options = {}, { stdin = process.stdin, stdou
|
|
|
307
407
|
// The vendor owns the TTY: not one byte; the new geometry is applied once on restore.
|
|
308
408
|
if (vendorOwnsTerminal) { resizedDuringHandoff = true; return; }
|
|
309
409
|
const plain = isPlain(), changed = plain !== writer.plain;
|
|
310
|
-
writer.resized(plain); lastPlainIdentity = "";
|
|
311
|
-
if (changed)
|
|
410
|
+
writer.resized(plain); lastPlainIdentity = ""; showCursor(plain); locate();
|
|
411
|
+
if (changed) print(plain ? "Plain view — terminal is small; all actions remain available." : "Rich view restored.", false);
|
|
312
412
|
session.change(); draw();
|
|
313
413
|
};
|
|
314
414
|
const onSignal = () => { try { session.close(); } catch {} };
|
|
@@ -316,15 +416,17 @@ export async function startTerminal(options = {}, { stdin = process.stdin, stdou
|
|
|
316
416
|
stdin.setRawMode(true); stdin.resume(); stdin.on("data", onData); stdout.on("resize", onResize);
|
|
317
417
|
process.once("SIGTERM", onSignal); process.once("SIGHUP", onSignal); process.once("SIGINT", onSignal);
|
|
318
418
|
if (env.TERM !== "dumb") stdout.write("\x1b[?2004h");
|
|
319
|
-
|
|
320
|
-
|
|
419
|
+
process.on("exit", restoreCursor);
|
|
420
|
+
if (writer.plain) writer.append(`Vecteur ${version}${channel === "preview" ? " · Preview" : ""} · ${safe(session.origin)}`);
|
|
421
|
+
else { showCursor(false); locate(color ? "\x1b]10;?\x1b\\\x1b]11;?\x1b\\" : ""); }
|
|
321
422
|
draw();
|
|
322
423
|
void session.initialize().then(() => { if (!input.text) syncInput(session.draft); draw(); }).catch(error => session.report(error));
|
|
323
424
|
try { await done; }
|
|
324
425
|
finally {
|
|
325
426
|
clearTimeout(frameTimer); clearInterval(clockTimer); input.close(); stdin.off("data", onData); stdout.off("resize", onResize);
|
|
326
427
|
process.off("SIGTERM", onSignal); process.off("SIGHUP", onSignal); process.off("SIGINT", onSignal);
|
|
327
|
-
writer.clearLive();
|
|
428
|
+
writer.clearLive(); writer.close(); commitHeader(); showCursor(true); process.off("exit", restoreCursor);
|
|
429
|
+
if (env.TERM !== "dumb") stdout.write("\x1b[?2004l");
|
|
328
430
|
stdin.setRawMode(false); stdin.pause();
|
|
329
431
|
stdout.write(session.restart ? "Restarting Vecteur…\n" : session.run?.open ? "Detached. The cloud Run continues; run vecteur here to follow it.\n" : "Session closed.\n");
|
|
330
432
|
}
|
package/src/tui/view.js
CHANGED
|
@@ -2,19 +2,101 @@ import stringWidth from "string-width";
|
|
|
2
2
|
import wrapAnsi from "wrap-ansi";
|
|
3
3
|
import { parseMarkdown } from "../markdown.js";
|
|
4
4
|
import { terminalText } from "../format.js";
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
|
|
5
|
+
|
|
6
|
+
// Colour roles, each one SGR parameter list. A theme of `null` is NO_COLOR / --no-color: no
|
|
7
|
+
// styling at all. Without a palette report every role stays relative to the user's own palette:
|
|
8
|
+
// faint for secondary/quiet text and rules, no input fill (a rule-only composer).
|
|
9
|
+
export const DEFAULT_THEME = Object.freeze({ accent: "38;5;81", attention: "33", secondary: "2", quiet: "2",
|
|
10
|
+
rule: "2", band: "", strong: "1", em: "3", link: "4" });
|
|
11
|
+
const themeOf = color => color === true ? DEFAULT_THEME : color || null;
|
|
12
|
+
const RESET = { 1: "22", 2: "22", 3: "23", 4: "24", 7: "27", 38: "39", 48: "49" };
|
|
13
|
+
// Targeted resets, never SGR 0: a role inside the input band must not clear the band.
|
|
14
|
+
const style = (value, code, theme) => {
|
|
15
|
+
if (!theme || !code || !value) return value;
|
|
16
|
+
const close = []; const parts = code.split(";");
|
|
17
|
+
for (let i = 0; i < parts.length; i++) {
|
|
18
|
+
const n = Number(parts[i]);
|
|
19
|
+
if (n === 38 || n === 48) { close.push(RESET[n]); i += parts[i + 1] === "5" ? 2 : 4; }
|
|
20
|
+
else close.push(RESET[n] ?? ((n >= 30 && n <= 37) || (n >= 90 && n <= 97) ? "39" : "0"));
|
|
21
|
+
}
|
|
22
|
+
return `\x1b[${code}m${value}\x1b[${[...new Set(close)].join(";")}m`;
|
|
23
|
+
};
|
|
24
|
+
const paint = (value, role, theme) => style(value, theme?.[role], theme);
|
|
25
|
+
|
|
26
|
+
// Terminal palette → roles. The reference values are blends of the terminal's own foreground
|
|
27
|
+
// toward its background; text roles are pulled back toward the foreground until they reach 4.5:1.
|
|
28
|
+
const channel = v => { v /= 255; return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; };
|
|
29
|
+
const luminance = ([r, g, b]) => 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
|
|
30
|
+
export const contrast = (a, b) => { const [x, y] = [luminance(a), luminance(b)].sort((p, q) => q - p); return (x + 0.05) / (y + 0.05); };
|
|
31
|
+
const mix = (from, to, t) => from.map((v, i) => Math.round(v + (to[i] - v) * t));
|
|
32
|
+
const cube = [0, 95, 135, 175, 215, 255];
|
|
33
|
+
function xterm256([r, g, b]) {
|
|
34
|
+
const near = v => cube.reduce((best, c, i) => Math.abs(c - v) < Math.abs(cube[best] - v) ? i : best, 0);
|
|
35
|
+
const [i, j, k] = [near(r), near(g), near(b)], rgb = [cube[i], cube[j], cube[k]];
|
|
36
|
+
const level = Math.max(0, Math.min(23, Math.round(((r + g + b) / 3 - 8) / 10))), grey = 8 + level * 10;
|
|
37
|
+
const distance = c => (c[0] - r) ** 2 + (c[1] - g) ** 2 + (c[2] - b) ** 2;
|
|
38
|
+
return distance([grey, grey, grey]) < distance(rgb) ? 232 + level : 16 + 36 * i + 6 * j + k;
|
|
39
|
+
}
|
|
40
|
+
// The colour a 256-colour terminal actually shows for an index.
|
|
41
|
+
const shown = index => index >= 232 ? Array(3).fill(8 + (index - 232) * 10)
|
|
42
|
+
: [Math.floor((index - 16) / 36), Math.floor((index - 16) / 6) % 6, (index - 16) % 6].map(i => cube[i]);
|
|
43
|
+
export function paletteTheme({ fg, bg }, truecolor = false) {
|
|
44
|
+
// Every check runs on the colour the terminal will render, never on the pre-rounding value.
|
|
45
|
+
const render = rgb => truecolor ? rgb : shown(xterm256(rgb));
|
|
46
|
+
const code = (rgb, layer = 38) => truecolor ? `${layer};2;${rgb.join(";")}` : `${layer};5;${xterm256(rgb)}`;
|
|
47
|
+
const band = render(mix(fg, bg, 0.88));
|
|
48
|
+
// Pull toward the foreground until legible on the background and, for roles drawn inside the
|
|
49
|
+
// input band (placeholder, caret), on the band too.
|
|
50
|
+
const readable = (from, grounds) => {
|
|
51
|
+
for (let t = 0; t <= 1; t += 0.05) {
|
|
52
|
+
const value = render(mix(from, fg, t));
|
|
53
|
+
if (grounds.every(ground => contrast(value, ground) >= 4.5)) return value;
|
|
54
|
+
}
|
|
55
|
+
return render(fg);
|
|
56
|
+
};
|
|
57
|
+
const dark = luminance(bg) < 0.4;
|
|
58
|
+
return Object.freeze({ ...DEFAULT_THEME,
|
|
59
|
+
accent: code(readable(dark ? [105, 215, 239] : [0, 120, 150], [bg, band])),
|
|
60
|
+
attention: code(readable(dark ? [233, 173, 12] : [154, 103, 0], [bg])),
|
|
61
|
+
secondary: code(readable(mix(fg, bg, 0.25), [bg, band])), quiet: code(readable(mix(fg, bg, 0.42), [bg])),
|
|
62
|
+
rule: code(mix(fg, bg, 0.62)), band: code(band, 48) });
|
|
63
|
+
}
|
|
64
|
+
|
|
8
65
|
const SPINNER = ["·", "✢", "✳", "✶", "✻", "✽", "✻", "✶", "✳", "✢"];
|
|
66
|
+
const PROSE = 88; // readable measure on wide terminals; tables and code may use the full width
|
|
67
|
+
const NEW_PROJECT = "New Project on first send";
|
|
9
68
|
const printableAscii = /^[\x20-\x7e]*$/;
|
|
10
69
|
const measure = value => printableAscii.test(value) ? value.length : stringWidth(value);
|
|
70
|
+
// Each wrapped row closes whatever style is still open and the next row reopens it: a role never
|
|
71
|
+
// rides a line feed into a row that is later dropped or drawn by someone else (wrap-ansi only
|
|
72
|
+
// re-closes the 16-colour codes, not 256/truecolor ones).
|
|
73
|
+
const CLOSE = { 1: "22", 2: "22", 3: "23", 4: "24", 7: "27" };
|
|
74
|
+
function seal(lines) {
|
|
75
|
+
let open = [];
|
|
76
|
+
return lines.map(line => {
|
|
77
|
+
const head = open.length ? `\x1b[${open.join(";")}m` : "";
|
|
78
|
+
for (const [, list] of line.matchAll(/\x1b\[([0-9;]*)m/g)) {
|
|
79
|
+
const parts = list.split(";");
|
|
80
|
+
for (let i = 0; i < parts.length; i++) {
|
|
81
|
+
const n = Number(parts[i] || 0);
|
|
82
|
+
if (n === 38 || n === 48) { const width = parts[i + 1] === "5" ? 3 : 5; open = open.filter(g => !g.startsWith(`${n};`)); open.push(parts.slice(i, i + width).join(";")); i += width - 1; }
|
|
83
|
+
else if (n === 0) open = [];
|
|
84
|
+
else if (n === 39 || n === 49) open = open.filter(g => !g.startsWith(n === 39 ? "38;" : "48;") && !(n === 39 ? /^(3[0-7]|9[0-7])$/ : /^(4[0-7]|10[0-7])$/).test(g));
|
|
85
|
+
else if (["22", "23", "24", "27"].includes(String(n))) open = open.filter(g => CLOSE[g] !== String(n));
|
|
86
|
+
else open.push(String(n));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const tail = open.length ? `\x1b[${[...new Set(open.map(g => CLOSE[g] ?? (g.startsWith("48") || /^(4[0-7]|10[0-7])$/.test(g) ? "49" : "39")))].join(";")}m` : "";
|
|
90
|
+
return head + line + tail;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
11
93
|
const wrapped = new Map();
|
|
12
94
|
const wrap = (value, width, wordWrap = true) => {
|
|
13
95
|
width = Math.max(1, width);
|
|
14
96
|
const key = value.length <= 512 ? `${width}:${wordWrap}:${value}` : null;
|
|
15
97
|
if (key && wrapped.has(key)) return wrapped.get(key);
|
|
16
98
|
const lines = !value.includes("\n") && measure(value) <= width ? [value]
|
|
17
|
-
: wrapAnsi(value, width, { hard: true, trim: false, wordWrap }).split("\n");
|
|
99
|
+
: seal(wrapAnsi(value, width, { hard: true, trim: false, wordWrap }).split("\n"));
|
|
18
100
|
if (key) {
|
|
19
101
|
if (wrapped.size === 128) wrapped.delete(wrapped.keys().next().value);
|
|
20
102
|
wrapped.set(key, lines);
|
|
@@ -22,62 +104,68 @@ const wrap = (value, width, wordWrap = true) => {
|
|
|
22
104
|
return lines;
|
|
23
105
|
};
|
|
24
106
|
// Prose wraps at word boundaries and drops the break's leading space; the composer keeps every space.
|
|
25
|
-
const prose = (value, width) => wrapAnsi(value, Math.max(1, width), { hard: true, trim: true }).split("\n");
|
|
107
|
+
const prose = (value, width) => seal(wrapAnsi(value, Math.max(1, width), { hard: true, trim: true }).split("\n"));
|
|
108
|
+
// Key hints fold between their " · " segments, never inside one ("? shortcuts" stays whole).
|
|
109
|
+
const fold = (text, width) => text.split("\n").flatMap(part => part.split(" · ").reduce((rows, segment) => {
|
|
110
|
+
if (rows.length && measure(rows.at(-1)) + 3 + measure(segment) <= width) rows[rows.length - 1] += " · " + segment;
|
|
111
|
+
else rows.push(segment);
|
|
112
|
+
return rows;
|
|
113
|
+
}, [])).flatMap(row => wrap(row, width));
|
|
26
114
|
const pad = (value, width) => value + " ".repeat(Math.max(0, width - measure(value)));
|
|
27
115
|
|
|
28
|
-
function inline(tokens,
|
|
116
|
+
function inline(tokens, theme) {
|
|
29
117
|
return tokens.map(token => {
|
|
30
118
|
const href = token.href && /^https?:\/\//i.test(token.href) ? ` <${token.href}>` : "";
|
|
31
119
|
const value = token.text + href;
|
|
32
|
-
return token.kind === "strong" ?
|
|
33
|
-
: token.kind === "em" ?
|
|
34
|
-
: token.kind === "code" ?
|
|
35
|
-
: token.kind === "link" ?
|
|
120
|
+
return token.kind === "strong" ? paint(value, "strong", theme)
|
|
121
|
+
: token.kind === "em" ? paint(value, "em", theme)
|
|
122
|
+
: token.kind === "code" ? paint(value, "accent", theme)
|
|
123
|
+
: token.kind === "link" ? paint(value, "link", theme) : value;
|
|
36
124
|
}).join("");
|
|
37
125
|
}
|
|
38
126
|
|
|
39
127
|
const numeric = /^[-+−~≈<>≤≥±]?\s?\d[\d.,]*(?:\s?[^\s\d]\S*)?$/;
|
|
40
|
-
function table(block, width,
|
|
41
|
-
const grid = [block.header, ...block.rows].map(row => row.map(cell => inline(cell,
|
|
128
|
+
function table(block, width, theme) {
|
|
129
|
+
const grid = [block.header, ...block.rows].map(row => row.map(cell => inline(cell, null)));
|
|
42
130
|
const columns = Math.max(...grid.map(row => row.length));
|
|
43
131
|
const widths = Array.from({ length: columns }, (_, i) => Math.max(1, ...grid.map(row => measure(row[i] ?? ""))));
|
|
44
132
|
if (widths.reduce((a, b) => a + b, 0) + 3 * (columns - 1) > width) {
|
|
45
133
|
// Too wide for the terminal: one record per row, never a clipped column.
|
|
46
134
|
const lines = [];
|
|
47
135
|
block.rows.forEach((row, index) => {
|
|
48
|
-
lines.push(
|
|
49
|
-
row.forEach((cell, column) => lines.push(...wrap(`${
|
|
136
|
+
lines.push(paint(`Record ${index + 1}`, "strong", theme));
|
|
137
|
+
row.forEach((cell, column) => lines.push(...wrap(`${paint(grid[0]?.[column] || `Column ${column + 1}`, "secondary", theme)} ${inline(cell, theme)}`, width)));
|
|
50
138
|
});
|
|
51
139
|
return lines;
|
|
52
140
|
}
|
|
53
|
-
const right = widths.map((_, i) => block.rows.length > 0 && block.rows.every(row => numeric.test(inline(row[i] ?? [],
|
|
141
|
+
const right = widths.map((_, i) => block.rows.length > 0 && block.rows.every(row => numeric.test(inline(row[i] ?? [], null).trim() || "0")));
|
|
54
142
|
const cell = (value, i) => right[i] ? " ".repeat(Math.max(0, widths[i] - measure(value))) + value : pad(value, widths[i]);
|
|
55
|
-
const bar =
|
|
56
|
-
return [
|
|
57
|
-
|
|
143
|
+
const bar = paint(" │ ", "rule", theme);
|
|
144
|
+
return [paint(grid[0].map((value, i) => pad(value, widths[i])).join(" │ "), "strong", theme),
|
|
145
|
+
paint(widths.map(w => "─".repeat(w)).join("─┼─"), "rule", theme),
|
|
58
146
|
...grid.slice(1).map(row => widths.map((_, i) => cell(row[i] ?? "", i)).join(bar))];
|
|
59
147
|
}
|
|
60
148
|
|
|
61
149
|
/** Shared web grammar; only trusted renderer styling can emit terminal controls. */
|
|
62
150
|
export function markdownView(source, width, color = true) {
|
|
63
|
-
const lines = [];
|
|
151
|
+
const theme = themeOf(color), lines = [], measureWidth = Math.min(width, PROSE);
|
|
64
152
|
for (const block of parseMarkdown(terminalText(source))) {
|
|
65
|
-
if (block.kind === "table") lines.push(...table(block, width,
|
|
153
|
+
if (block.kind === "table") lines.push(...table(block, width, theme));
|
|
66
154
|
else if (block.kind === "codeblock") {
|
|
67
|
-
if (block.language) lines.push(
|
|
68
|
-
lines.push(...wrap(block.text, width - 2, false).map(line =>
|
|
155
|
+
if (block.language) lines.push(paint(block.language, "quiet", theme));
|
|
156
|
+
lines.push(...wrap(block.text, width - 2, false).map(line => paint("│ ", "rule", theme) + line));
|
|
69
157
|
} else if (block.kind === "hr") {
|
|
70
|
-
lines.push(
|
|
158
|
+
lines.push(paint("─".repeat(Math.min(width, 48)), "rule", theme));
|
|
71
159
|
} else if (block.kind === "list") {
|
|
72
160
|
block.items.forEach((item, index) => {
|
|
73
161
|
const prefix = block.ordered ? `${index + 1}. ` : "• ";
|
|
74
|
-
prose(inline(item,
|
|
162
|
+
prose(inline(item, theme), measureWidth - prefix.length).forEach((line, i) => lines.push((i ? " ".repeat(prefix.length) : paint(prefix, "quiet", theme)) + line));
|
|
75
163
|
});
|
|
76
164
|
} else {
|
|
77
|
-
let value = inline(block.inline,
|
|
78
|
-
if (block.kind === "heading") value =
|
|
79
|
-
if (block.kind === "quote") lines.push(...prose(value,
|
|
80
|
-
else lines.push(...prose(value,
|
|
165
|
+
let value = inline(block.inline, theme);
|
|
166
|
+
if (block.kind === "heading") value = paint(value, "strong", theme);
|
|
167
|
+
if (block.kind === "quote") lines.push(...prose(value, measureWidth - 2).map(line => paint("│ ", "rule", theme) + line));
|
|
168
|
+
else lines.push(...prose(value, measureWidth));
|
|
81
169
|
}
|
|
82
170
|
lines.push("");
|
|
83
171
|
}
|
|
@@ -88,7 +176,7 @@ export function markdownView(source, width, color = true) {
|
|
|
88
176
|
// One terminal owns this cache. Cursor movement never rewraps unrelated paragraphs.
|
|
89
177
|
let composerText;
|
|
90
178
|
const composerLayouts = new Map();
|
|
91
|
-
function composerViewport(composer, cursor, width, height) {
|
|
179
|
+
function composerViewport(composer, cursor, width, height, theme) {
|
|
92
180
|
if (composerText !== composer) { composerLayouts.clear(); composerText = composer; }
|
|
93
181
|
let composerLayout = composerLayouts.get(width);
|
|
94
182
|
if (!composerLayout) {
|
|
@@ -105,7 +193,9 @@ function composerViewport(composer, cursor, width, height) {
|
|
|
105
193
|
const paragraph = paragraphs.findLast(row => row.offset <= cursor) ?? paragraphs[0];
|
|
106
194
|
const local = cursor - paragraph.offset;
|
|
107
195
|
const before = terminalText(paragraph.text.slice(0, local));
|
|
108
|
-
|
|
196
|
+
// The caret is the one accent mark inside the input: wrapped on its plain glyph, painted after.
|
|
197
|
+
const active = wrap(before + "▌" + terminalText(paragraph.text.slice(local)), width, false)
|
|
198
|
+
.map(line => line.replace("▌", paint("▌", "accent", theme)));
|
|
109
199
|
const cursorRow = paragraph.row + wrap(before + "▌", width, false).length - 1;
|
|
110
200
|
const start = Math.max(0, cursorRow - height + 1);
|
|
111
201
|
const next = paragraphs[paragraphs.indexOf(paragraph) + 1]?.row ?? lines.length;
|
|
@@ -115,27 +205,14 @@ function composerViewport(composer, cursor, width, height) {
|
|
|
115
205
|
scrolling: lines.length > height };
|
|
116
206
|
}
|
|
117
207
|
|
|
118
|
-
/**
|
|
119
|
-
function
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return style(left + text + "─".repeat(Math.max(0, width - 2 - measure(text))) + right, DIM, color);
|
|
208
|
+
/** The composer: a filled input band between two rules; a label may sit in either rule. */
|
|
209
|
+
function composerBand(rows, width, theme, { top = "", bottom = "" } = {}) {
|
|
210
|
+
const rule = label => {
|
|
211
|
+
const text = label ? `── ${trimEnd(label, width - 4)} ` : "";
|
|
212
|
+
return paint(text + "─".repeat(Math.max(0, width - measure(text))), "rule", theme);
|
|
124
213
|
};
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** The once-printed welcome: one calm box, a single accent glyph, no ASCII logo. */
|
|
130
|
-
export function welcome(version, origin, columns = 80, color = true, channel, folder) {
|
|
131
|
-
const width = Math.max(20, columns - 1), inner = width - 4;
|
|
132
|
-
const label = `${version}${channel === "preview" ? " · Preview" : ""}`;
|
|
133
|
-
const where = [folder, origin === "https://vecteur.space" ? "" : origin].filter(Boolean).map(terminalText).join(" · ");
|
|
134
|
-
const head = `${style("▼", ACCENT, color)} ${style("Vecteur", "1", color)} ${style(label, DIM, color)}`;
|
|
135
|
-
const lines = measure(`▼ Vecteur ${label} · Laplace · Vecteur cloud`) <= inner
|
|
136
|
-
? [`${head} ${style("· Laplace · Vecteur cloud", DIM, color)}`] : [head];
|
|
137
|
-
if (where) lines.push(style(trimEnd(` ${where}`, inner), DIM, color));
|
|
138
|
-
return box(lines, width, color).join("\n") + "\n";
|
|
214
|
+
const band = theme?.band;
|
|
215
|
+
return [rule(top), ...rows.map(row => band ? style(pad(row, width), band, theme) : row), rule(bottom)];
|
|
139
216
|
}
|
|
140
217
|
|
|
141
218
|
// Display-cell truncation with an ellipsis; never more work than the visible width needs.
|
|
@@ -155,8 +232,46 @@ function trimStart(value, max) {
|
|
|
155
232
|
return "…" + chars.join("");
|
|
156
233
|
}
|
|
157
234
|
|
|
158
|
-
|
|
159
|
-
|
|
235
|
+
const execution = view => view.location && view.location !== "Vecteur cloud" ? view.location : "Cloud";
|
|
236
|
+
const routeOf = view => `${view.lead ? `${view.lead} · ` : ""}${view.model} · ${execution(view)}`;
|
|
237
|
+
const customOrigin = view => view.origin && view.origin !== "https://vecteur.space" ? view.origin : "";
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Identity: `▼ Vecteur <version>` and the folder, the route group on the right (beneath when
|
|
241
|
+
* narrow). Live while the transcript is empty, then committed once to scrollback.
|
|
242
|
+
*/
|
|
243
|
+
export function richHeader(view, columns = 80, color = true) {
|
|
244
|
+
const theme = themeOf(color), width = Math.max(20, columns - 1);
|
|
245
|
+
const label = `${view.version ?? ""}${view.channel === "preview" ? " · Preview" : ""}`;
|
|
246
|
+
const brand = `${paint("▼", "accent", theme)} ${paint("Vecteur", "strong", theme)} ${paint(label, "quiet", theme)}`;
|
|
247
|
+
const brandWidth = measure(`▼ Vecteur ${label}`);
|
|
248
|
+
// Only a signed-in, online session earns the filled dot; it claims nothing about a server.
|
|
249
|
+
const live = !!view.account && !view.offline;
|
|
250
|
+
const route = trimEnd(routeOf(view), width - 4);
|
|
251
|
+
const routeText = `${paint(live ? "●" : "○", live ? "accent" : "quiet", theme)} ${paint(route, "secondary", theme)}`;
|
|
252
|
+
const routeWidth = measure(route) + 2;
|
|
253
|
+
const lines = ["", brandWidth + 4 + routeWidth <= width
|
|
254
|
+
? brand + " ".repeat(width - brandWidth - routeWidth) + routeText : brand];
|
|
255
|
+
// A non-production origin is never hidden for visual simplicity.
|
|
256
|
+
const origin = customOrigin(view), directory = view.directory ? terminalText(view.directory) : "";
|
|
257
|
+
if (directory && origin && measure(directory) + 3 + measure(origin) <= width - 2) {
|
|
258
|
+
lines.push(` ${paint(directory, "quiet", theme)}${paint(" · ", "quiet", theme)}${paint(terminalText(origin), "accent", theme)}`);
|
|
259
|
+
} else {
|
|
260
|
+
if (directory) lines.push(" " + paint(trimStart(directory, width - 2), "quiet", theme));
|
|
261
|
+
if (origin) lines.push(" " + paint(trimEnd(origin, width - 2), "accent", theme));
|
|
262
|
+
}
|
|
263
|
+
if (lines[1] === brand) lines.push(" " + routeText);
|
|
264
|
+
return lines;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// First-use suggestions are plain text, never a menu; existing commands stay authoritative.
|
|
268
|
+
const ONBOARDING = ["Review a mission brief and find the missing inputs",
|
|
269
|
+
"Compare orbit options for an Earth-observation mission", "Turn requirements into an analysis plan"];
|
|
270
|
+
|
|
271
|
+
/** The live region: `top` (identity, first-use note) above a flexible gap, `bottom` anchored. */
|
|
272
|
+
export function richLayout(view, composer, cursor, options = {}) {
|
|
273
|
+
const { columns = 80, rows = 24, color = true, tick = null } = options;
|
|
274
|
+
const theme = themeOf(color);
|
|
160
275
|
const width = Math.max(20, columns - 1), budget = Math.max(1, rows - 3);
|
|
161
276
|
const active = !!view.run?.subject && !view.run.terminal;
|
|
162
277
|
const readOnly = view.project?.role === "VIEWER", signedOut = view.offline || !view.account;
|
|
@@ -164,6 +279,7 @@ export function richFrame(view, composer, cursor, { columns = 80, rows = 24, col
|
|
|
164
279
|
const runHint = readOnly ? "Read-only · /help · Ctrl+D detach"
|
|
165
280
|
: signedOut ? `${view.offline ? "/resume" : "/login"} · /help · Ctrl+D detach`
|
|
166
281
|
: `${view.run?.question ? "/answer" : "/steer · /answer-now"} · /help${width < 60 ? "\n" : " · "}Esc twice cancel · Ctrl+D detach`;
|
|
282
|
+
const idleHint = `${readOnly ? "Read-only · /project" : signedOut ? "Enter sign in" : view.busy ? "Please wait" : "Enter send"} · / commands · ? shortcuts`;
|
|
167
283
|
if (rows < 12) {
|
|
168
284
|
const focus = view.focus ?? view.prompt;
|
|
169
285
|
const choices = view.choices ?? focus?.options ?? view.palette ?? [];
|
|
@@ -174,17 +290,18 @@ export function richFrame(view, composer, cursor, { columns = 80, rows = 24, col
|
|
|
174
290
|
const status = wrap(terminalText(content), width).slice(0, 1);
|
|
175
291
|
if (choice) status.push(wrap(terminalText(`› ${selected + 1}/${choices.length} ${choice.label ?? '/' + choice.name}`), width)[0]);
|
|
176
292
|
else if (view.login) status.push(['› Open browser', '› Copy link', '› Work offline'][view.loginAction ?? 0]);
|
|
177
|
-
const draft = composerViewport(composer, cursor, width - 2, 1).lines[0];
|
|
293
|
+
const draft = composerViewport(composer, cursor, width - 2, 1, theme).lines[0];
|
|
178
294
|
const hint = focus || choices.length ? '↑↓ Enter choose · Esc back' : view.login ? 'Tab actions · Enter open · Esc offline'
|
|
179
|
-
: active ? runHint :
|
|
180
|
-
return [...status, `${
|
|
295
|
+
: active ? runHint : idleHint;
|
|
296
|
+
return { top: [], bottom: [...status, `${paint("›", "accent", theme)} ${draft}`,
|
|
297
|
+
...fold(hint, width).slice(0, 2).map(row => paint(row, "secondary", theme))].slice(0, budget) };
|
|
181
298
|
}
|
|
182
|
-
if (rows < 8) return wrap(`${view.status} · Ctrl+D exit`, width).slice(0, budget).join("\n");
|
|
183
|
-
const project = view.projectLabel ?? view.project?.name ?? "New Project on first send";
|
|
184
299
|
// The steady footer names the Workspace, never a personal address; /status shows the account.
|
|
185
300
|
const workspaces = view.account?.workspaces ?? [];
|
|
186
301
|
const account = view.account ? (workspaces.find(row => row.id === view.project?.workspace_id) ?? workspaces[0])?.name ?? "Signed in"
|
|
187
302
|
: view.offline ? "Offline" : "Not signed in";
|
|
303
|
+
const projectLabel = view.projectLabel ?? view.project?.name ?? NEW_PROJECT;
|
|
304
|
+
const identity = `${account} · ${projectLabel === NEW_PROJECT ? "New project" : projectLabel}`;
|
|
188
305
|
const compact = width < 60 || rows < 18;
|
|
189
306
|
const prompt = view.focus ?? view.prompt;
|
|
190
307
|
const compactFocus = rows < 24 && (view.focus || view.prompt || view.login || view.palette?.length || view.issue);
|
|
@@ -194,44 +311,39 @@ export function richFrame(view, composer, cursor, { columns = 80, rows = 24, col
|
|
|
194
311
|
const hint = prompt ? prompt.multiple ? "Space toggle · ↑↓ move · Enter confirm · Esc back" : "↑↓ choose · Enter confirm · Esc back · Ctrl+D exit"
|
|
195
312
|
: view.login ? "Tab actions · Enter on empty activates · Esc offline"
|
|
196
313
|
: view.palette?.length ? "↑↓ choose · Tab complete · Enter open · Esc close"
|
|
197
|
-
: active ? runHint
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const identity = trimEnd(`${project === "New Project on first send" ? "New Project" : project} · ${account}`, width);
|
|
204
|
-
const where = trimStart(view.folder ?? ".", Math.max(6, width - measure(identity) - measure(context) - 7));
|
|
205
|
-
const left = where === "." ? identity : `${identity} · ${where}`;
|
|
206
|
-
if (!compact && measure(left) + measure(context) + 4 <= width) {
|
|
207
|
-
footer.push(" " + style(left + " ".repeat(width - 2 - measure(left) - measure(context)) + context, DIM, color));
|
|
314
|
+
: active ? runHint : idleHint;
|
|
315
|
+
// While the identity header is live it owns the route and origin; afterwards the footer does.
|
|
316
|
+
const context = view.header ? identity : compact ? routeOf(view) : `${identity} · ${routeOf(view)}`;
|
|
317
|
+
const footer = [];
|
|
318
|
+
if (!compact && !hint.includes("\n") && measure(context) + 3 + measure(hint) <= width) {
|
|
319
|
+
footer.push(paint(context, "secondary", theme) + " ".repeat(width - measure(context) - measure(hint)) + paint(hint, "quiet", theme));
|
|
208
320
|
} else {
|
|
209
|
-
footer.push(
|
|
210
|
-
|
|
321
|
+
footer.push(...fold(hint, width).slice(0, 2).map(row => paint(row, "quiet", theme)));
|
|
322
|
+
footer.push(paint(trimEnd(context, width), "secondary", theme));
|
|
211
323
|
}
|
|
212
|
-
if (view.
|
|
324
|
+
if (!view.header && customOrigin(view)) footer.push(paint(trimEnd(customOrigin(view), width), "accent", theme));
|
|
213
325
|
|
|
214
326
|
// A breathing line between the scrollback and the live area when the window has room for it.
|
|
215
327
|
const lines = rows >= 18 && !compactFocus ? [""] : [];
|
|
216
328
|
const room = () => budget - footer.length - 3 - lines.length;
|
|
217
|
-
const line = (value,
|
|
329
|
+
const line = (value, role, limit = 2) => {
|
|
218
330
|
const text = terminalText(value);
|
|
219
|
-
lines.push(...wrap(
|
|
331
|
+
lines.push(...wrap(text, width).slice(0, Math.max(0, Math.min(limit, room()))).map(row => paint(row, role, theme)));
|
|
220
332
|
};
|
|
221
333
|
// Activity: the one animated glyph, the current step and the server-anchored clock.
|
|
222
334
|
if (active && !compactFocus) {
|
|
223
335
|
const clock = view.run.elapsed ?? "Elapsed unavailable";
|
|
224
|
-
const doing = compactRun ? view.status : (view.run.progress || "Working").replace(/^Starting: (.+)$/, "$1") + "…";
|
|
225
|
-
const head = `${
|
|
226
|
-
if (measure(`${glyph} ${terminalText(doing)} · ${terminalText(clock)}`) <= width) lines.push(`${head} ${
|
|
336
|
+
const doing = compactRun ? view.status : (view.run.progress || "Working").replace(/^Starting: (.+)$/, "$1").replace(/[.…]+$/, "") + "…";
|
|
337
|
+
const head = `${paint(glyph, "accent", theme)} ${terminalText(doing)}`, tail = paint(terminalText(clock), "quiet", theme);
|
|
338
|
+
if (measure(`${glyph} ${terminalText(doing)} · ${terminalText(clock)}`) <= width) lines.push(`${head} ${paint("·", "quiet", theme)} ${tail}`);
|
|
227
339
|
else if (compactRun) lines.push(wrap(head, width)[0], ` ${tail}`);
|
|
228
|
-
else lines.push(wrap(`${
|
|
229
|
-
if (!compactRun && view.status && view.status !== "Running") line(view.status
|
|
340
|
+
else lines.push(wrap(`${paint(glyph, "accent", theme)} ${tail} · ${terminalText(doing)}`, width)[0]);
|
|
341
|
+
if (!compactRun && view.status && view.status !== "Running") line(` ${view.status}`, "secondary", 1);
|
|
230
342
|
}
|
|
231
343
|
if (!compactFocus && !compactRun && view.run?.question) line(readOnly ? "Waiting for an editor to answer"
|
|
232
|
-
: signedOut ? "Question pending — reconnect to answer" : "Your answer is needed · /answer",
|
|
344
|
+
: signedOut ? "Question pending — reconnect to answer" : "Your answer is needed · /answer", "attention", 1);
|
|
233
345
|
if (prompt) {
|
|
234
|
-
line(prompt.title, "
|
|
346
|
+
line(prompt.title, "strong", compactFocus ? 1 : 3);
|
|
235
347
|
if (!prompt.typed) {
|
|
236
348
|
const choices = view.choices ?? prompt.options, selected = Math.min(view.selected ?? 0, Math.max(0, choices.length - 1));
|
|
237
349
|
const more = choices.length > 1 && !compactFocus ? 1 : 0;
|
|
@@ -240,14 +352,14 @@ export function richFrame(view, composer, cursor, { columns = 80, rows = 24, col
|
|
|
240
352
|
choices.slice(offset, offset + count).forEach((item, i) => {
|
|
241
353
|
const index = prompt.options.indexOf(item) + 1, current = i + offset === selected;
|
|
242
354
|
const checked = prompt.multiple ? `${composer.split(/[, ]+/).includes(String(index)) ? "[x]" : "[ ]"} ` : "";
|
|
243
|
-
lines.push(wrap(`${current ?
|
|
355
|
+
lines.push(wrap(`${current ? paint("›", "accent", theme) : " "} ${index}. ${checked}${current ? paint(terminalText(item.label), "accent", theme) : terminalText(item.label)}`, width)[0]);
|
|
244
356
|
});
|
|
245
|
-
if (!choices.length) line("No matching choices; clear the filter",
|
|
246
|
-
if (choices.length > count && more) line(`${selected + 1}/${choices.length} · ↑↓ more choices`,
|
|
357
|
+
if (!choices.length) line("No matching choices; clear the filter", "attention", 1);
|
|
358
|
+
if (choices.length > count && more) line(`${selected + 1}/${choices.length} · ↑↓ more choices`, "quiet", 1);
|
|
247
359
|
}
|
|
248
360
|
} else if (view.login) {
|
|
249
|
-
line(`Approve code ${view.login.user_code} in your browser`, "
|
|
250
|
-
if (!compactFocus) line(view.login.verification_uri_complete,
|
|
361
|
+
line(`Approve code ${view.login.user_code} in your browser`, "strong", 1);
|
|
362
|
+
if (!compactFocus) line(view.login.verification_uri_complete, "accent", 2);
|
|
251
363
|
const actions = ["Open browser", "Copy link", "Work offline"];
|
|
252
364
|
line(compactFocus ? `› ${actions[view.loginAction ?? 0]} (${(view.loginAction ?? 0) + 1}/3)`
|
|
253
365
|
: actions.map((text, i) => `${i === view.loginAction ? "› " : ""}${text}`).join(" · "), undefined, compactFocus ? 1 : 2);
|
|
@@ -258,51 +370,84 @@ export function richFrame(view, composer, cursor, { columns = 80, rows = 24, col
|
|
|
258
370
|
const name = Math.min(16, Math.max(...view.palette.map(row => row.name.length + 1)));
|
|
259
371
|
view.palette.slice(offset, offset + count).forEach((row, i) => {
|
|
260
372
|
const current = i + offset === selected, label = pad(`/${row.name}`, name);
|
|
261
|
-
lines.push(wrap(`${current ?
|
|
373
|
+
lines.push(wrap(`${current ? paint("›", "accent", theme) : " "} ${current ? paint(label, "accent", theme) : label} ${paint(terminalText(row.disabled || row.description), "secondary", theme)}`, width)[0]);
|
|
262
374
|
});
|
|
263
375
|
}
|
|
264
376
|
const compactAttachments = rows < 24 && view.attachments?.length;
|
|
265
|
-
if (view.attachments?.length && !compactAttachments) line(`${view.attachments.length} file(s) selected · uploaded only on send`,
|
|
266
|
-
if (view.issue && !prompt) line(view.issue.text + (view.issue.count > 1 ? ` (${view.issue.count} attempts)` : ""),
|
|
267
|
-
|
|
377
|
+
if (view.attachments?.length && !compactAttachments) line(`${view.attachments.length} file(s) selected · uploaded only on send`, "secondary", 1);
|
|
378
|
+
if (view.issue && !prompt) line(view.issue.text + (view.issue.count > 1 ? ` (${view.issue.count} attempts)` : ""), "attention", Math.max(1, Math.min(3, room())));
|
|
379
|
+
// First use: signed in, idle, an unbound folder, an empty transcript and an empty draft.
|
|
380
|
+
const firstUse = view.header && view.firstUse && !active && !composer && !prompt
|
|
381
|
+
&& !view.login && !view.issue && !view.palette?.length && !view.attachments?.length;
|
|
382
|
+
const idle = !active && !view.issue && !prompt && !view.login && !view.palette?.length;
|
|
383
|
+
// "Ready" is what the idle hint already says; every other state keeps its line.
|
|
384
|
+
if (idle && !firstUse && view.status !== "Ready") line(view.status, "secondary", 1);
|
|
268
385
|
const maxDraft = Math.max(1, Math.min(6, budget - lines.length - footer.length - 2));
|
|
269
|
-
const draft = composerViewport(composer, cursor, width -
|
|
386
|
+
const draft = composerViewport(composer, cursor, width - 4, maxDraft, theme);
|
|
270
387
|
const selection = compactAttachments ? `${view.attachments.length} file(s) · upload on send` : "";
|
|
271
388
|
const placeholder = prompt ? (prompt.typed ? "Type your answer…" : "Filter or type a choice number…")
|
|
272
389
|
: readOnly ? "Read-only Project — draft here…" : view.offline ? "Draft offline…"
|
|
273
390
|
: !view.account ? "Sign in to send — draft here…"
|
|
274
391
|
: active ? "Draft your next question…" : "Ask Laplace…";
|
|
275
|
-
lines.push(...
|
|
276
|
-
|
|
392
|
+
lines.push(...composerBand(draft.lines.map((value, index) => ` ${index ? " " : paint("›", "accent", theme)} ${composer ? value
|
|
393
|
+
: paint("▌", "accent", theme) + paint(placeholder, "secondary", theme)}`), width, theme,
|
|
394
|
+
{ top: selection, bottom: draft.scrolling ? draft.position : "" }));
|
|
277
395
|
lines.push(...footer);
|
|
278
|
-
|
|
396
|
+
const bottom = lines.slice(-budget);
|
|
397
|
+
|
|
398
|
+
// Above the gap: identity while the transcript is empty, then the first-use note if it fits.
|
|
399
|
+
const space = rows - 1 - bottom.length;
|
|
400
|
+
const top = [];
|
|
401
|
+
if (view.header) {
|
|
402
|
+
const header = richHeader(view, columns, theme);
|
|
403
|
+
// No room for the identity: the footer owns the route and origin, as after the commit.
|
|
404
|
+
if (header.length > space) return richLayout({ ...view, header: false }, composer, cursor, options);
|
|
405
|
+
top.push(...header);
|
|
406
|
+
}
|
|
407
|
+
if (firstUse) {
|
|
408
|
+
const note = "New folder · A project will be created on your first message. /link existing";
|
|
409
|
+
const intro = [paint("What are we working on?", "strong", theme), paint("Ask Laplace about your mission, or start from a file.", "secondary", theme)];
|
|
410
|
+
const suggestions = ONBOARDING.map(text => `${paint("›", "accent", theme)} ${paint(text, "secondary", theme)}`);
|
|
411
|
+
const quiet = wrap(note, width).map(row => paint(row, "quiet", theme));
|
|
412
|
+
const body = [[...intro, "", ...suggestions, "", ...quiet], [...intro, "", ...quiet], quiet]
|
|
413
|
+
.map(rows => rows.map(row => measure(row) > width ? wrap(row, width)[0] : row))
|
|
414
|
+
.find(rows => top.length + 1 + rows.length + 1 <= space);
|
|
415
|
+
if (body) top.push("", ...body);
|
|
416
|
+
}
|
|
417
|
+
return { top, bottom };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Bottom-anchored live area as one string: identity, gap, activity, focus, composer, footer. */
|
|
421
|
+
export function richFrame(view, composer, cursor, options = {}) {
|
|
422
|
+
const { top, bottom } = richLayout(view, composer, cursor, options);
|
|
423
|
+
return [...top, ...bottom].join("\n");
|
|
279
424
|
}
|
|
280
425
|
|
|
281
426
|
const indent = (text, first, rest = " ") => text.split("\n").map((line, i) => (i ? (line ? rest : "") : first) + line).join("\n");
|
|
282
427
|
|
|
283
428
|
/** One scrollback block per message: the ask, the answer with its record, or a quiet notice. */
|
|
284
429
|
export function richMessage(message, columns, color, { stepsShown = false } = {}) {
|
|
285
|
-
const width = Math.max(20, columns - 1);
|
|
430
|
+
const theme = themeOf(color), width = Math.max(20, columns - 1);
|
|
286
431
|
if (message.kind === "user") {
|
|
287
|
-
return "\n" + indent(wrap(terminalText(message.text), width - 2).join("\n"),
|
|
432
|
+
return "\n" + indent(wrap(terminalText(message.text), width - 2).join("\n"), paint("›", "accent", theme) + " ") + "\n";
|
|
288
433
|
}
|
|
289
|
-
if (message.kind === "step") return
|
|
434
|
+
if (message.kind === "step") return paint(trimEnd(` ⎿ ${message.text}`, width), "secondary", theme);
|
|
290
435
|
if (message.kind === "cost") {
|
|
291
436
|
const display = message.value?.display ?? {};
|
|
292
437
|
const parts = [["Used", display.used], ["Limit", display.limit], ["Remaining", display.remaining]].filter(([, v]) => v !== null && v !== undefined);
|
|
293
|
-
return
|
|
438
|
+
return paint(trimEnd(` Usage this UTC month · ${parts.length ? parts.map(([k, v]) => `${k} ${v}`).join(" · ") : "no exact amount available"} · /usage`, width), "quiet", theme);
|
|
294
439
|
}
|
|
295
440
|
if (message.kind !== "answer") return wrap(terminalText(message.text), width - 2).map(line => " " + line).join("\n");
|
|
296
441
|
const run = message.sections;
|
|
297
|
-
if (!run) return "\n" + indent(markdownView(message.text, width - 2,
|
|
442
|
+
if (!run) return "\n" + indent(markdownView(message.text, width - 2, theme), paint("●", "accent", theme) + " ") + "\n";
|
|
298
443
|
const out = [];
|
|
299
|
-
out.push(run.answer !== undefined ? indent(markdownView(run.answer, width - 2,
|
|
300
|
-
:
|
|
301
|
-
const section = (title, rows) => { if (rows.length) out.push("", " " +
|
|
444
|
+
out.push(run.answer !== undefined ? indent(markdownView(run.answer, width - 2, theme), paint("●", "accent", theme) + " ")
|
|
445
|
+
: paint("●", "attention", theme) + " No answer was published for this Run.");
|
|
446
|
+
const section = (title, rows) => { if (rows.length) out.push("", " " + paint(title, "strong", theme), ...rows); };
|
|
302
447
|
const one = text => text.split("\n").map(part => part.trim()).filter(Boolean).join(" · ");
|
|
303
|
-
section("Steps", run.tasks.slice(stepsShown).map(task =>
|
|
448
|
+
section("Steps", run.tasks.slice(stepsShown).map(task => paint(trimEnd(` ⎿ ${one(task.replace(/^\s*[▸·]\s*/, ""))}`, width), "secondary", theme)));
|
|
304
449
|
// Evidence rows are never truncated: quantity, unit and origin wrap with a hanging indent.
|
|
305
|
-
const full = (row,
|
|
450
|
+
const full = (row, role = "secondary") => prose(terminalText(row.trim()), width - 4).map((part, i) => (i ? " " : " ") + paint(part, role, theme));
|
|
306
451
|
section("Inputs and origins", run.origins.flatMap(row => full(row)));
|
|
307
452
|
section("Assumptions", run.assumptions.flatMap(row => full(row)));
|
|
308
453
|
section("Transfer tables", run.tables.flatMap(rows => rows.flatMap(row => full(row))));
|
|
@@ -311,9 +456,9 @@ export function richMessage(message, columns, color, { stepsShown = false } = {}
|
|
|
311
456
|
const digest = row => /^[0-9a-f]{32,}\./.test(row.artifact.filename);
|
|
312
457
|
const shown = [...numbered.filter(row => !digest(row)), ...numbered.filter(digest)].slice(0, 3);
|
|
313
458
|
const more = run.artifacts.length - shown.length;
|
|
314
|
-
section("Files", shown.flatMap(({ artifact, number }) => prose(`${terminalText(artifact.filename)} ${
|
|
315
|
-
.map((part, i) => ` ${i ? " " :
|
|
316
|
-
.concat(more > 0 ? [
|
|
317
|
-
out.push("", ...full(run.tail.map(terminalText).join(" · "), run.state === "ok" ?
|
|
459
|
+
section("Files", shown.flatMap(({ artifact, number }) => prose(`${terminalText(artifact.filename)} ${paint(`· ${artifact.kind} · /download ${number}`, "quiet", theme)}`, width - 4)
|
|
460
|
+
.map((part, i) => ` ${i ? " " : paint("↓", "accent", theme)} ${part}`))
|
|
461
|
+
.concat(more > 0 ? [paint(` … ${more} more · /files`, "quiet", theme)] : []));
|
|
462
|
+
out.push("", ...full(run.tail.map(terminalText).join(" · "), run.state === "ok" ? "quiet" : "attention"));
|
|
318
463
|
return "\n" + out.join("\n") + "\n";
|
|
319
464
|
}
|
package/src/update.js
CHANGED
|
@@ -210,8 +210,8 @@ export async function stageUpdate(plan, { signal, progress = () => {} } = {}) {
|
|
|
210
210
|
const recovery = join(directory, "recover.mjs");
|
|
211
211
|
receipt.recovery = recovery;
|
|
212
212
|
writeStore(join(directory, "activation.json"), receipt);
|
|
213
|
-
writeFileSync(recovery, `import {readFileSync,symlinkSync,renameSync,
|
|
214
|
-
import {dirname,join} from 'node:path';
|
|
213
|
+
writeFileSync(recovery, `import {readFileSync,symlinkSync,renameSync,lstatSync,readlinkSync,openSync,closeSync,writeFileSync,fsyncSync,unlinkSync} from 'node:fs';
|
|
214
|
+
import {dirname,join,resolve} from 'node:path';
|
|
215
215
|
import {randomUUID} from 'node:crypto';
|
|
216
216
|
${syncDirectory.toString()}
|
|
217
217
|
${activateLauncher.toString()}
|
|
@@ -268,7 +268,7 @@ export function rollbackLauncher(receipt) {
|
|
|
268
268
|
catch { throw new Error(`Recovery deferred: another updater owns ${lock}`); }
|
|
269
269
|
try {
|
|
270
270
|
writeFileSync(fd, JSON.stringify({ pid: process.pid, launcher: receipt.launcher })); fsyncSync(fd);
|
|
271
|
-
if (
|
|
271
|
+
if (!lstatSync(receipt.launcher).isSymbolicLink() || resolve(dirname(receipt.launcher), readlinkSync(receipt.launcher)) !== receipt.target) throw new Error("Recovery refused: another process changed the launcher");
|
|
272
272
|
activateLauncher(receipt.launcher, receipt.previous);
|
|
273
273
|
} finally { closeSync(fd); unlinkSync(lock); }
|
|
274
274
|
}
|