From ec3889fe34c6da6b4198d282a450ab2dccf44c86 Mon Sep 17 00:00:00 2001 From: yannickmonney Date: Mon, 22 Jun 2026 06:14:49 +0200 Subject: [PATCH 01/13] feat(platform): adaptive chat reliability and blue-green deploy tiers Adaptive chat reliability, blue-green deploy tiers, CLI version alignment, and sandbox blue-green re-architecture. --- .github/workflows/update-models.yml | 4 +- AGENTS.md | 1 + README.md | 2 +- builtin-configs/agents/chat/assistant.json | 4 +- builtin-configs/agents/chat/coder.json | 4 +- .../agents/chat/integration-assistant.json | 4 +- builtin-configs/agents/chat/researcher.json | 4 +- .../agents/chat/workflow-assistant.json | 4 +- .../agents/github/pull-request-reviewer.json | 2 +- builtin-configs/agents/router.json | 2 +- .../workforce/chief-executive-officer.json | 2 +- .../workforce/chief-marketing-officer.json | 2 +- .../workforce/chief-operating-officer.json | 2 +- .../workforce/chief-technology-officer.json | 2 +- .../agents/workforce/legal-counsel.json | 2 +- .../agents/workforce/security-engineer.json | 2 +- .../agents/workforce/software-architect.json | 2 +- .../agents/workforce/software-developer.json | 2 +- builtin-configs/providers/openrouter.json | 580 ++++++++++-------- docs/de/cloud/onboarding.md | 2 +- docs/de/develop/ai-assisted-development.md | 18 +- docs/de/platform/agents/external-agent.md | 4 +- docs/de/platform/member/environment.md | 8 +- docs/de/platform/member/overview.md | 2 +- docs/de/platform/member/preferences.md | 2 +- docs/de/platform/models.md | 74 ++- .../de/self-hosted/configuration/providers.md | 2 +- .../de/self-hosted/configuration/retention.md | 2 + docs/de/self-hosted/install/cli-install.md | 16 +- .../operate/backups-and-restore.md | 4 +- .../operate/release-notes/format.md | 6 +- docs/de/self-hosted/operate/upgrades.md | 61 +- docs/en/cloud/onboarding.md | 2 +- docs/en/develop/ai-assisted-development.md | 4 +- docs/en/platform/agents/external-agent.md | 4 +- docs/en/platform/member/environment.md | 8 +- docs/en/platform/member/overview.md | 2 +- docs/en/platform/member/preferences.md | 2 +- docs/en/platform/models.md | 74 ++- .../en/self-hosted/configuration/providers.md | 2 +- .../en/self-hosted/configuration/retention.md | 2 + docs/en/self-hosted/install/cli-install.md | 16 +- .../operate/backups-and-restore.md | 4 +- .../operate/release-notes/format.md | 6 +- docs/en/self-hosted/operate/upgrades.md | 59 +- docs/fr/cloud/onboarding.md | 2 +- docs/fr/develop/ai-assisted-development.md | 4 +- docs/fr/platform/agents/external-agent.md | 4 +- docs/fr/platform/member/environment.md | 8 +- docs/fr/platform/member/overview.md | 2 +- docs/fr/platform/member/preferences.md | 2 +- docs/fr/platform/models.md | 74 ++- .../fr/self-hosted/configuration/providers.md | 2 +- .../fr/self-hosted/configuration/retention.md | 2 + docs/fr/self-hosted/install/cli-install.md | 16 +- .../operate/backups-and-restore.md | 4 +- .../operate/release-notes/format.md | 6 +- docs/fr/self-hosted/operate/upgrades.md | 61 +- packages/shared/src/tux/lines.test.ts | 12 +- packages/shared/src/tux/lines.ts | 16 +- packages/shared/src/tux/step.ts | 4 +- .../components/primitives/button.stories.tsx | 2 +- .../src/components/primitives/button.test.tsx | 33 +- .../ui/src/components/primitives/button.tsx | 89 ++- .../src/components/primitives/icon-button.tsx | 6 + .../ui/src/i18n/tests/glossary/glossary.json | 2 +- services/docs/app/content/frontmatter.json | 12 +- services/docs/tests/lib/markdown.ts | 14 +- .../app/components/flow/flow-canvas.tsx | 7 +- .../components/layout/mobile-bottom-nav.tsx | 2 +- .../ui/data-display/json-viewer.tsx | 3 + .../ui/data-display/zoom-pan-viewer.test.tsx | 25 +- .../ui/data-display/zoom-pan-viewer.tsx | 134 ++-- .../ui/data-table/data-table-pagination.tsx | 4 +- .../ui/editor/editor-actions.test.tsx | 81 +++ .../components/ui/editor/editor-actions.tsx | 13 +- .../app/components/ui/feedback/banner.tsx | 2 +- .../components/ui/filters/filter-button.tsx | 7 +- .../components/ui/navigation/navigation.tsx | 45 +- .../components/ui/navigation/pagination.tsx | 4 +- .../agents/components/agent-knowledge.tsx | 2 +- .../agents/components/agent-navigation.tsx | 11 - .../components/agent-response-tuning.tsx | 158 ----- .../hooks/use-agent-config-context.test.tsx | 20 +- .../agents/organigram/organigram-panel.tsx | 2 +- .../components/automation-ai-chat-panel.tsx | 2 +- .../components/automation-sidepanel.tsx | 2 +- .../components/automation-steps.tsx | 2 + .../executions/executions-table.tsx | 2 + .../chat/components/agent-selector.tsx | 12 +- .../chat/components/branch-navigator.tsx | 4 +- .../chat/components/chat-error-display.tsx | 13 +- .../features/chat/components/chat-header.tsx | 81 +-- .../features/chat/components/chat-input.tsx | 336 +++++++--- .../components/chat-input/attachment-tray.tsx | 102 +-- .../paste-image-overlay.browser.test.tsx | 120 ++++ .../chat-input/paste-image-overlay.tsx | 122 ++++ .../chat-input/paste-image-tokens.test.ts | 123 ++++ .../chat-input/paste-image-tokens.ts | 98 +++ .../scroll-to-bottom-button.tsx | 2 +- .../chat/components/chat-messages.tsx | 13 + .../chat/components/composer-mode-menu.tsx | 33 +- .../chat/components/human-input-fields.tsx | 2 +- .../components/human-input-request-card.tsx | 4 +- .../chat/components/memory-proposal-card.tsx | 21 +- .../chat/components/message-bubble.tsx | 18 +- .../message-bubble/image-preview-dialog.tsx | 14 +- .../chat/components/model-fallback-notice.tsx | 45 ++ .../chat/components/shared-chat-view.tsx | 2 +- .../chat/context/chat-layout-context.tsx | 5 +- .../chat/hooks/use-message-processing.ts | 10 +- .../use-model-fallback-auto-switch.test.ts | 12 +- .../hooks/use-model-fallback-auto-switch.ts | 47 +- .../features/chat/hooks/use-send-message.ts | 132 ++-- .../chat/hooks/use-textarea-token-rects.ts | 168 +++++ .../chat/hooks/use-video-url-ingest.ts | 46 ++ .../features/chat/utils/capture-screenshot.ts | 103 ---- .../chat/utils/sanitize-chat-error.test.ts | 495 ++------------- .../chat/utils/sanitize-chat-error.ts | 170 ++--- .../components/conversation-header.tsx | 2 +- .../message-editor/editor-action-bar.tsx | 8 + .../message-editor/improve-mode.tsx | 7 +- .../components/customer-delete-dialog.tsx | 2 +- .../components/discussion-thread-view.tsx | 2 +- .../comparison-file-selector.tsx | 2 +- .../document-preview-image.test.tsx | 8 +- .../components/project-secrets-tab.tsx | 2 +- .../components/settings-field.test.tsx | 103 ---- .../settings/components/settings-field.tsx | 86 --- .../components/settings-page.test.tsx | 5 - .../settings/components/settings-page.tsx | 11 - .../components/settings-toggle-row.tsx | 2 +- .../components/deployment-settings.tsx | 12 +- .../governance/components/budget-editor.tsx | 12 +- .../components/chat-filter-config.tsx | 8 +- .../components/default-model-editor.tsx | 10 +- .../components/dsar-policy-editor.tsx | 6 +- .../components/feature-flags-editor.tsx | 12 +- .../components/guardrails-overview.tsx | 6 +- .../components/login-policy-editor.tsx | 75 +-- .../components/model-access-editor.tsx | 10 +- .../components/moderation-provider-config.tsx | 8 +- .../components/password-policy-editor.tsx | 18 +- .../personalization-policy-editor.tsx | 6 +- .../governance/components/pii-config.tsx | 6 +- .../components/pii/pii-config-panel.tsx | 2 +- .../components/retention-editor.tsx | 8 +- .../session-idle-timeout-editor.tsx | 66 +- .../components/system-prompt-editor.tsx | 6 +- .../governance/components/trash-page.tsx | 8 +- .../components/two-factor-policy-editor.tsx | 17 +- .../components/upload-policy-editor.tsx | 20 +- .../components/voice-output-policy-editor.tsx | 20 +- .../settings/governance/config-parser.test.ts | 52 ++ .../settings/governance/config-parser.ts | 25 + .../requests-list-section.tsx | 6 +- .../use-governance-policy-toggle.test.ts | 90 +++ .../hooks/use-governance-policy-toggle.ts | 80 +++ .../legal-hold/active-holds-section.tsx | 6 +- .../governance/legal-hold/matters-section.tsx | 6 +- .../legal-hold/release-history-section.tsx | 6 +- .../legal-hold/release-requests-section.tsx | 6 +- .../sso-config/role-mapping-section.tsx | 2 +- .../organization/components/member-table.tsx | 4 +- .../components/personalization-settings.tsx | 30 +- .../components/model-catalog-card.tsx | 7 +- .../settings/sandboxes/sandboxes-settings.tsx | 10 +- .../user-env/components/user-env-settings.tsx | 214 ++++--- .../webdav/components/webdav-settings.tsx | 6 +- .../tasks/components/task-dependencies.tsx | 2 +- .../components/vendor-delete-dialog.tsx | 2 +- services/platform/app/hooks/use-is-mac.ts | 25 + .../app/hooks/use-navigation-items.ts | 15 +- services/platform/app/routeTree.gen.ts | 23 - .../$id/agents/$agentId/response-tuning.tsx | 21 - .../governance/security-monitoring.tsx | 2 +- .../dashboard/$id/settings/organization.tsx | 2 +- .../dashboard/$id/settings/providers.tsx | 2 +- .../$id/settings/providers/$providerName.tsx | 2 +- services/platform/convex/_generated/api.d.ts | 8 + services/platform/convex/agents/auto_route.ts | 21 +- .../convex/agents/auto_route_helpers.test.ts | 38 +- .../convex/agents/auto_route_helpers.ts | Bin 12815 -> 12631 bytes services/platform/convex/agents/chat_turn.ts | 11 + .../convex/agents/chat_turn_generate.ts | 15 + services/platform/convex/agents/config.ts | 1 - .../continue_external_agent_turn.ts | 7 + .../recover_external_agent_turns.ts | 15 +- .../external_agent/run_external_agent.ts | 36 +- services/platform/convex/agents/file_utils.ts | 4 - .../convex/agents/internal_mutations.ts | 5 + .../convex/agents/internal_queries.ts | 4 +- .../agents/knowledge_file_rag_info.test.ts | 8 +- .../convex/agents/recover_stuck_chat_turns.ts | 164 +++++ services/platform/convex/agents/schema.ts | 19 + .../platform/convex/control/drain.test.ts | 119 ++++ services/platform/convex/control/drain.ts | 117 ++++ services/platform/convex/control/schema.ts | 34 + services/platform/convex/crons.ts | 14 + .../convex/lib/agent_chat/internal_actions.ts | 439 +++++++------ .../platform/convex/lib/agent_chat/types.ts | 24 +- .../lib/agent_response/generate_response.ts | 87 ++- .../generate_response_error_handling.test.ts | 24 +- .../reasoning/build_reasoning_options.test.ts | 111 ++-- .../reasoning/build_reasoning_options.ts | 126 +--- .../reasoning/controller.test.ts | 64 ++ .../agent_response/reasoning/controller.ts | 82 ++- .../reasoning/generation_params.ts | 13 +- .../agent_response/reasoning/signals.test.ts | 61 ++ .../lib/agent_response/reasoning/signals.ts | 54 +- .../lib/agent_response/reasoning/types.ts | 8 + .../convex/lib/agent_response/types.ts | 22 +- .../convex/lib/error_classification.test.ts | 101 +-- .../convex/lib/error_classification.ts | 99 --- .../lib/prompts/registry/entries/routing.ts | 4 +- .../sandbox/helpers/session_client.ts | 85 ++- .../sandbox/helpers/spawner_client.ts | 154 +++-- .../helpers/spawner_color_routing.test.ts | 36 ++ .../node_only/sandbox/internal_actions.ts | 24 +- .../convex/node_only/sandbox/run_agent.ts | 17 +- .../platform/convex/providers/errors.test.ts | 39 ++ services/platform/convex/providers/errors.ts | 31 + .../convex/providers/failure_scope.test.ts | 102 +++ .../convex/providers/failure_scope.ts | 80 +++ .../convex/reasoning_profiles/schema.ts | 10 + .../convex/sandbox/internal_mutations.ts | 26 +- services/platform/convex/sandbox/schema.ts | 9 + .../convex/sandbox/session_mutations.ts | 29 + .../convex/sandbox/session_queries.ts | 14 +- .../convex/sandbox/sessions_schema.ts | 10 + services/platform/convex/schema.ts | 4 + .../convex/threads/generation_liveness.ts | 44 ++ .../convex/threads/internal_mutations.ts | 10 + .../platform/convex/threads/message_queue.ts | 47 +- services/platform/convex/threads/queries.ts | 35 +- services/platform/convex/threads/schema.ts | 17 +- .../nodes/llm/execute_llm_node.test.ts | 82 ++- .../helpers/nodes/llm/execute_llm_node.ts | 96 ++- .../platform/lib/shared/chat-errors.test.ts | 196 ++++++ services/platform/lib/shared/chat-errors.ts | 352 +++++++++++ .../shared/constants/system-message-tags.ts | 50 +- .../platform/lib/shared/model-sync.test.ts | 25 +- services/platform/lib/shared/model-sync.ts | 12 +- .../lib/shared/response-tuning.test.ts | 45 +- .../platform/lib/shared/response-tuning.ts | 87 ++- .../lib/shared/schemas/agents.test.ts | 60 +- .../platform/lib/shared/schemas/agents.ts | 67 -- .../platform/lib/utils/convex-error.test.ts | 57 ++ services/platform/lib/utils/convex-error.ts | 24 + services/platform/messages/de.json | 75 +-- services/platform/messages/en.json | 75 +-- services/platform/messages/fr.json | 75 +-- services/platform/scripts/dev-engine.ts | 101 ++- .../platform/scripts/update-model-catalog.ts | Bin 5821 -> 13284 bytes .../tests/e2e/specs/agent-editor.spec.ts | 46 -- .../platform/tests/e2e/specs/agents.spec.ts | 1 - .../tests/e2e/specs/chat-threads.spec.ts | 10 +- .../tests/e2e/specs/navigation.spec.ts | 25 +- .../tests/e2e/specs/page-loads.spec.ts | 12 +- services/platform/tests/manual/TEMPLATE.md | 2 +- services/platform/tests/manual/agents.md | 49 +- services/platform/tests/manual/chat.md | 2 +- .../backend/docker/docker-session-backend.ts | 5 + services/sandbox/src/cleanup.ts | 26 +- services/sandbox/src/config.test.ts | 25 + services/sandbox/src/config.ts | 31 +- services/sandbox/src/docker-args.test.ts | Bin 11442 -> 12545 bytes services/sandbox/src/docker-args.ts | 5 + services/sandbox/src/server.ts | 153 ++++- .../src/session/docker-session-args.ts | 3 + .../sandbox/src/session/session-routes.ts | 42 +- .../src/session/session-test-config.ts | 1 + services/sandbox/src/sse.ts | 5 + services/sandbox/src/types.ts | 17 + tools/cli/README.md | 40 +- tools/cli/src/commands/deploy/index.ts | 121 +--- tools/cli/src/commands/rollback.ts | 5 +- tools/cli/src/commands/uninstall.ts | 31 + tools/cli/src/commands/update/index.ts | 37 ++ tools/cli/src/commands/upgrade/index.ts | 41 -- tools/cli/src/index.ts | 20 +- tools/cli/src/lib/actions/deploy.ts | 183 ++++-- .../cli/src/lib/actions/drain-convex.test.ts | 121 ++++ tools/cli/src/lib/actions/drain-convex.ts | 128 ++++ .../cli/src/lib/actions/flip-sandbox.test.ts | 126 ++++ tools/cli/src/lib/actions/flip-sandbox.ts | 303 +++++++++ tools/cli/src/lib/actions/restore.test.ts | 2 +- tools/cli/src/lib/actions/restore.ts | 4 +- tools/cli/src/lib/actions/rollback.test.ts | 69 ++- tools/cli/src/lib/actions/rollback.ts | 55 +- tools/cli/src/lib/actions/run-deploy.ts | 152 +++++ tools/cli/src/lib/actions/run-update.test.ts | 100 +++ tools/cli/src/lib/actions/run-update.ts | 219 +++++++ tools/cli/src/lib/actions/uninstall.test.ts | 133 ++++ tools/cli/src/lib/actions/uninstall.ts | 183 ++++++ .../generators/generate-color-compose.test.ts | 23 + .../generate-sandbox-color-compose.test.ts | 76 +++ .../generate-sandbox-color-compose.ts | 43 ++ .../src/lib/compose/select-services.test.ts | 72 +++ tools/cli/src/lib/compose/select-services.ts | 55 ++ .../services/create-platform-service.ts | 11 + .../services/create-sandbox-egress-service.ts | 11 +- .../services/create-sandbox-service.ts | 39 +- tools/cli/src/lib/compose/types.ts | 51 +- tools/cli/src/lib/docker/convex-run.ts | 101 +++ tools/cli/src/lib/project/project-context.ts | 6 +- tools/cli/src/lib/project/types.ts | 2 +- tools/cli/src/lib/version/align.test.ts | 143 +++++ tools/cli/src/lib/version/align.ts | 132 ++++ .../upgrade.ts => version/self-update.ts} | 407 ++++++------ tools/cli/src/utils/prompt.ts | 3 +- tools/cli/tests/smoke.test.ts | 30 + 312 files changed, 9357 insertions(+), 4135 deletions(-) create mode 100644 services/platform/app/components/ui/editor/editor-actions.test.tsx delete mode 100644 services/platform/app/features/agents/components/agent-response-tuning.tsx create mode 100644 services/platform/app/features/chat/components/chat-input/paste-image-overlay.browser.test.tsx create mode 100644 services/platform/app/features/chat/components/chat-input/paste-image-overlay.tsx create mode 100644 services/platform/app/features/chat/components/chat-input/paste-image-tokens.test.ts create mode 100644 services/platform/app/features/chat/components/chat-input/paste-image-tokens.ts create mode 100644 services/platform/app/features/chat/components/model-fallback-notice.tsx create mode 100644 services/platform/app/features/chat/hooks/use-textarea-token-rects.ts create mode 100644 services/platform/app/features/chat/hooks/use-video-url-ingest.ts delete mode 100644 services/platform/app/features/chat/utils/capture-screenshot.ts delete mode 100644 services/platform/app/features/settings/components/settings-field.test.tsx delete mode 100644 services/platform/app/features/settings/components/settings-field.tsx create mode 100644 services/platform/app/features/settings/governance/config-parser.test.ts create mode 100644 services/platform/app/features/settings/governance/config-parser.ts create mode 100644 services/platform/app/features/settings/governance/hooks/use-governance-policy-toggle.test.ts create mode 100644 services/platform/app/features/settings/governance/hooks/use-governance-policy-toggle.ts create mode 100644 services/platform/app/hooks/use-is-mac.ts delete mode 100644 services/platform/app/routes/dashboard/$id/agents/$agentId/response-tuning.tsx create mode 100644 services/platform/convex/agents/recover_stuck_chat_turns.ts create mode 100644 services/platform/convex/control/drain.test.ts create mode 100644 services/platform/convex/control/drain.ts create mode 100644 services/platform/convex/control/schema.ts create mode 100644 services/platform/convex/node_only/sandbox/helpers/spawner_color_routing.test.ts create mode 100644 services/platform/convex/providers/failure_scope.test.ts create mode 100644 services/platform/convex/providers/failure_scope.ts create mode 100644 services/platform/convex/threads/generation_liveness.ts create mode 100644 services/platform/lib/shared/chat-errors.test.ts create mode 100644 services/platform/lib/shared/chat-errors.ts create mode 100644 services/platform/lib/utils/convex-error.test.ts create mode 100644 tools/cli/src/commands/uninstall.ts create mode 100644 tools/cli/src/commands/update/index.ts delete mode 100644 tools/cli/src/commands/upgrade/index.ts create mode 100644 tools/cli/src/lib/actions/drain-convex.test.ts create mode 100644 tools/cli/src/lib/actions/drain-convex.ts create mode 100644 tools/cli/src/lib/actions/flip-sandbox.test.ts create mode 100644 tools/cli/src/lib/actions/flip-sandbox.ts create mode 100644 tools/cli/src/lib/actions/run-deploy.ts create mode 100644 tools/cli/src/lib/actions/run-update.test.ts create mode 100644 tools/cli/src/lib/actions/run-update.ts create mode 100644 tools/cli/src/lib/actions/uninstall.test.ts create mode 100644 tools/cli/src/lib/actions/uninstall.ts create mode 100644 tools/cli/src/lib/compose/generators/generate-sandbox-color-compose.test.ts create mode 100644 tools/cli/src/lib/compose/generators/generate-sandbox-color-compose.ts create mode 100644 tools/cli/src/lib/compose/select-services.test.ts create mode 100644 tools/cli/src/lib/compose/select-services.ts create mode 100644 tools/cli/src/lib/docker/convex-run.ts create mode 100644 tools/cli/src/lib/version/align.test.ts create mode 100644 tools/cli/src/lib/version/align.ts rename tools/cli/src/lib/{actions/upgrade.ts => version/self-update.ts} (57%) diff --git a/.github/workflows/update-models.yml b/.github/workflows/update-models.yml index d42574e52c..08a9cd3f09 100644 --- a/.github/workflows/update-models.yml +++ b/.github/workflows/update-models.yml @@ -48,7 +48,7 @@ jobs: - name: Detect changes id: diff run: | - if [ -n "$(git status --porcelain builtin-configs/providers)" ]; then + if [ -n "$(git status --porcelain builtin-configs/providers docs/en/platform/models.md docs/de/platform/models.md docs/fr/platform/models.md)" ]; then echo "changed=true" >> "$GITHUB_OUTPUT" else echo "changed=false" >> "$GITHUB_OUTPUT" @@ -69,7 +69,7 @@ jobs: git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git checkout -B "$BRANCH" - git add builtin-configs/providers + git add builtin-configs/providers docs/en/platform/models.md docs/de/platform/models.md docs/fr/platform/models.md # --no-verify: skip any husky hooks `bun install` may have registered. git commit --no-verify -m "$TITLE" # Bot-owned branch, recreated from HEAD each run → a plain force push is diff --git a/AGENTS.md b/AGENTS.md index 9da70a0e5d..7c826bfb9c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -164,6 +164,7 @@ knip · strict typecheck. **When you add a rule, add the guard.** - **Filenames:** dash-case everywhere except Convex, which uses snake_case. - **No status comments** (`// REFACTORED`, `// TODO: see #123`) and no comments narrating removed code. Git is the record. Comments explain _why_, rarely _what_. +- **Comments address the next reader of the code, never the chat or a follow-up.** No verification-status notes, handoff messages, or references to the conversation/plan (e.g. `// ⚠️ UNVERIFIED … require a live stack to validate (see plan)`). That belongs in the PR description, an issue, or your reply — not the source. If something is genuinely unproven, say so in the PR, not in a comment that ships forever. - **No empty catch blocks.** Log (`console.warn`/`console.error`) or re-throw. Silent catches hide bugs. - **No locale-aware date methods.** `toLocaleDateString`/`toLocaleTimeString`/`toLocaleString` are banned. Use `useFormatDate()` in React or `formatDate()` from [`lib/utils/date/format`](services/platform/lib/utils/date/format.ts). - **No `\uXXXX` escapes in JSON.** Write non-ASCII literally as UTF-8 (`ät`, `é`, `—`, `«»`). JSON's required escapes (`\n`, `\t`, `\"`, `\\`) stay. diff --git a/README.md b/README.md index b8f8ffded4..feab2bdeee 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ tale init [directory] # Create a new project with example configs ( tale start # Start all services locally tale start --detach # Start in background tale start --port 8443 # Use a custom HTTPS port -tale upgrade # Upgrade CLI and sync project files +tale update # Update the CLI + sync project files (then `tale deploy`; CLI self-aligns automatically) tale convex admin # Generate Convex dashboard admin key tale config # Manage CLI configuration ``` diff --git a/builtin-configs/agents/chat/assistant.json b/builtin-configs/agents/chat/assistant.json index b1420e201f..616d5f80dc 100644 --- a/builtin-configs/agents/chat/assistant.json +++ b/builtin-configs/agents/chat/assistant.json @@ -28,8 +28,8 @@ "supportedModels": [ "openrouter:anthropic/claude-haiku-4.5", "openrouter:anthropic/claude-sonnet-4.6", - "openrouter:anthropic/claude-opus-4.6", - "openrouter:openai/gpt-5.2", + "openrouter:anthropic/claude-opus-4.8", + "openrouter:openai/gpt-5.5", "openrouter:google/gemini-3.1-pro-preview" ], "knowledgeMode": "tool", diff --git a/builtin-configs/agents/chat/coder.json b/builtin-configs/agents/chat/coder.json index 8554ba00fd..394f0aba4f 100644 --- a/builtin-configs/agents/chat/coder.json +++ b/builtin-configs/agents/chat/coder.json @@ -21,8 +21,8 @@ "supportedModels": [ "openrouter:anthropic/claude-sonnet-4.6", "openrouter:anthropic/claude-haiku-4.5", - "openrouter:anthropic/claude-opus-4.6", - "openrouter:openai/gpt-5.2" + "openrouter:anthropic/claude-opus-4.8", + "openrouter:openai/gpt-5.5" ], "knowledgeMode": "tool", "includeOrgKnowledge": true, diff --git a/builtin-configs/agents/chat/integration-assistant.json b/builtin-configs/agents/chat/integration-assistant.json index db542b91d8..3457ce7f4e 100644 --- a/builtin-configs/agents/chat/integration-assistant.json +++ b/builtin-configs/agents/chat/integration-assistant.json @@ -10,8 +10,8 @@ "outputReserve": 2048, "supportedModels": [ "openrouter:anthropic/claude-sonnet-4.6", - "openrouter:anthropic/claude-opus-4.6", - "openrouter:openai/gpt-5.2" + "openrouter:anthropic/claude-opus-4.8", + "openrouter:openai/gpt-5.5" ], "roleRestriction": "admin_developer", "visibleInChat": true, diff --git a/builtin-configs/agents/chat/researcher.json b/builtin-configs/agents/chat/researcher.json index 3dc42734fb..f56f359d48 100644 --- a/builtin-configs/agents/chat/researcher.json +++ b/builtin-configs/agents/chat/researcher.json @@ -8,8 +8,8 @@ "integrationBindings": ["tavily"], "supportedModels": [ "openrouter:anthropic/claude-sonnet-4.6", - "openrouter:anthropic/claude-opus-4.6", - "openrouter:openai/gpt-5.2", + "openrouter:anthropic/claude-opus-4.8", + "openrouter:openai/gpt-5.5", "openrouter:google/gemini-3.1-pro-preview" ], "maxSteps": 40, diff --git a/builtin-configs/agents/chat/workflow-assistant.json b/builtin-configs/agents/chat/workflow-assistant.json index 3640237f64..55b0e73167 100644 --- a/builtin-configs/agents/chat/workflow-assistant.json +++ b/builtin-configs/agents/chat/workflow-assistant.json @@ -16,8 +16,8 @@ ], "supportedModels": [ "openrouter:anthropic/claude-sonnet-4.6", - "openrouter:anthropic/claude-opus-4.6", - "openrouter:openai/gpt-5.2" + "openrouter:anthropic/claude-opus-4.8", + "openrouter:openai/gpt-5.5" ], "maxSteps": 30, "timeoutMs": 240000, diff --git a/builtin-configs/agents/github/pull-request-reviewer.json b/builtin-configs/agents/github/pull-request-reviewer.json index 1cb4ca7e16..3917c853cc 100644 --- a/builtin-configs/agents/github/pull-request-reviewer.json +++ b/builtin-configs/agents/github/pull-request-reviewer.json @@ -19,7 +19,7 @@ ], "integrationBindings": ["github"], "supportedModels": [ - "openrouter:anthropic/claude-opus-4.6", + "openrouter:anthropic/claude-opus-4.8", "openrouter:anthropic/claude-sonnet-4.6" ], "personalizationMode": "off", diff --git a/builtin-configs/agents/router.json b/builtin-configs/agents/router.json index fade97dd6e..1e90a88708 100644 --- a/builtin-configs/agents/router.json +++ b/builtin-configs/agents/router.json @@ -20,6 +20,6 @@ "supportedModels": [ "openrouter:anthropic/claude-haiku-4.5", "openrouter:google/gemini-3.1-pro-preview", - "openrouter:openai/gpt-5.2" + "openrouter:openai/gpt-5.5" ] } diff --git a/builtin-configs/agents/workforce/chief-executive-officer.json b/builtin-configs/agents/workforce/chief-executive-officer.json index 0a6d947e82..72e09ec4db 100644 --- a/builtin-configs/agents/workforce/chief-executive-officer.json +++ b/builtin-configs/agents/workforce/chief-executive-officer.json @@ -17,7 +17,7 @@ "request_human_input" ], "supportedModels": [ - "openrouter:anthropic/claude-opus-4.6", + "openrouter:anthropic/claude-opus-4.8", "openrouter:anthropic/claude-sonnet-4.6" ], "personalizationMode": "off", diff --git a/builtin-configs/agents/workforce/chief-marketing-officer.json b/builtin-configs/agents/workforce/chief-marketing-officer.json index a3eff41d36..7c1ef44686 100644 --- a/builtin-configs/agents/workforce/chief-marketing-officer.json +++ b/builtin-configs/agents/workforce/chief-marketing-officer.json @@ -17,7 +17,7 @@ ], "supportedModels": [ "openrouter:anthropic/claude-sonnet-4.6", - "openrouter:anthropic/claude-opus-4.6" + "openrouter:anthropic/claude-opus-4.8" ], "personalizationMode": "off", "maxSteps": 30, diff --git a/builtin-configs/agents/workforce/chief-operating-officer.json b/builtin-configs/agents/workforce/chief-operating-officer.json index 1a1644a514..34ac75b003 100644 --- a/builtin-configs/agents/workforce/chief-operating-officer.json +++ b/builtin-configs/agents/workforce/chief-operating-officer.json @@ -16,7 +16,7 @@ "request_human_input" ], "supportedModels": [ - "openrouter:anthropic/claude-opus-4.6", + "openrouter:anthropic/claude-opus-4.8", "openrouter:anthropic/claude-sonnet-4.6" ], "personalizationMode": "off", diff --git a/builtin-configs/agents/workforce/chief-technology-officer.json b/builtin-configs/agents/workforce/chief-technology-officer.json index 82ca320642..86e9bd313d 100644 --- a/builtin-configs/agents/workforce/chief-technology-officer.json +++ b/builtin-configs/agents/workforce/chief-technology-officer.json @@ -16,7 +16,7 @@ "request_human_input" ], "supportedModels": [ - "openrouter:anthropic/claude-opus-4.6", + "openrouter:anthropic/claude-opus-4.8", "openrouter:anthropic/claude-sonnet-4.6" ], "personalizationMode": "off", diff --git a/builtin-configs/agents/workforce/legal-counsel.json b/builtin-configs/agents/workforce/legal-counsel.json index 5daf9ecfd0..667895813b 100644 --- a/builtin-configs/agents/workforce/legal-counsel.json +++ b/builtin-configs/agents/workforce/legal-counsel.json @@ -15,7 +15,7 @@ "request_human_input" ], "supportedModels": [ - "openrouter:anthropic/claude-opus-4.6", + "openrouter:anthropic/claude-opus-4.8", "openrouter:anthropic/claude-sonnet-4.6" ], "personalizationMode": "off", diff --git a/builtin-configs/agents/workforce/security-engineer.json b/builtin-configs/agents/workforce/security-engineer.json index a28795904f..2f6f47c8da 100644 --- a/builtin-configs/agents/workforce/security-engineer.json +++ b/builtin-configs/agents/workforce/security-engineer.json @@ -14,7 +14,7 @@ "document_write" ], "supportedModels": [ - "openrouter:anthropic/claude-opus-4.6", + "openrouter:anthropic/claude-opus-4.8", "openrouter:anthropic/claude-sonnet-4.6" ], "personalizationMode": "off", diff --git a/builtin-configs/agents/workforce/software-architect.json b/builtin-configs/agents/workforce/software-architect.json index a52d2d782c..15df1a89b6 100644 --- a/builtin-configs/agents/workforce/software-architect.json +++ b/builtin-configs/agents/workforce/software-architect.json @@ -14,7 +14,7 @@ "run_code" ], "supportedModels": [ - "openrouter:anthropic/claude-opus-4.6", + "openrouter:anthropic/claude-opus-4.8", "openrouter:anthropic/claude-sonnet-4.6" ], "personalizationMode": "on", diff --git a/builtin-configs/agents/workforce/software-developer.json b/builtin-configs/agents/workforce/software-developer.json index 9ff5b586e4..fb23431dd7 100644 --- a/builtin-configs/agents/workforce/software-developer.json +++ b/builtin-configs/agents/workforce/software-developer.json @@ -10,7 +10,7 @@ "integrationBindings": ["github"], "supportedModels": [ "openrouter:anthropic/claude-sonnet-4.6", - "openrouter:anthropic/claude-opus-4.6" + "openrouter:anthropic/claude-opus-4.8" ], "timeoutMs": 1800000, "personalizationMode": "on", diff --git a/builtin-configs/providers/openrouter.json b/builtin-configs/providers/openrouter.json index f64086478b..954c859cc3 100644 --- a/builtin-configs/providers/openrouter.json +++ b/builtin-configs/providers/openrouter.json @@ -13,28 +13,6 @@ "text-to-speech": "openai/gpt-4o-mini-tts-2025-12-15" }, "models": [ - { - "id": "anthropic/claude-opus-4.6", - "displayName": "Claude Opus 4.6", - "description": "Anthropic's flagship — best for complex reasoning and coding", - "tags": ["chat", "vision"], - "reasoning": { - "knob": "budgetTokens", - "minBudgetTokens": 1024 - }, - "promptCaching": { - "mode": "explicit-breakpoints", - "maxBreakpoints": 4 - }, - "qualityScore": 0.98, - "contextWindow": 1000000, - "cost": { - "inputCentsPerMillion": 500.00000000000006, - "outputCentsPerMillion": 2500 - }, - "maxOutputTokens": 128000, - "hidden": true - }, { "id": "anthropic/claude-sonnet-4.6", "displayName": "Claude Sonnet 4.6", @@ -77,63 +55,6 @@ }, "maxOutputTokens": 64000 }, - { - "id": "openai/gpt-5.2-pro", - "displayName": "GPT-5.2 Pro", - "description": "OpenAI's most advanced model for the hardest reasoning tasks", - "tags": ["chat", "vision"], - "reasoning": { - "knob": "effort" - }, - "promptCaching": { - "mode": "auto-server" - }, - "qualityScore": 0.97, - "contextWindow": 400000, - "cost": { - "inputCentsPerMillion": 2100, - "outputCentsPerMillion": 16800 - }, - "maxOutputTokens": 128000, - "hidden": true - }, - { - "id": "openai/gpt-5.2", - "displayName": "GPT-5.2", - "description": "OpenAI's flagship general-purpose model", - "tags": ["chat", "vision"], - "reasoning": { - "knob": "effort" - }, - "promptCaching": { - "mode": "auto-server" - }, - "qualityScore": 0.95, - "contextWindow": 400000, - "cost": { - "inputCentsPerMillion": 175, - "outputCentsPerMillion": 1400 - }, - "maxOutputTokens": 128000, - "hidden": true - }, - { - "id": "openai/gpt-5.2-chat", - "displayName": "GPT-5.2 Instant", - "description": "Low-latency GPT-5.2 variant tuned for chat", - "tags": ["chat"], - "promptCaching": { - "mode": "auto-server" - }, - "qualityScore": 0.85, - "contextWindow": 128000, - "cost": { - "inputCentsPerMillion": 175, - "outputCentsPerMillion": 1400 - }, - "maxOutputTokens": 16384, - "hidden": true - }, { "id": "openai/gpt-oss-120b", "displayName": "GPT-OSS 120B", @@ -313,45 +234,25 @@ } }, { - "id": "moonshotai/kimi-k2.5", - "displayName": "Kimi K2.5", - "description": "High-performance Moonshot general-purpose model", - "tags": ["chat"], - "qualityScore": 0.8, - "contextWindow": 262144, - "cost": { - "inputCentsPerMillion": 37.5, - "outputCentsPerMillion": 202.5 - }, - "reasoning": { - "knob": "effort" - } - }, - { - "id": "minimax/minimax-m2.7", - "displayName": "MiniMax M2.7", - "description": "MiniMax flagship MoE for agentic coding and reasoning", - "tags": ["chat"], + "id": "moonshotai/kimi-k2.7-code", + "displayName": "Kimi K2.7 Code", + "description": "Moonshot's coding-specialized Kimi K2.7 — agentic coding, multimodal, 262K context", + "tags": ["chat", "vision"], "reasoning": { "knob": "effort" }, - "qualityScore": 0.78, - "contextWindow": 204800, + "qualityScore": 0.84, + "routingTags": ["code"], + "contextWindow": 262144, "cost": { - "inputCentsPerMillion": 25, - "outputCentsPerMillion": 100 - }, - "providerOptions": { - "provider": { - "quantizations": ["fp8", "fp4"] - } + "inputCentsPerMillion": 61.2, + "outputCentsPerMillion": 306.9 }, - "maxOutputTokens": 131072, - "hidden": true + "maxOutputTokens": 262144 }, { "id": "nvidia/nemotron-3-super-120b-a12b", - "displayName": "NVIDIA Nemotron 3 Super", + "displayName": "Nemotron 3 Super", "description": "NVIDIA frontier open-weight reasoning model", "tags": ["chat"], "reasoning": { @@ -364,6 +265,22 @@ "outputCentsPerMillion": 45 } }, + { + "id": "nvidia/nemotron-3-ultra-550b-a55b", + "displayName": "Nemotron 3 Ultra", + "description": "NVIDIA's flagship open-weight reasoning model (550B / 55B active), 1M context", + "tags": ["chat"], + "reasoning": { + "knob": "effort" + }, + "qualityScore": 0.82, + "contextWindow": 1000000, + "cost": { + "inputCentsPerMillion": 50, + "outputCentsPerMillion": 220 + }, + "maxOutputTokens": 16384 + }, { "id": "qwen/qwen3.6-max-preview", "displayName": "Qwen3.6 Max Preview", @@ -380,23 +297,6 @@ }, "maxOutputTokens": 65536 }, - { - "id": "qwen/qwen3.6-plus", - "displayName": "Qwen3.6 Plus", - "description": "Multimodal Qwen with 1M context", - "tags": ["chat", "vision"], - "qualityScore": 0.82, - "contextWindow": 1000000, - "cost": { - "inputCentsPerMillion": 32.5, - "outputCentsPerMillion": 195 - }, - "maxOutputTokens": 65536, - "reasoning": { - "knob": "effort" - }, - "hidden": true - }, { "id": "qwen/qwen3.6-flash", "displayName": "Qwen3.6 Flash", @@ -413,43 +313,6 @@ "knob": "effort" } }, - { - "id": "qwen/qwen3.6-35b-a3b", - "displayName": "Qwen3.6 35B A3B", - "description": "Compact Qwen3.6 MoE, multimodal", - "tags": ["chat", "vision"], - "qualityScore": 0.72, - "contextWindow": 262144, - "cost": { - "inputCentsPerMillion": 15, - "outputCentsPerMillion": 100 - }, - "maxOutputTokens": 262144, - "reasoning": { - "knob": "effort" - } - }, - { - "id": "qwen/qwen3.5-397b-a17b", - "displayName": "Qwen3.5 397B A17B", - "description": "Qwen3.5 large open-weight MoE (397B / 17B active)", - "tags": ["chat"], - "reasoning": { - "knob": "effort" - }, - "qualityScore": 0.84, - "contextWindow": 262144, - "cost": { - "inputCentsPerMillion": 39, - "outputCentsPerMillion": 234 - }, - "providerOptions": { - "provider": { - "quantizations": ["fp8", "fp4"] - } - }, - "maxOutputTokens": 65536 - }, { "id": "qwen/qwen3-coder", "displayName": "Qwen3 Coder 480B", @@ -464,22 +327,6 @@ }, "maxOutputTokens": 65536 }, - { - "id": "qwen/qwen3-235b-a22b-2507", - "displayName": "Qwen3 235B A22B", - "description": "Qwen3 open-weight MoE (235B / 22B active), 262K context", - "tags": ["chat"], - "reasoning": { - "knob": "effort" - }, - "qualityScore": 0.78, - "contextWindow": 262144, - "cost": { - "inputCentsPerMillion": 9, - "outputCentsPerMillion": 10 - }, - "maxOutputTokens": 16384 - }, { "id": "qwen/qwen3-vl-32b-instruct", "displayName": "Qwen3 VL 32B", @@ -565,20 +412,6 @@ "outputCentsPerMillion": 150 } }, - { - "id": "mistralai/mistral-medium-3", - "displayName": "Mistral Medium 3", - "description": "Mid-tier Mistral for general tasks", - "tags": ["chat"], - "maxOutputTokens": 8192, - "qualityScore": 0.74, - "contextWindow": 131072, - "cost": { - "inputCentsPerMillion": 40, - "outputCentsPerMillion": 200 - }, - "hidden": true - }, { "id": "xiaomi/mimo-v2.5-pro", "displayName": "MiMo V2.5 Pro", @@ -692,7 +525,8 @@ }, { "id": "qwen/qwen3.7-plus", - "displayName": "Qwen: Qwen3.7 Plus", + "displayName": "Qwen3.7 Plus", + "description": "Alibaba Qwen3.7 flagship — multimodal, 1M context", "tags": ["chat", "vision"], "reasoning": { "knob": "effort" @@ -706,7 +540,8 @@ }, { "id": "minimax/minimax-m3", - "displayName": "MiniMax: MiniMax M3", + "displayName": "MiniMax M3", + "description": "MiniMax flagship MoE — multimodal, 1M context", "tags": ["chat", "vision"], "reasoning": { "knob": "effort" @@ -720,7 +555,8 @@ }, { "id": "anthropic/claude-opus-4.8", - "displayName": "Anthropic: Claude Opus 4.8", + "displayName": "Claude Opus 4.8", + "description": "Anthropic's flagship — best for complex reasoning and coding", "tags": ["chat", "vision"], "reasoning": { "knob": "budgetTokens", @@ -739,7 +575,8 @@ }, { "id": "mistralai/mistral-medium-3-5", - "displayName": "Mistral: Mistral Medium 3.5", + "displayName": "Mistral Medium 3.5", + "description": "Mistral's balanced multimodal mid-tier model", "tags": ["chat", "vision"], "reasoning": { "knob": "effort" @@ -752,7 +589,8 @@ }, { "id": "openai/gpt-5.5-pro", - "displayName": "OpenAI: GPT-5.5 Pro", + "displayName": "GPT-5.5 Pro", + "description": "OpenAI's most advanced model for the hardest reasoning tasks", "tags": ["chat", "vision"], "reasoning": { "knob": "effort" @@ -769,7 +607,8 @@ }, { "id": "openai/gpt-5.5", - "displayName": "OpenAI: GPT-5.5", + "displayName": "GPT-5.5", + "description": "OpenAI's flagship general-purpose model", "tags": ["chat", "vision"], "reasoning": { "knob": "effort" @@ -786,7 +625,8 @@ }, { "id": "openai/gpt-5.3-chat", - "displayName": "OpenAI: GPT-5.3 Chat", + "displayName": "GPT-5.3 Chat", + "description": "Low-latency GPT-5 variant tuned for chat", "tags": ["chat", "vision"], "promptCaching": { "mode": "auto-server" @@ -797,30 +637,170 @@ "inputCentsPerMillion": 175, "outputCentsPerMillion": 1400 } + }, + { + "id": "x-ai/grok-4.20", + "displayName": "Grok 4.20", + "description": "xAI's flagship Grok — 2M-context multimodal reasoning with agentic tool use", + "tags": ["chat", "vision"], + "reasoning": { + "knob": "effort" + }, + "promptCaching": { + "mode": "auto-server" + }, + "qualityScore": 0.93, + "contextWindow": 2000000, + "cost": { + "inputCentsPerMillion": 125, + "outputCentsPerMillion": 250 + } + }, + { + "id": "cohere/command-a", + "displayName": "Command A", + "description": "Cohere's flagship — enterprise RAG and tool use, 256K context", + "tags": ["chat"], + "qualityScore": 0.8, + "contextWindow": 256000, + "cost": { + "inputCentsPerMillion": 250, + "outputCentsPerMillion": 1000 + }, + "maxOutputTokens": 8192 + }, + { + "id": "cohere/command-r-08-2024", + "displayName": "Command R", + "description": "Fast, low-cost Cohere model for RAG and retrieval", + "tags": ["chat"], + "qualityScore": 0.68, + "contextWindow": 128000, + "cost": { + "inputCentsPerMillion": 15, + "outputCentsPerMillion": 60 + }, + "maxOutputTokens": 4000 + }, + { + "id": "microsoft/phi-4", + "displayName": "Phi-4", + "description": "Microsoft's compact open-weight model for efficient reasoning", + "tags": ["chat"], + "qualityScore": 0.62, + "contextWindow": 16384, + "cost": { + "inputCentsPerMillion": 6.5, + "outputCentsPerMillion": 14 + }, + "maxOutputTokens": 16384 + }, + { + "id": "amazon/nova-premier-v1", + "displayName": "Nova Premier", + "description": "Amazon's most capable Nova model — multimodal, 1M context", + "tags": ["chat", "vision"], + "qualityScore": 0.8, + "contextWindow": 1000000, + "cost": { + "inputCentsPerMillion": 250, + "outputCentsPerMillion": 1250 + }, + "maxOutputTokens": 32000 + }, + { + "id": "amazon/nova-2-lite-v1", + "displayName": "Nova 2 Lite", + "description": "Fast, low-cost multimodal Nova with reasoning, 1M context", + "tags": ["chat", "vision"], + "reasoning": { + "knob": "effort" + }, + "qualityScore": 0.72, + "contextWindow": 1000000, + "cost": { + "inputCentsPerMillion": 30, + "outputCentsPerMillion": 250 + }, + "maxOutputTokens": 65535 + }, + { + "id": "perplexity/sonar-pro", + "displayName": "Sonar Pro", + "description": "Perplexity's flagship answer engine with real-time web search and citations", + "tags": ["chat", "vision"], + "qualityScore": 0.78, + "contextWindow": 200000, + "cost": { + "inputCentsPerMillion": 300, + "outputCentsPerMillion": 1500 + }, + "maxOutputTokens": 8000 + }, + { + "id": "perplexity/sonar", + "displayName": "Sonar", + "description": "Low-cost Perplexity model with real-time web search", + "tags": ["chat", "vision"], + "qualityScore": 0.7, + "contextWindow": 127072, + "cost": { + "inputCentsPerMillion": 100, + "outputCentsPerMillion": 100 + } + }, + { + "id": "ai21/jamba-large-1.7", + "displayName": "Jamba Large 1.7", + "description": "AI21's hybrid SSM-Transformer with very long context", + "tags": ["chat"], + "qualityScore": 0.72, + "contextWindow": 256000, + "cost": { + "inputCentsPerMillion": 200, + "outputCentsPerMillion": 800 + }, + "maxOutputTokens": 4096 + }, + { + "id": "rekaai/reka-flash-3", + "displayName": "Reka Flash 3", + "description": "Reka's fast, efficient reasoning model", + "tags": ["chat"], + "reasoning": { + "knob": "effort" + }, + "qualityScore": 0.66, + "contextWindow": 65536, + "cost": { + "inputCentsPerMillion": 10, + "outputCentsPerMillion": 20 + }, + "maxOutputTokens": 65536 + }, + { + "id": "liquid/lfm-2-24b-a2b", + "displayName": "LFM2 24B", + "description": "Liquid's efficient LFM2 MoE for fast, low-cost inference", + "tags": ["chat"], + "qualityScore": 0.6, + "contextWindow": 128000, + "cost": { + "inputCentsPerMillion": 3, + "outputCentsPerMillion": 12 + } } ], "i18n": { "de": { "description": "Multi-Modell-KI-Gateway mit Zugang zu führenden LLM-Anbietern", "models": { - "anthropic/claude-opus-4.6": { - "description": "Leistungsstärkstes Modell für komplexe Aufgaben und Programmierung" - }, "anthropic/claude-sonnet-4.6": { "description": "Ausgewogenes Anthropic-Modell für alltägliche Aufgaben" }, "anthropic/claude-haiku-4.5": { "description": "Schnelles, schlankes Anthropic-Modell" }, - "openai/gpt-5.2-pro": { - "description": "OpenAIs fortschrittlichstes Modell für anspruchsvollste Denkaufgaben" - }, - "openai/gpt-5.2": { - "description": "OpenAIs Flaggschiff-Allzweckmodell" - }, - "openai/gpt-5.2-chat": { - "description": "GPT-5.2-Variante mit niedriger Latenz, optimiert für Chat" - }, "openai/gpt-oss-120b": { "description": "OpenAI open-weight MoE (117B / 5,1B aktiv), Apache 2.0, 131K Kontext" }, @@ -848,36 +828,18 @@ "moonshotai/kimi-k2.6": { "description": "Moonshot K2.6 — multimodal, 262K Kontext" }, - "moonshotai/kimi-k2.5": { - "description": "Hochleistungs-Allzweckmodell von Moonshot" - }, - "minimax/minimax-m2.7": { - "description": "MiniMax-Flaggschiff-MoE für agentische Programmierung und Reasoning" - }, "nvidia/nemotron-3-super-120b-a12b": { "description": "NVIDIAs open-weight-Reasoning-Modell der Spitzenklasse" }, "qwen/qwen3.6-max-preview": { "description": "Alibaba-Qwen-Flaggschiff-Vorschau, 262K Kontext" }, - "qwen/qwen3.6-plus": { - "description": "Multimodales Qwen mit 1M Kontext" - }, "qwen/qwen3.6-flash": { "description": "Schnellste Qwen-Variante mit 1M Kontext, multimodal" }, - "qwen/qwen3.6-35b-a3b": { - "description": "Kompaktes Qwen3.6-MoE, multimodal" - }, - "qwen/qwen3.5-397b-a17b": { - "description": "Großes open-weight Qwen3.5-MoE (397B / 17B aktiv)" - }, "qwen/qwen3-coder": { "description": "Qwen3-Coder-MoE (480B / 35B aktiv), abgestimmt auf agentische Programmierung, 262K Kontext" }, - "qwen/qwen3-235b-a22b-2507": { - "description": "Qwen3 open-weight MoE (235B / 22B aktiv), 262K Kontext" - }, "qwen/qwen3-vl-32b-instruct": { "description": "Qwen-Vision-Language-Modell zum Bildverständnis" }, @@ -896,9 +858,6 @@ "mistralai/mistral-large-2512": { "description": "Mistrals Flaggschiff-Großmodell" }, - "mistralai/mistral-medium-3": { - "description": "Mittelklasse-Mistral für allgemeine Aufgaben" - }, "xiaomi/mimo-v2.5-pro": { "description": "Xiaomis Flaggschiff-Reasoning-Modell MiMo" }, @@ -922,30 +881,78 @@ }, "openai/gpt-4o-mini-tts-2025-12-15": { "description": "Günstige Sprachsynthese für den Voice-Modus, locale-zugeordnete Stimmen. Aktuelle Modell-id unter openrouter.ai/models (Text-to-Speech) prüfen — TTS-Versionen sind datiert und rotieren; id + defaults.text-to-speech gemeinsam aktualisieren. HINWEIS: Der cost-Wert ist eine Zeichen-Näherung nur für Tales Budget/Ledger; der Upstream rechnet pro Token ab — gegen den realen Nutzungs-Mix kalibrieren." + }, + "x-ai/grok-4.20": { + "description": "xAIs Flaggschiff Grok — multimodales Reasoning mit 2M Kontext und agentischer Tool-Nutzung" + }, + "cohere/command-a": { + "description": "Coheres Flaggschiff — Unternehmens-RAG und Tool-Nutzung, 256K Kontext" + }, + "cohere/command-r-08-2024": { + "description": "Schnelles, günstiges Cohere-Modell für RAG und Retrieval" + }, + "microsoft/phi-4": { + "description": "Microsofts kompaktes open-weight-Modell für effizientes Reasoning" + }, + "amazon/nova-premier-v1": { + "description": "Amazons leistungsfähigstes Nova-Modell — multimodal, 1M Kontext" + }, + "amazon/nova-2-lite-v1": { + "description": "Schnelles, günstiges multimodales Nova mit Reasoning, 1M Kontext" + }, + "perplexity/sonar-pro": { + "description": "Perplexitys Flaggschiff-Antwortmaschine mit Echtzeit-Websuche und Quellenangaben" + }, + "perplexity/sonar": { + "description": "Günstiges Perplexity-Modell mit Echtzeit-Websuche" + }, + "ai21/jamba-large-1.7": { + "description": "AI21s hybrides SSM-Transformer-Modell mit sehr langem Kontext" + }, + "rekaai/reka-flash-3": { + "description": "Rekas schnelles, effizientes Reasoning-Modell" + }, + "liquid/lfm-2-24b-a2b": { + "description": "Liquids effizientes LFM2-MoE für schnelle, günstige Inferenz" + }, + "nvidia/nemotron-3-ultra-550b-a55b": { + "description": "NVIDIAs Flaggschiff-open-weight-Reasoning-Modell (550B / 55B aktiv), 1M Kontext" + }, + "moonshotai/kimi-k2.7-code": { + "description": "Moonshot Kimi K2.7, spezialisiert auf agentisches Programmieren, multimodal, 262K Kontext" + }, + "qwen/qwen3.7-plus": { + "description": "Alibaba-Qwen3.7-Flaggschiff — multimodal, 1M Kontext" + }, + "minimax/minimax-m3": { + "description": "MiniMax-Flaggschiff-MoE — multimodal, 1M Kontext" + }, + "anthropic/claude-opus-4.8": { + "description": "Anthropics Flaggschiff — am besten für komplexes Reasoning und Programmierung" + }, + "mistralai/mistral-medium-3-5": { + "description": "Ausgewogenes multimodales Mittelklasse-Modell von Mistral" + }, + "openai/gpt-5.5-pro": { + "description": "OpenAIs fortschrittlichstes Modell für anspruchsvollste Denkaufgaben" + }, + "openai/gpt-5.5": { + "description": "OpenAIs Flaggschiff-Allzweckmodell" + }, + "openai/gpt-5.3-chat": { + "description": "GPT-5-Variante mit niedriger Latenz, optimiert für Chat" } } }, "fr": { "description": "Passerelle IA multi-modèles avec accès aux principaux fournisseurs de LLM", "models": { - "anthropic/claude-opus-4.6": { - "description": "Le modèle phare d'Anthropic — idéal pour le raisonnement complexe et le code" - }, "anthropic/claude-sonnet-4.6": { "description": "Modèle Anthropic équilibré pour les tâches courantes" }, "anthropic/claude-haiku-4.5": { "description": "Modèle Anthropic rapide et léger" }, - "openai/gpt-5.2-pro": { - "description": "Le modèle le plus avancé d'OpenAI pour les tâches de raisonnement les plus exigeantes" - }, - "openai/gpt-5.2": { - "description": "Modèle généraliste phare d'OpenAI" - }, - "openai/gpt-5.2-chat": { - "description": "Variante GPT-5.2 à faible latence optimisée pour le chat" - }, "openai/gpt-oss-120b": { "description": "MoE open-weight d'OpenAI (117B / 5,1B actifs), Apache 2.0, contexte 131K" }, @@ -973,36 +980,18 @@ "moonshotai/kimi-k2.6": { "description": "Moonshot K2.6 — multimodal, contexte 262K" }, - "moonshotai/kimi-k2.5": { - "description": "Modèle généraliste haute performance de Moonshot" - }, - "minimax/minimax-m2.7": { - "description": "MoE phare de MiniMax pour le code agentique et le raisonnement" - }, "nvidia/nemotron-3-super-120b-a12b": { "description": "Modèle de raisonnement open-weight de pointe de NVIDIA" }, "qwen/qwen3.6-max-preview": { "description": "Aperçu phare d'Alibaba Qwen, contexte 262K" }, - "qwen/qwen3.6-plus": { - "description": "Qwen multimodal avec contexte 1M" - }, "qwen/qwen3.6-flash": { "description": "La variante Qwen la plus rapide à contexte 1M, multimodale" }, - "qwen/qwen3.6-35b-a3b": { - "description": "MoE Qwen3.6 compact, multimodal" - }, - "qwen/qwen3.5-397b-a17b": { - "description": "Grand MoE open-weight Qwen3.5 (397B / 17B actifs)" - }, "qwen/qwen3-coder": { "description": "MoE Qwen3-Coder (480B / 35B actifs) optimisé pour le code agentique, contexte 262K" }, - "qwen/qwen3-235b-a22b-2507": { - "description": "MoE Qwen3 open-weight (235B / 22B actifs), contexte 262K" - }, "qwen/qwen3-vl-32b-instruct": { "description": "Modèle vision-langage Qwen pour la compréhension d'images" }, @@ -1021,9 +1010,6 @@ "mistralai/mistral-large-2512": { "description": "Grand modèle phare de Mistral" }, - "mistralai/mistral-medium-3": { - "description": "Mistral milieu de gamme pour les tâches générales" - }, "xiaomi/mimo-v2.5-pro": { "description": "Modèle de raisonnement phare MiMo de Xiaomi" }, @@ -1047,6 +1033,66 @@ }, "openai/gpt-4o-mini-tts-2025-12-15": { "description": "Synthèse vocale économique pour le mode vocal, voix mappées par locale. Vérifie l'id du modèle actuel sur openrouter.ai/models (Text-to-Speech) — les versions TTS sont datées et changent ; mets à jour l'id + defaults.text-to-speech ensemble. NOTE : le coût ci-dessous est une approximation par caractère utilisée uniquement par le budget/ledger de Tale ; l'upstream facture au token — recalibre selon ton usage réel." + }, + "x-ai/grok-4.20": { + "description": "Le modèle phare Grok de xAI — raisonnement multimodal avec contexte 2M et usage agentique d'outils" + }, + "cohere/command-a": { + "description": "Le modèle phare de Cohere — RAG d'entreprise et usage d'outils, contexte 256K" + }, + "cohere/command-r-08-2024": { + "description": "Modèle Cohere rapide et économique pour le RAG et la recherche" + }, + "microsoft/phi-4": { + "description": "Modèle open-weight compact de Microsoft pour un raisonnement efficace" + }, + "amazon/nova-premier-v1": { + "description": "Le modèle Nova le plus performant d'Amazon — multimodal, contexte 1M" + }, + "amazon/nova-2-lite-v1": { + "description": "Nova multimodal rapide et économique avec raisonnement, contexte 1M" + }, + "perplexity/sonar-pro": { + "description": "Le moteur de réponse phare de Perplexity avec recherche web en temps réel et citations" + }, + "perplexity/sonar": { + "description": "Modèle Perplexity économique avec recherche web en temps réel" + }, + "ai21/jamba-large-1.7": { + "description": "Modèle hybride SSM-Transformer d'AI21 à très long contexte" + }, + "rekaai/reka-flash-3": { + "description": "Modèle de raisonnement rapide et efficace de Reka" + }, + "liquid/lfm-2-24b-a2b": { + "description": "MoE LFM2 efficace de Liquid pour une inférence rapide et économique" + }, + "nvidia/nemotron-3-ultra-550b-a55b": { + "description": "Le modèle de raisonnement open-weight phare de NVIDIA (550B / 55B actifs), contexte 1M" + }, + "moonshotai/kimi-k2.7-code": { + "description": "Kimi K2.7 de Moonshot, spécialisé dans le code agentique, multimodal, contexte 262K" + }, + "qwen/qwen3.7-plus": { + "description": "Modèle phare Alibaba Qwen3.7 — multimodal, contexte 1M" + }, + "minimax/minimax-m3": { + "description": "MoE phare de MiniMax — multimodal, contexte 1M" + }, + "anthropic/claude-opus-4.8": { + "description": "Le modèle phare d'Anthropic — idéal pour le raisonnement complexe et le code" + }, + "mistralai/mistral-medium-3-5": { + "description": "Modèle Mistral milieu de gamme, multimodal et équilibré" + }, + "openai/gpt-5.5-pro": { + "description": "Le modèle le plus avancé d'OpenAI pour les tâches de raisonnement les plus exigeantes" + }, + "openai/gpt-5.5": { + "description": "Modèle généraliste phare d'OpenAI" + }, + "openai/gpt-5.3-chat": { + "description": "Variante GPT-5 à faible latence optimisée pour le chat" } } } diff --git a/docs/de/cloud/onboarding.md b/docs/de/cloud/onboarding.md index c4c0540dc2..37440c9d50 100644 --- a/docs/de/cloud/onboarding.md +++ b/docs/de/cloud/onboarding.md @@ -41,7 +41,7 @@ Für einen tieferen Spaziergang dazu, was einen Agent gut macht, siehe [Einen Ag ## Schritt 5 — Chat öffnen -Öffne **Chat** in der Sidebar und klick **Neuer Chat**. Wähl den Agent im Picker, tipp eine Frage, die die Domäne des Agents abdeckt, sende. Die Antwort streamt zurück; landet sie so, wie du die Instructions geschrieben hast, ist die Org mit dem Onboarding fertig. +Klick in der Sidebar auf **Neuer Chat**. Wähl den Agent im Picker, tipp eine Frage, die die Domäne des Agents abdeckt, sende. Die Antwort streamt zurück; landet sie so, wie du die Instructions geschrieben hast, ist die Org mit dem Onboarding fertig. Drei Folgeaufgaben, die sich jetzt lohnen, solange alles frisch ist: diff --git a/docs/de/develop/ai-assisted-development.md b/docs/de/develop/ai-assisted-development.md index b0bda80e92..226723f30a 100644 --- a/docs/de/develop/ai-assisted-development.md +++ b/docs/de/develop/ai-assisted-development.md @@ -27,14 +27,14 @@ Die Direktive zählt, weil jeder Editor unter Last Schema-Reads überspringt, so ## Was wo liegt -| Pfad | Was es ist | -| ---------------------------------- | ------------------------------------------------------------------------------------ | -| `agents/` | Eine JSON-Datei pro Agent — Anweisungen, Wissen, Tools, Modell. | -| `workflows/` | Workflow-JSON-Configs, gruppiert nach Kategorie-Unterverzeichnis. | -| `integrations//config.json` | Integration-Manifest — Operations, Auth-Methode, erlaubte Hosts. | -| `integrations//connector.ts` | Optionaler TypeScript-Connector für REST-Formen, die das Manifest nicht abdeckt. | -| `branding/branding.json` | Org-Branding — Farben, Logos, E-Mail-Absender. | -| `.tale/reference/` | Schreibgeschützter Schema-Spiegel; neu erzeugt durch `tale init` und `tale upgrade`. | +| Pfad | Was es ist | +| ---------------------------------- | ----------------------------------------------------------------------------------- | +| `agents/` | Eine JSON-Datei pro Agent — Anweisungen, Wissen, Tools, Modell. | +| `workflows/` | Workflow-JSON-Configs, gruppiert nach Kategorie-Unterverzeichnis. | +| `integrations//config.json` | Integration-Manifest — Operations, Auth-Methode, erlaubte Hosts. | +| `integrations//connector.ts` | Optionaler TypeScript-Connector für REST-Formen, die das Manifest nicht abdeckt. | +| `branding/branding.json` | Org-Branding — Farben, Logos, E-Mail-Absender. | +| `.tale/reference/` | Schreibgeschützter Schema-Spiegel; neu erzeugt durch `tale init` und `tale update`. | Der Reference-Baum ist byte-identisch zu den Schemas, gegen die die Plattform beim Deploy validiert. Behandle ihn als kanonisch: wenn ein Feldname in einer handgeschriebenen Config dem Reference widerspricht, gewinnt das Reference. @@ -46,7 +46,7 @@ Die Rules-Datei nennt drei Regeln, die jeder Editor beim Bearbeiten durchsetzt: - **Workflows nutzen Integration-Operations.** Ein Workflow-Schritt referenziert Integration-Operations, die in `integrations//config.json` deklariert sind. Ein Schritt gegen eine nicht-existente Operation zu bearbeiten, lässt die Validierung scheitern. - **Benennung ist erzwungen.** Agent-Dateinamen matchen `[a-z0-9][a-z0-9_-]*\.json`. Workflow-Step-Slugs matchen `[a-z0-9][a-z0-9_-]*`. Integration-Verzeichnisse sind kleingeschrieben alphanumerisch mit Bindestrichen oder Unterstrichen. -Wenn der Editor eine Änderung vorschlägt, frag ihn, welche Datei in `.tale/reference/` er zugrunde gelegt hat. Wenn er das nicht kann, erzeug den Spiegel mit `tale upgrade` neu und versuch es nochmal. +Wenn der Editor eine Änderung vorschlägt, frag ihn, welche Datei in `.tale/reference/` er zugrunde gelegt hat. Wenn er das nicht kann, erzeug den Spiegel mit `tale update` neu und versuch es nochmal. ## Wo das hingehört diff --git a/docs/de/platform/agents/external-agent.md b/docs/de/platform/agents/external-agent.md index 425ddf1ee7..f9fdd85d66 100644 --- a/docs/de/platform/agents/external-agent.md +++ b/docs/de/platform/agents/external-agent.md @@ -29,7 +29,7 @@ Wie der Agent sein Modell erreicht, ist eine Entscheidung pro Agent, die du im * **Verwaltet (Plattform-Gateway)** ist die Voreinstellung und das, was alle obigen Abschnitte beschreiben. Die Plattform prägt für die Runde einen kurzlebigen virtuellen Schlüssel, leitet den Agenten über ihr Gateway, erzwingt die erlaubten Modelle des Agenten, erfasst die Nutzung und wendet die Ausgabengrenzen der Organisation an. Die Sandbox hält nie einen echten Provider-Schlüssel. -**Eigene Anmeldedaten (BYO)** nimmt die Plattform aus dem Anfragepfad heraus. Es wird kein virtueller Schlüssel geprägt; der Agent authentifiziert sich mit Anmeldedaten, die du unter [Umgebung & Geheimnisse](/de/platform/member/environment) hinterlegst, und erreicht den Provider direkt. Das Modell wird zu einer rohen Provider-ID, die du wortwörtlich eintippst, statt zu einem Katalogeintrag, und die native Websuche und das native Abrufen des Agenten bleiben aktiviert, statt deaktiviert zu werden. Weil das Gateway umgangen wird, gelten die Modell-Erlaubnisliste, die Ausgabengrenzen und die Nutzungserfassung der Organisation nicht für BYO-Runden — Abrechnung und Limits wandern in dein eigenes Provider-Konto. Wechselst du einen Agenten von verwaltet auf BYO, werden seine gespeicherten Plattform-Modelle gelöscht, da Katalogverweise für eine rohe Durchleitung bedeutungslos sind; du gibst die rohen IDs neu ein. +**Eigene Anmeldedaten (BYO)** nimmt die Plattform aus dem Anfragepfad heraus. Es wird kein virtueller Schlüssel geprägt; der Agent authentifiziert sich mit Anmeldedaten, die du unter [Umgebungsvariablen & Geheimnisse](/de/platform/member/environment) hinterlegst, und erreicht den Provider direkt. Das Modell wird zu einer rohen Provider-ID, die du wortwörtlich eintippst, statt zu einem Katalogeintrag, und die native Websuche und das native Abrufen des Agenten bleiben aktiviert, statt deaktiviert zu werden. Weil das Gateway umgangen wird, gelten die Modell-Erlaubnisliste, die Ausgabengrenzen und die Nutzungserfassung der Organisation nicht für BYO-Runden — Abrechnung und Limits wandern in dein eigenes Provider-Konto. Wechselst du einen Agenten von verwaltet auf BYO, werden seine gespeicherten Plattform-Modelle gelöscht, da Katalogverweise für eine rohe Durchleitung bedeutungslos sind; du gibst die rohen IDs neu ein. Das verschiebt auch die Vertrauensgrenze. Im verwalteten Modus hält die Sandbox nur einen budgetbegrenzten Gateway-Schlüssel; im BYO-Modus wird deine echte Provider-Anmeldedaten in die Sandbox-Umgebung eingespeist — dieselbe Lage wie beim GitHub-Token in der Sandbox —, sodass jeder Code, den der Agent in der Box ausführt, sie lesen kann. Das ist Absicht: Es ist deine Box und deine Anmeldedaten. Einen Agenten zu konfigurieren ist ohnehin eine privilegierte Aktion, daher ist der Schalter pro Agent die einzige Stellschraube; es gibt keinen separaten Schalter auf Organisationsebene. @@ -47,4 +47,4 @@ Diese Abrechnung ist eine Eigenschaft des Gateways und gilt daher nur für verwa ## Wo das hineinpasst -Ein externer Agent verwandelt einen Chat-Thread in eine Live-Sitzung mit einem Coding-Werkzeug in einer Sandbox — du steuerst ihn in normaler Sprache, er arbeitet in einem isolierten Arbeitsbereich, und die Sitzung bleibt für Folgefragen bestehen, bis du den Thread schließt. Die Anmeldedaten sind die Achse, die entscheidet, wie viel davon unter der Kontrolle der Organisation läuft: Ein verwalteter Agent bleibt am Plattform-Gateway unter den Grenzen und der Erfassung der Organisation, während ein Agent mit eigenen Anmeldedaten mit den Schlüsseln läuft, die du unter [Umgebung & Geheimnisse](/de/platform/member/environment) hinterlegst, und deinem eigenen Provider-Konto gegenüber rechenschaftspflichtig ist. Die Drift-Kandidaten hier sind die Agenten- und Modellnamen; kombiniere diese Seite mit der laufenden [Provider-Liste](/de/platform/admin/providers), statt dir bestimmte Modellzeichenketten zu merken, und mit [Integrationen](/de/platform/integrations/overview) für die verbundenen Integrationen, die der Agent erreichen kann — von GitHub für einen echten Pull-Request-Workflow bis zu einer Such- oder Datenintegration, die externe Fakten in die Arbeit holt. +Ein externer Agent verwandelt einen Chat-Thread in eine Live-Sitzung mit einem Coding-Werkzeug in einer Sandbox — du steuerst ihn in normaler Sprache, er arbeitet in einem isolierten Arbeitsbereich, und die Sitzung bleibt für Folgefragen bestehen, bis du den Thread schließt. Die Anmeldedaten sind die Achse, die entscheidet, wie viel davon unter der Kontrolle der Organisation läuft: Ein verwalteter Agent bleibt am Plattform-Gateway unter den Grenzen und der Erfassung der Organisation, während ein Agent mit eigenen Anmeldedaten mit den Schlüsseln läuft, die du unter [Umgebungsvariablen & Geheimnisse](/de/platform/member/environment) hinterlegst, und deinem eigenen Provider-Konto gegenüber rechenschaftspflichtig ist. Die Drift-Kandidaten hier sind die Agenten- und Modellnamen; kombiniere diese Seite mit der laufenden [Provider-Liste](/de/platform/admin/providers), statt dir bestimmte Modellzeichenketten zu merken, und mit [Integrationen](/de/platform/integrations/overview) für die verbundenen Integrationen, die der Agent erreichen kann — von GitHub für einen echten Pull-Request-Workflow bis zu einer Such- oder Datenintegration, die externe Fakten in die Arbeit holt. diff --git a/docs/de/platform/member/environment.md b/docs/de/platform/member/environment.md index 36512f5100..8d70d151db 100644 --- a/docs/de/platform/member/environment.md +++ b/docs/de/platform/member/environment.md @@ -1,15 +1,15 @@ --- -title: Umgebung & Geheimnisse +title: Umgebungsvariablen & Geheimnisse description: Deine persönlichen Umgebungsvariablen und Geheimnisse, die Tale in jede Agent-Sandbox einspeist, die du in einer Organisation startest — meist die Provider-Anmeldedaten, mit denen sich ein BYO-Agent authentifiziert. --- -Umgebung & Geheimnisse ist dein persönlicher Speicher für Variablen, die Tale in jede Agent-Sandbox einspeist, die du in dieser Organisation startest. Startet ein externer Agent seine Sandbox, setzt Tale jeden hier gespeicherten Eintrag in die Umgebung des Containers, bevor der Agent läuft — ein Befehl, den der Agent absetzt, oder der Agent selbst kann ihn dann lesen. Der Hauptzweck sind Anmeldedaten: ein [externer BYO-Agent](/de/platform/agents/external-agent) authentifiziert sich mit dem API-Schlüssel oder Token, den du hier hältst, statt über das Plattform-Gateway. Es ist eine Seite auf Mitglieds-Ebene, die jede Rolle erreicht, und die Einträge sind auf dich und die aktuelle Organisation begrenzt — sie lecken nie zu Teamkolleginnen durch und folgen dir nie in eine andere Organisation. +Umgebungsvariablen & Geheimnisse ist dein persönlicher Speicher für Variablen, die Tale in jede Agent-Sandbox einspeist, die du in dieser Organisation startest. Startet ein externer Agent seine Sandbox, setzt Tale jeden hier gespeicherten Eintrag in die Umgebung des Containers, bevor der Agent läuft — ein Befehl, den der Agent absetzt, oder der Agent selbst kann ihn dann lesen. Der Hauptzweck sind Anmeldedaten: ein [externer BYO-Agent](/de/platform/agents/external-agent) authentifiziert sich mit dem API-Schlüssel oder Token, den du hier hältst, statt über das Plattform-Gateway. Es ist eine Seite auf Mitglieds-Ebene, die jede Rolle erreicht, und die Einträge sind auf dich und die aktuelle Organisation begrenzt — sie lecken nie zu Teamkolleginnen durch und folgen dir nie in eine andere Organisation. Diese Seite zeigt die zwei Arten von Eintrag, wie Geheimnisse geschützt werden, welche Regeln Name und Wert erfüllen müssen, und wo die Werte landen. ## Variablen und Geheimnisse -Öffne **Einstellungen > Umgebung**. Die Seite ist ein Hinzufügen-Formular oben und darunter die Liste dessen, was du gespeichert hast. Jeder Eintrag hat einen **Name** und einen **Wert**, dazu einen **Geheimnis**-Schalter, der entscheidet, wie der Wert gespeichert und angezeigt wird. +Öffne **Einstellungen > Umgebung**. **Variable hinzufügen** öffnet einen Dialog für einen neuen Eintrag, darunter steht die Liste dessen, was du gespeichert hast. Jeder Eintrag hat einen **Name** und einen **Wert**, dazu einen **Geheimnis**-Schalter, der entscheidet, wie der Wert gespeichert und angezeigt wird. Eine einfache Variable wird unverändert gespeichert und in der Liste in voller Länge zurückgezeigt — nimm sie für unkritische Konfiguration, die der Agent erwartet, einen Regionsnamen oder einen Endpunkt. Ein **Geheimnis** wird im Moment des Speicherns verschlüsselt und ist von da an schreibgeschützt: Die Liste zeigt `••••••••` statt des Werts, und es gibt keinen Weg, ihn zurückzulesen. Schalt den Schalter für alles Sensible ein — einen API-Schlüssel, einen OAuth-Token, ein Passwort. Der Preis dafür ist, dass du den Wert eines Geheimnisses später nicht mehr prüfen kannst: Bist du dir unsicher, ob er stimmt, lösch ihn und füg ihn neu hinzu, statt nach einem Anzeigen-Knopf zu suchen, den es nicht gibt. @@ -29,4 +29,4 @@ Dieser letzte Schritt ist die Grenze, die es zu verstehen lohnt: Die Werte lande ## Wo das hingehört -Umgebung & Geheimnisse ist die eine Seite auf Mitglieds-Ebene, die in die Sandbox reicht statt in den Chat — so kommen deine eigenen Schlüssel und deine Konfiguration zu den Agents, die du startest, ohne dass ein Redakteur oder Admin sie für dich setzt. Der Eintrag, den du am häufigsten hinzufügen wirst, sind die Provider-Anmeldedaten für einen [externen BYO-Agenten](/de/platform/agents/external-agent); lies diese Seite neben jener, um beide Hälften zu sehen — wo die Anmeldedaten liegen und wie einem Agenten gesagt wird, sie statt des Plattform-Gateways zu nutzen. Für den Rest deiner persönlichen Einstellungen — Anzeigename, Passwort, eigene Anweisungen — siehe [Einstellungen](/de/platform/member/preferences). +Umgebungsvariablen & Geheimnisse ist die eine Seite auf Mitglieds-Ebene, die in die Sandbox reicht statt in den Chat — so kommen deine eigenen Schlüssel und deine Konfiguration zu den Agents, die du startest, ohne dass ein Redakteur oder Admin sie für dich setzt. Der Eintrag, den du am häufigsten hinzufügen wirst, sind die Provider-Anmeldedaten für einen [externen BYO-Agenten](/de/platform/agents/external-agent); lies diese Seite neben jener, um beide Hälften zu sehen — wo die Anmeldedaten liegen und wie einem Agenten gesagt wird, sie statt des Plattform-Gateways zu nutzen. Für den Rest deiner persönlichen Einstellungen — Anzeigename, Passwort, eigene Anweisungen — siehe [Einstellungen](/de/platform/member/preferences). diff --git a/docs/de/platform/member/overview.md b/docs/de/platform/member/overview.md index 677b009bf9..11d56b0946 100644 --- a/docs/de/platform/member/overview.md +++ b/docs/de/platform/member/overview.md @@ -16,7 +16,7 @@ Die Mitglieder-Oberfläche ist bewusst eng. Die vier Kübel sind: - **Konversationen** — die Inbox-Threads lesen, die dir zugewiesen sind. Mitglieder antworten, wenn ein Agent eine Konversation zurückgibt; sie können Threads, die anderen gehören, nicht neu zuweisen oder schliessen. - **Genehmigungen** — die Genehmigungs-Karten lesen, die zu dir geroutet wurden. Klick auf Genehmigen, Ablehnen oder Änderungen anfordern; hinterlass einen Kommentar, wenn die Regel danach fragt. -Die Org-Konfigurationseinstellungen — Anbieter, Integrationen, Agents, Governance — sind für Mitglieder ausgeblendet; was bleibt, ist zum Grossteil die Arbeits-Oberfläche. Die Ausnahme ist eine kleine persönliche Einstellungs-Gruppe, die jede Rolle trägt: Konto, Personalisierung und [Umgebung & Geheimnisse](/de/platform/member/environment), die Schlüssel und Variablen, die in die Sandboxes injiziert werden, die du fährst. +Die Org-Konfigurationseinstellungen — Anbieter, Integrationen, Agents, Governance — sind für Mitglieder ausgeblendet; was bleibt, ist zum Grossteil die Arbeits-Oberfläche. Die Ausnahme ist eine kleine persönliche Einstellungs-Gruppe, die jede Rolle trägt: Konto, Personalisierung und [Umgebungsvariablen & Geheimnisse](/de/platform/member/environment), die Schlüssel und Variablen, die in die Sandboxes injiziert werden, die du fährst. ## Seiten in diesem Bereich diff --git a/docs/de/platform/member/preferences.md b/docs/de/platform/member/preferences.md index ab34c77874..f2a4e6c248 100644 --- a/docs/de/platform/member/preferences.md +++ b/docs/de/platform/member/preferences.md @@ -37,4 +37,4 @@ Die Zeile **Abmelden** unten im Profilmenü bestätigt mit einem Dialog, bevor s ## Wo das hingehört -Einstellungen sind die Linie zwischen dir und dem Rest der Organisation. Der Org-Admin setzt Standardwerte — inklusive ob Personalisierung für neue Mitglieder an ist, was die Passwort-Richtlinie ist, welche Modelle erlaubt sind — und deine Einstellungen überschreiben die Standardwerte dort, wo Tale es zulässt. Eine persönliche Seite steht abseits dieses Sets: [Umgebung & Geheimnisse](/de/platform/member/environment) hält Variablen und Anmeldedaten, die innerhalb einer einzelnen Organisation auf dich begrenzt sind, statt dir über Organisationen hinweg zu folgen — der Ort für den Provider-Schlüssel, den ein BYO-Agent benutzt. Die nächste Lektüre, die sich lohnt, ist [Mitglieds-Übersicht](/de/platform/member/overview) für die Karte des restlichen Mitglieder-Bereichs, oder [Als App installieren](/de/platform/member/install-as-app), wenn du willst, dass Tale in deinem Dock statt in deinen Browser-Tabs lebt. +Einstellungen sind die Linie zwischen dir und dem Rest der Organisation. Der Org-Admin setzt Standardwerte — inklusive ob Personalisierung für neue Mitglieder an ist, was die Passwort-Richtlinie ist, welche Modelle erlaubt sind — und deine Einstellungen überschreiben die Standardwerte dort, wo Tale es zulässt. Eine persönliche Seite steht abseits dieses Sets: [Umgebungsvariablen & Geheimnisse](/de/platform/member/environment) hält Variablen und Anmeldedaten, die innerhalb einer einzelnen Organisation auf dich begrenzt sind, statt dir über Organisationen hinweg zu folgen — der Ort für den Provider-Schlüssel, den ein BYO-Agent benutzt. Die nächste Lektüre, die sich lohnt, ist [Mitglieds-Übersicht](/de/platform/member/overview) für die Karte des restlichen Mitglieder-Bereichs, oder [Als App installieren](/de/platform/member/install-as-app), wenn du willst, dass Tale in deinem Dock statt in deinen Browser-Tabs lebt. diff --git a/docs/de/platform/models.md b/docs/de/platform/models.md index e1fe104659..11148c2049 100644 --- a/docs/de/platform/models.md +++ b/docs/de/platform/models.md @@ -17,21 +17,65 @@ OpenRouter ist ein OpenAI-kompatibler Endpunkt, den Tale per HTTPS mit Bearer-To ## OpenRouter — Chat, Vision, Embeddings -OpenRouter ist ein Multi-Modell-Gateway. Die ausgelieferte Konfiguration wählt `deepseek-v4-flash` als Default-Chat-Modell, `qwen3-vl-32b-instruct` für Vision und `qwen3-embedding-8b` für Embeddings — alle wegen des Geschwindigkeits-zu-Qualität-Verhältnisses zum Zeitpunkt des Schreibens. Die volle Lieferliste: - -- **Anthropic** — Claude Opus 4.6, Claude Sonnet 4.6, Claude Haiku 4.5. -- **OpenAI** — GPT-5.2 Pro, GPT-5.2, GPT-5.2 Instant, GPT-OSS 120B (das Open-Weight-Release). -- **Google** — Gemini 3 Pro, Gemini 3 Flash, Gemma 4 31B IT, Gemma 4 26B A4B IT, Nano Banana (Gemini 2.5 Flash Image). -- **DeepSeek** — DeepSeek V4 Pro, DeepSeek V4 Flash. -- **Moonshot AI** — Kimi K2.6, Kimi K2.5. -- **MiniMax** — MiniMax M2.7. -- **NVIDIA** — Nemotron 3 Super 120B. -- **Qwen** — Qwen3.6 Max Preview, Qwen3.6 Plus, Qwen3.6 Flash, Qwen3.6 35B A3B, Qwen3.5 397B A17B, Qwen3 Coder 480B, Qwen3 235B A22B, Qwen3 VL 32B, Qwen3 Embedding 8B. -- **Z.AI** — GLM 5.1, GLM 5 Turbo, GLM 5V Turbo. -- **Mistral** — Mistral Large 3, Mistral Medium 3. -- **Xiaomi** — MiMo V2.5 Pro. -- **Meta** — LLaMA 4 Maverick, LLaMA 4 Scout. -- **Black Forest Labs** — FLUX.2 [max], FLUX.2 [pro], FLUX.2 [flex]. +OpenRouter ist ein Multi-Modell-Gateway. Die ausgelieferte Konfiguration wählt `deepseek-v4-flash` als Default-Chat-Modell, `qwen3-vl-32b-instruct` für Vision und `qwen3-embedding-8b` für Embeddings — alle wegen des Geschwindigkeits-zu-Qualität-Verhältnisses zum Zeitpunkt des Schreibens. Die volle Lieferliste unten umfasst jedes sichtbare Modell der ausgelieferten `openrouter.json` und wird vom wöchentlichen Katalog-Job neu generiert, damit sie nie von der Konfiguration abweicht: + + + + + +| Anbieter | Modell | Fähigkeiten | Kontext | Eingabe ($/M) | Ausgabe ($/M) | +| ----------------- | ------------------------------------ | ---------------------------- | ------- | ------------- | ------------- | +| AI21 | Jamba Large 1.7 | chat | 256K | 2.00 | 8.00 | +| Amazon | Nova Premier | chat, vision | 1M | 2.50 | 12.50 | +| Amazon | Nova 2 Lite | chat, vision | 1M | 0.30 | 2.50 | +| Anthropic | Claude Sonnet 4.6 | chat, vision | 1M | 3.00 | 15.00 | +| Anthropic | Claude Haiku 4.5 | chat | 200K | 1.00 | 5.00 | +| Anthropic | Claude Opus 4.8 | chat, vision | 1M | 5.00 | 25.00 | +| Black Forest Labs | FLUX.2 [flex] | image-generation, image-edit | — | — | — | +| Black Forest Labs | FLUX.2 [max] | image-generation, image-edit | — | — | — | +| Black Forest Labs | FLUX.2 [pro] | image-generation, image-edit | — | — | — | +| Cohere | Command A | chat | 256K | 2.50 | 10.00 | +| Cohere | Command R | chat | 128K | 0.15 | 0.60 | +| DeepSeek | DeepSeek V4 Pro | chat | 1M | 0.43 | 0.87 | +| DeepSeek | DeepSeek V4 Flash | chat | 1M | 0.09 | 0.18 | +| Google | Gemini 3 Pro | chat, vision | 1M | 2.00 | 12.00 | +| Google | Gemini 3 Flash | chat, vision | 1M | 0.50 | 3.00 | +| Google | Gemma 4 31B IT | chat, vision | 262K | 0.12 | 0.35 | +| Google | Gemma 4 26B A4B IT | chat, vision | 262K | 0.06 | 0.33 | +| Google | Nano Banana (Gemini 2.5 Flash Image) | image-generation, image-edit | 33K | 0.30 | 2.50 | +| Liquid | LFM2 24B | chat | 128K | 0.03 | 0.12 | +| Meta | LLaMA 4 Maverick | chat | 1M | 0.15 | 0.60 | +| Meta | LLaMA 4 Scout | chat | 10M | 0.10 | 0.30 | +| Microsoft | Phi-4 | chat | 16K | 0.07 | 0.14 | +| MiniMax | MiniMax M3 | chat, vision | 1M | 0.30 | 1.20 | +| Mistral | Mistral Large 3 | chat | 262K | 0.50 | 1.50 | +| Mistral | Mistral Medium 3.5 | chat, vision | 262K | 1.50 | 7.50 | +| Moonshot AI | Kimi K2.6 | chat, vision | 262K | 0.68 | 3.41 | +| Moonshot AI | Kimi K2.7 Code | chat, vision | 262K | 0.61 | 3.07 | +| NVIDIA | Nemotron 3 Ultra | chat | 1M | 0.50 | 2.20 | +| NVIDIA | Nemotron 3 Super | chat | 1M | 0.09 | 0.45 | +| OpenAI | GPT-OSS 120B | chat | 131K | 0.04 | 0.18 | +| OpenAI | GPT-4o mini TTS | text-to-speech | — | — | — | +| OpenAI | GPT-5.3 Chat | chat, vision | 128K | 1.75 | 14.00 | +| OpenAI | GPT-5.5 | chat, vision | 1M | 5.00 | 30.00 | +| OpenAI | GPT-5.5 Pro | chat, vision | 1M | 30.00 | 180.00 | +| OpenAI | Whisper v1 | transcription | — | — | — | +| Perplexity | Sonar Pro | chat, vision | 200K | 3.00 | 15.00 | +| Perplexity | Sonar | chat, vision | 127K | 1.00 | 1.00 | +| Qwen | Qwen3.6 Max Preview | chat | 262K | 1.04 | 6.24 | +| Qwen | Qwen3 Coder 480B | chat | 1M | 0.22 | 1.80 | +| Qwen | Qwen3 VL 32B | chat, vision | 262K | 0.10 | 0.42 | +| Qwen | Qwen3.6 Flash | chat, vision | 1M | 0.19 | 1.13 | +| Qwen | Qwen3 Embedding 8B | embedding | — | 0.01 | 0.00 | +| Qwen | Qwen3.7 Plus | chat, vision | 1M | 0.32 | 1.28 | +| Reka | Reka Flash 3 | chat | 66K | 0.10 | 0.20 | +| Xiaomi | MiMo V2.5 Pro | chat | 1M | 0.43 | 0.87 | +| Z.AI | GLM 5.1 | chat | 203K | 0.98 | 3.08 | +| Z.AI | GLM 5 Turbo | chat | 262K | 1.20 | 4.00 | +| Z.AI | GLM 5V Turbo | chat, vision | 131K | 1.20 | 4.00 | +| xAI | Grok 4.20 | chat, vision | 2M | 1.25 | 2.50 | + + Der volle und aktuelle Katalog lebt auf [openrouter.ai/models](https://openrouter.ai/models). Jedes Modell, das OpenRouter exponiert, kannst du auf deiner Instanz hinzufügen, indem du das `models`-Array in `/providers/openrouter.json` unter `TALE_CONFIG_DIR` bearbeitest (pro Org unter dem org-first Layout). diff --git a/docs/de/self-hosted/configuration/providers.md b/docs/de/self-hosted/configuration/providers.md index 26c0cc82d5..d333135ff6 100644 --- a/docs/de/self-hosted/configuration/providers.md +++ b/docs/de/self-hosted/configuration/providers.md @@ -33,7 +33,7 @@ Die Referenz ist das Dateiformat auf Platte und die Reihenfolge der Operationen, } ``` -Die vollständige Menge der Felder lebt in [`builtin-configs/providers/`](https://github.com/tale-project/tale/tree/main/builtin-configs/providers). Der ausgelieferte Default ist eine einzige `openrouter.json`, die Chat, Vision, Embeddings, Transkription, Text-to-Speech und Bildgenerierung abdeckt — ein Key für alles. Um einen Anbieter direkt statt über OpenRouter aufzurufen, füg eine weitere Datei hinzu (z. B. eine `openai.json`, die auf `https://api.openai.com/v1` zeigt); siehe [Modelle out of the box](/de/platform/models) für den vollen Default-Katalog. +Die vollständige Menge der Felder lebt in [`builtin-configs/providers/`](https://github.com/tale-project/tale/tree/main/builtin-configs/providers). Der ausgelieferte Default ist eine einzige `openrouter.json`, die Chat, Vision, Embeddings, Transkription, Text-to-Speech und Bildgenerierung abdeckt — ein Key für alles — mit kuratierten Presets für die gängigen Anbieter (Anthropic, OpenAI, Google, xAI, Mistral, Meta, DeepSeek, Qwen, Cohere, Amazon, Perplexity und mehr). Um einen Anbieter direkt statt über OpenRouter aufzurufen, füg eine weitere Datei hinzu (z. B. eine `openai.json`, die auf `https://api.openai.com/v1` zeigt); siehe [Modelle out of the box](/de/platform/models) für den vollen Default-Katalog. `transcriptionMode` wählt, wie der Request-Body eines `transcription`-Modells geformt wird: `json-base64` (OpenRouters `input_audio`-Envelope) oder, wenn weggelassen, `multipart` — der OpenAI/Whisper-`multipart/form-data`-Upload, den auch vLLM, LocalAI und ein direkter OpenAI-Key erwarten. Setz es passend zum Transkriptions-Endpunkt, auf den du zeigst. diff --git a/docs/de/self-hosted/configuration/retention.md b/docs/de/self-hosted/configuration/retention.md index 5553f2c1ff..f86fd1f910 100644 --- a/docs/de/self-hosted/configuration/retention.md +++ b/docs/de/self-hosted/configuration/retention.md @@ -37,6 +37,8 @@ Unter dem Org-first-Layout sind Retention-Grenzen **pro Org**: editiere `retenti Der Plattform-Container beobachtet die Datei; Änderungen schlagen ein Grenzen-Update für jede bestehende Org vor. Admins sehen den Vorschlag in ihrem **Retention-Policy**-Bildschirm und wenden ihn selbst an. Der Vorschlagen-dann-anwenden-Schritt ist Absicht: Eine Untergrenze anzuziehen kürzt Historie, was eine destruktive Aktion ist, die kein Operator stillschweigend bei jeder Mandantin landen sollte. +Dieselbe `retention.json` enthält unter einem `policy`-Schlüssel auch die vom Admin gewählten Aufbewahrungsfenster (z. B. `"policy": { "auditLogEnabled": true, "auditLogRetentionDays": 730 }`). Diesen Block schreibt **Einstellungen > Governance > Retention-Policy** in der App, Admins bearbeiten ihn also normalerweise nie von Hand — aber Grenzen und Policy in einer Datei zu halten bedeutet, dass es pro Org nur eine Retention-Datei zu durchdenken gibt. + ## Der Retention-Sweep Ein geplanter Cron in `tale-convex` läuft die tatsächliche Löschung. Jede Kategorie wird unabhängig gesweept — ein langsamer Lauf einer blockiert die anderen nicht. Löschungen sind audited (jede Kategorie hat ihr eigenes `*.retention_deleted`-Event), und eine Entität in ihrem Gnaden-Fenster wiederherzustellen ist von **Papierkorb** möglich, bevor der finale Sweep läuft. diff --git a/docs/de/self-hosted/install/cli-install.md b/docs/de/self-hosted/install/cli-install.md index 0f236eefd8..78d72695a4 100644 --- a/docs/de/self-hosted/install/cli-install.md +++ b/docs/de/self-hosted/install/cli-install.md @@ -133,14 +133,16 @@ Befehle beenden mit `0` bei Erfolg, `2` bei einem Nutzungsfehler, `3` bei einer - `--stop` — laufende Projekt-Container vor dem Wiederherstellen stoppen. - `-y, --yes` — die Bestätigungsabfrage überspringen. -`tale rollback` — auf die vorherige Patch-Version zurückrollen (nur Patch-Ebene). Keine Argumente. +`tale rollback` — auf die vorherige Patch-Version zurückrollen (nur Patch-Ebene). Fragt vorher nach Bestätigung. + +- `-y, --yes` — die Bestätigungsabfrage überspringen (im nicht-interaktiven Betrieb erforderlich). ### Wartung -`tale upgrade` (Alias `tale update`) — die CLI auf die neueste Version aktualisieren und Projektdateien synchronisieren. +`tale update` — diese Tale-Instanz auf eine neue Version bewegen: zuerst das CLI-Binary aktualisieren, dann die Projektdateien synchronisieren; danach `tale deploy` ausführen, um die Container zu rollen. Die CLI gleicht sich bei jedem Befehl ohnehin an die Instanz-Version an, also brauchst du das nur, um die Version bewusst zu wechseln. -- `-v, --version ` — genau diese Version installieren (z. B. `0.9.0`) statt der neuesten; erlaubt Downgrades. -- `-f, --force` — Neudownload erzwingen und lokal geänderte Dateien überschreiben. +- `-v, --version ` — auf genau diese Version aktualisieren (z. B. `0.9.0`) statt der neuesten; erlaubt Downgrades. +- `-f, --force` — Re-Sync erzwingen und lokal geänderte Projektdateien überschreiben. - `--dry-run` — anzeigen, was sich ändern würde, ohne etwas zu ändern. `tale cleanup` — inaktive (nicht-aktuelle) Container entfernen. Keine Argumente. @@ -151,6 +153,12 @@ Befehle beenden mit `0` bei Erfolg, `2` bei einem Nutzungsfehler, `3` bei einer - `-a, --all` — auch die zustandsbehafteten Infrastruktur-Container entfernen. - `--dry-run` — den Reset vorab anzeigen, ohne Änderungen. +`tale uninstall` — das `tale`-CLI-Binary von diesem System entfernen. Fragt nach, bevor etwas gelöscht wird, und _bietet an_, zusätzlich die benutzereigene Konfiguration (`~/.tale-daemon`) zu entfernen und die Docker-Ressourcen und Dateien eines Projekts abzubauen. Ohne `--purge` bleiben ein Projekt und seine Container unangetastet — führ darin `tale reset --all` aus, um sie zu entfernen. + +- `-f, --force` — die Bestätigungsabfrage überspringen (entfernt nur das Binary; die optionalen Aufräumschritte brauchen weiterhin `--purge`). +- `--purge` — zusätzlich `~/.tale-daemon` entfernen und, für ein vom aktuellen Verzeichnis aus gefundenes Projekt, dessen Docker-Ressourcen abbauen und seine Dateien löschen. Nicht umkehrbar. +- `--dry-run` — anzeigen, was entfernt würde, ohne etwas zu entfernen. + `tale config` — CLI-Konfiguration verwalten. Mit dem Unterbefehl `show` die aufgelöste Konfiguration ausgeben. ### Erweitert diff --git a/docs/de/self-hosted/operate/backups-and-restore.md b/docs/de/self-hosted/operate/backups-and-restore.md index d695bb8134..aecdc1de92 100644 --- a/docs/de/self-hosted/operate/backups-and-restore.md +++ b/docs/de/self-hosted/operate/backups-and-restore.md @@ -58,8 +58,8 @@ tale restore tale restore 20260611-142530-deploy --stop # Den Stack auf der Version zurückbringen, die zu den Daten passt -tale upgrade --version 0.9.6 -tale deploy --all +tale update --version 0.9.6 +tale deploy --stop ``` Das Redeploy der passenden Version ist Teil des Restores, kein optionales Extra: Der Snapshot hat die Daten exakt so erfasst, wie diese Plattform-Version sie hinterlassen hat, und ein neueres Binary würde sofort wieder seine Migrationen darauf laufen lassen. Die Restore-Ausgabe druckt die exakte Version aus dem Manifest des Snapshots. diff --git a/docs/de/self-hosted/operate/release-notes/format.md b/docs/de/self-hosted/operate/release-notes/format.md index 0dd1a38f35..b0b36605e9 100644 --- a/docs/de/self-hosted/operate/release-notes/format.md +++ b/docs/de/self-hosted/operate/release-notes/format.md @@ -5,7 +5,7 @@ description: Die Form, der Tales Release-Notes folgen — das semver-Versprechen Tale liefert ein Release pro Minor-Version und Patches als Bugfix-Tags dazwischen aus. Die Release-Notes für jeden Tag folgen derselben Form, damit du eine in einer Minute scannen kannst und weißt, ob das Upgrade ein Fünf-Minuten-Bump oder ein Wartungsfenster ist. Diese Seite deckt das Format ab: das semver-Versprechen, was jeder Abschnitt garantiert, und wo du tiefer liest, wenn eine Zeile auf eine Migration zeigt. -Die Notes selbst leben auf der GitHub-Release-Seite zu jedem Tag. Das CLI bringt sie ebenfalls hoch — `tale upgrade --notes` druckt die Notes für die Version, die es gerade installieren will. +Die Notes selbst leben auf der GitHub-Release-Seite zu jedem Tag. Das CLI bringt sie ebenfalls hoch — `tale update --notes` druckt die Notes für die Version, die es gerade installieren will. ## Das semver-Versprechen @@ -30,7 +30,7 @@ Jede Release-Seite ist dieselbe geordnete Abschnitts-Liste. Leere Abschnitte wer ## Wie du ein Release scannst -Lies die Versionszeile, die Highlights und den Breaking-Changes-Abschnitt. Ist Breaking Changes leer und nennt der Security-Abschnitt keinen Fix, der dein Install berührt, ist das Upgrade die Zwei-Kommando-Sequenz aus [Upgrades](/de/self-hosted/operate/upgrades). Hat einer der beiden Abschnitte Zeilen, gehst du sie durch, bevor du `tale deploy` läufst. +Lies die Versionszeile, die Highlights und den Breaking-Changes-Abschnitt. Ist Breaking Changes leer und nennt der Security-Abschnitt keinen Fix, der dein Install berührt, ist das Upgrade die `tale update` + `tale deploy`-Sequenz aus [Upgrades](/de/self-hosted/operate/upgrades). Hat einer der beiden Abschnitte Zeilen, gehst du sie durch, bevor du `tale deploy` läufst. ```text 0.12.0 (minor) — 14.05.2026 @@ -49,7 +49,7 @@ Security Siehe: Advisory TAL-2026-007. ``` -Die Form oben ist das, was `tale upgrade --notes` druckt. Die Web-Version desselben Releases fügt auf jeder Advisory- und Migrations-Zeile Links hinzu. +Die Form oben ist das, was `tale update --notes` druckt. Die Web-Version desselben Releases fügt auf jeder Advisory- und Migrations-Zeile Links hinzu. ## Wo das hingehört diff --git a/docs/de/self-hosted/operate/upgrades.md b/docs/de/self-hosted/operate/upgrades.md index 0ec5c51a4a..ff31fbd459 100644 --- a/docs/de/self-hosted/operate/upgrades.md +++ b/docs/de/self-hosted/operate/upgrades.md @@ -1,41 +1,59 @@ --- title: Upgrades -description: Wie `tale upgrade` und `tale deploy` eine Tale-Instanz vorwärtsbewegen — das Rolling-Restart-Pattern, was vor einem Upgrade zu tun ist und die Versions-Kompatibilitäts-Story. +description: Wie `tale update` eine Tale-Instanz vorwärtsbewegt — die automatische CLI-/Instanz-Versions-Angleichung, das Rolling-Restart-Pattern, was vor einem Upgrade zu tun ist und die Versions-Kompatibilitäts-Story. --- -Upgrades auf einer self-hosted Tale-Instanz laufen durch das `tale`-CLI in zwei Schritten: `tale upgrade`, um das Binary selbst auf die neue Version zu bewegen, dann `tale deploy`, um die Plattform-Container passend zu rollen. Der Deploy nutzt ein Blue-Green-Pattern — die neue Farbe startet neben der alten, Healthchecks bestehen, der Traffic kippt, die alte Farbe drainet. Zero-Downtime ist der Default; macht ein Patch-Release Ärger, bringt `tale rollback` den vorherigen Patch in einem Kommando zurück, und alles Größere recovert aus dem Pre-Upgrade-Snapshot. +Upgrades auf einer self-hosted Tale-Instanz laufen durch zwei Kommandos: `tale update` bewegt das CLI-Binary auf die neue Version und synct deine Projektdateien passend dazu, dann rollt `tale deploy` die Plattform-Container. Der Deploy nutzt ein Blue-Green-Pattern — die neue Farbe startet neben der alten, Healthchecks bestehen, der Traffic kippt, die alte Farbe drainet. Zero-Downtime ist der Default; macht ein Patch-Release Ärger, bringt `tale rollback` den vorherigen Patch in einem Kommando zurück, und alles Größere recovert aus dem Pre-Upgrade-Snapshot. -Die CLI-Installation lebt in [Tale-CLI installieren](/de/self-hosted/install/cli-install). Diese Seite deckt ab, was jedes Subkommando tut und in welcher Reihenfolge sie zu laufen sind. +Was du nicht mehr tust, ist das CLI von Hand im Gleichschritt zu halten: Das CLI gleicht sich automatisch an die Instanz an (siehe unten), sodass der einzige bewusste Schritt die Wahl ist, wann du mit `tale update` die Version wechselst. + +Die CLI-Installation lebt in [Tale-CLI installieren](/de/self-hosted/install/cli-install). Diese Seite deckt ab, was jedes Kommando tut und wie das Versions-Modell funktioniert. + +## Das CLI verfolgt die Instanz automatisch + +Das CLI-Binary hat immer dieselbe Version wie die Instanz, die es verwaltet. Der Workspace zeichnet diese Version in `tale.json` auf; bei jedem Kommando vergleicht das CLI seine eigene Version dagegen und aktualisiert sich selbst — auf- oder abwärts —, falls sie sich unterscheiden, bevor es läuft. Stimmen sie schon überein — der ganz überwiegend häufige Fall —, ist das ein No-op ohne Netzwerk-Aufruf, sodass du nie etwas davon merkst. + +Das heißt, du läufst `tale update` selten, außer wenn du bewusst auf eine neue Version willst. Ein Teamkollege, der ein neueres CLI als deine Instanz installiert hat, oder einen älteren Snapshot wiederhergestellt hat, bekommt beim nächsten Kommando automatisch die richtige CLI-Version. Es gibt kein Flag, das abzuschalten — Tool und Instanz im Gleichschritt zu halten ist das, was Deploys sicher macht. ## Bevor du upgradest Zwei Dinge sind es wert, zuerst zu bestätigen: -- Deine Off-Host-Kopie des `backups`-Volumes ist aktuell — siehe [Backups und Restore](/de/self-hosted/operate/backups-and-restore). `tale deploy` snapshotet die Daten-Volumes automatisch vor jedem Schritt, der Daten migrieren kann, aber der Snapshot lebt auf demselben Host; die Off-Host-Kopie ist das, was eine tote Platte überlebt. +- Deine Off-Host-Kopie des `backups`-Volumes ist aktuell — siehe [Backups und Restore](/de/self-hosted/operate/backups-and-restore). `tale update` snapshotet die Daten-Volumes automatisch vor jedem Schritt, der Daten migrieren kann, aber der Snapshot lebt auf demselben Host; die Off-Host-Kopie ist das, was eine tote Platte überlebt. - Die Release-Notes für die Zielversion nennen keinen breaking Change. Die Notes sind von der GitHub-Release-Seite verlinkt; breaking Changes sind oben als solche markiert. Überschreitet das Upgrade eine Major-Version (1.x → 2.x), lies die Migrations-Notes End-to-End, bevor du anfängst. Major-Versionen sind, wo Schema-Migrationen und Config-Datei-Format-Änderungen landen. ## Die zwei Kommandos -`tale upgrade` aktualisiert das CLI-Binary selbst. Die deployte Plattform-Version stimmt mit der Version des CLI überein — diese Kopplung ist Absicht, damit das CLI, das du ausführst, nicht eine Version deployen kann, die es nicht kennt. +`tale update` aktualisiert das CLI-Binary und synct dann deine Projektdateien auf die Templates dieser Version. Es fasst die laufenden Container **nicht** an — das ist der Job von `tale deploy`. Scheitert der Datei-Sync, rollt das CLI sein eigenes Binary auf die Version zurück, auf der dein Workspace war, sodass Binary und `tale.json` nie auseinanderdriften. ```bash -# Bewege das CLI auf das letzte Release -tale upgrade +# Bewege das CLI und die Projektdateien auf das letzte Release +tale update -# Dann rolle die Plattform passend -tale deploy +# Eine bestimmte Version festnageln (erlaubt Downgrades — siehe Zurückrollen) +tale update --version 0.10.2 + +# Versions-Wechsel und Datei-Sync vorab ansehen, ohne etwas anzufassen +tale update --dry-run ``` -`tale deploy` macht den eigentlichen Rolling-Restart: Es zieht neue Images, startet die neue Blue- oder Green-Farbe neben der laufenden, wartet auf Healthchecks, kippt den Proxy, drainet und entfernt die alte Farbe. Der Default zielt auf die rotierbaren Services (`platform`, `rag`, `crawler`); stateful Services (`db`, `proxy`) brauchen `--all`, um in-place aktualisiert zu werden. +`tale deploy` macht den eigentlichen Rolling-Restart und deployt immer die eigene Version des CLI — die dank der Angleichung die Version ist, die dein Workspace aufzeichnet. Es sortiert die Services in drei Tiers: + +- **App-/Compute-Tier** — `platform`, `sandbox`, `sandbox-egress` — rollt bei **jedem** Deploy ohne Downtime (Blue-Green: die neue Farbe startet neben der alten, Healthchecks bestehen, der Traffic kippt, die alte Farbe drainet). +- **Backend** — `convex` — rollt ebenfalls bei jedem Deploy, sodass es nie gegenüber `platform` versions-skewt; in-place neu erstellt wird es nur, wenn sich sein Image tatsächlich geändert hat. +- **Stop-gegateter Tier** — `db`, `proxy` — bleibt standardmäßig **laufend und unangetastet** (Postgres oder den Proxy neu zu erstellen ist eine kurze Ausfallzeit, die du bei einem Routine-Roll nicht willst). Mit `--stop` aktualisierst du sie; der Deploy warnt und nennt sie, wenn er sie überspringt. ```bash -# Schliess die stateful Services ein -tale deploy --all +# Nach tale update die Container passend rollen (App-Tier + convex) +tale deploy + +# Auch db/proxy aktualisieren (kurze Downtime, während sie neu erstellt werden) +tale deploy --stop -# Rolle nur bestimmte Services -tale deploy --services platform,rag +# Nur bestimmte Services rollen +tale deploy --services platform # Vorschau ohne Änderungen tale deploy --dry-run @@ -45,7 +63,7 @@ tale deploy --dry-run ## Das Blue-Green-Pattern -Eine laufende Instanz ist zu jeder Zeit eine der zwei Farben (Blue oder Green). `tale deploy` bringt die andere Farbe hoch, wartet, bis sie Healthchecks besteht, und kippt dann Caddys Upstream auf die neue Farbe. Die alte Farbe drainet ihre in-flight-Anfragen (Default 30 s), dann beendet sie sich. +Eine laufende Instanz ist zu jeder Zeit eine der zwei Farben (Blue oder Green). Die Deploy-Phase bringt die andere Farbe hoch, wartet, bis sie Healthchecks besteht, und kippt dann Caddys Upstream auf die neue Farbe. Die alte Farbe drainet ihre in-flight-Anfragen (Default 30 s), dann beendet sie sich. Drei Garantien, die das Pattern dir gibt: @@ -53,16 +71,21 @@ Drei Garantien, die das Pattern dir gibt: - **Patch-Rollback ist ein Kommando.** `tale rollback` deployt das vorherige Patch-Release auf der inaktiven Farbe neu und kippt den Traffic zurück. Minor- und Major-Downgrades verweigert es — die können die Datenbank vor dem Binary zurücklassen, und ihr Recovery-Pfad ist ein Snapshot-Restore. - **Gescheiterte Healthchecks blockieren den Kipp.** Besteht die neue Farbe nicht innerhalb des Timeouts, bricht der Deploy ab und die alte Farbe serviert weiter. -Die vollständige Deploy-Prozedur inklusive der Cleanup-Phase lebt in `tale --help`; das operatorseitige Rezept ist `tale deploy && tale status` und visuelle Bestätigung im Browser. +Die vollständige Deploy-Prozedur inklusive der Cleanup-Phase lebt in `tale --help`; das operatorseitige Rezept ist `tale update && tale deploy && tale status` und visuelle Bestätigung im Browser. ## Zurückrollen ```bash -# Zurück zur vorherigen Patch-Version +# Zurück zur vorherigen Patch-Version (fragt nach Bestätigung) tale rollback + +# Die Abfrage im nicht-interaktiven Betrieb überspringen +tale rollback --yes ``` -`tale rollback` ist auf Patch-Schritte begrenzt: Es zielt nur auf die aufgezeichnete vorherige Version und verweigert, wenn diese Version nicht `major.minor` mit der laufenden Plattform teilt. Patch-Releases tragen nie Migrationen, also ist das Redeploy des vorherigen Patches immer sicher. Alles Größere kann Daten vorwärts migriert haben — ein älteres Binary auf migrierten Daten zu deployen korrumpiert die Instanz, statt sie zu retten. Für diese Fälle ist der Recovery-Pfad, den Pre-Upgrade-Snapshot wiederherzustellen und die passende Version neu zu deployen; die Verweigerungs-Meldung druckt die exakten Kommandos, und der volle Walk lebt in [Backups und Restore](/de/self-hosted/operate/backups-and-restore). +`tale rollback` ist auf Patch-Schritte begrenzt: Es zielt nur auf die aufgezeichnete vorherige Version und verweigert, wenn diese Version nicht `major.minor` mit der laufenden Plattform teilt. Patch-Releases tragen nie Migrationen, also ist das Redeploy des vorherigen Patches immer sicher. Alles Größere kann Daten vorwärts migriert haben — ein älteres Binary auf migrierten Daten zu deployen korrumpiert die Instanz, statt sie zu retten. Für diese Fälle ist der Recovery-Pfad, den Pre-Upgrade-Snapshot wiederherzustellen und mit `tale update --version ` gefolgt von `tale deploy --stop` (sodass `db`/`proxy` ebenfalls zurückrollen) auf die passende Version zurückzugehen; die Verweigerungs-Meldung druckt die exakten Kommandos, und der volle Walk lebt in [Backups und Restore](/de/self-hosted/operate/backups-and-restore). + +Weil das Zurückrollen die laufenden Container abräumt, warnt das Kommando, was es vorhat, und fragt nach Bestätigung, bevor es auch nur ein Image zieht; mit `--yes` überspringst du diese Abfrage in Skripten oder CI. ## Versions-Kompatibilität @@ -74,6 +97,8 @@ Tale-Versionen sind semver. Die Kompatibilitäts-Regeln: Minor-Versionen zu überspringen (von 0.9 auf 0.11 zu gehen) ist unterstützt, solange die Zwischen-Migrationen noch im Binary sind; die Release-Notes nennen es, wenn das nicht der Fall ist. +Um bewusst eine Version _runter_ zu gehen — etwa wenn ein Minor-Release Ärger macht und du seine Migrationen schon zurückgenommen hast —, nagle das Ziel mit `tale update --version ` fest. Das Kommando warnt, wenn das Ziel älter als die laufende Version ist, und erinnert dich, zuerst die Daten-Migrationen zurückzunehmen. + ## Wo das hingehört Der Upgrade-Flow knüpft jede andere Operate-Seite an — Backups sind das, was ein gescheitertes Upgrade wiederherstellbar macht, Observability ist das, was dir sagt, dass die neue Farbe healthy ist, Hardening ist das, was du nach einer Major-Version neu durchgehst. Setzt du das CLI zum ersten Mal auf, deckt [Tale-CLI installieren](/de/self-hosted/install/cli-install) das workstationseitige Setup ab; nimmst du den Pager mitten im Rollout auf, nennt [Troubleshooting](/de/self-hosted/operate/observability/troubleshooting) die Symptome. diff --git a/docs/en/cloud/onboarding.md b/docs/en/cloud/onboarding.md index 03d71b45d9..0088fde910 100644 --- a/docs/en/cloud/onboarding.md +++ b/docs/en/cloud/onboarding.md @@ -41,7 +41,7 @@ For a deeper walk on what makes an agent good, see [Create an agent](/platform/a ## Step 5 — Open chat -Open **Chat** in the sidebar and click **New chat**. Pick the agent from the picker, type a question the agent's domain covers, send. The reply streams back; if it lands the way you wrote the instructions to land, the org is done with onboarding. +Click **New chat** in the sidebar. Pick the agent from the picker, type a question the agent's domain covers, send. The reply streams back; if it lands the way you wrote the instructions to land, the org is done with onboarding. Three follow-ups worth doing now while everything is fresh: diff --git a/docs/en/develop/ai-assisted-development.md b/docs/en/develop/ai-assisted-development.md index 956b9d0a7c..00ffdbe3f2 100644 --- a/docs/en/develop/ai-assisted-development.md +++ b/docs/en/develop/ai-assisted-development.md @@ -34,7 +34,7 @@ The directive matters because every editor under load skips schema reads unless | `integrations//config.json` | Integration manifest — operations, auth method, allowed hosts. | | `integrations//connector.ts` | Optional TypeScript connector for REST shapes the manifest can't cover. | | `branding/branding.json` | Org branding — colours, logos, email senders. | -| `.tale/reference/` | Read-only schema mirror; regenerated by `tale init` and `tale upgrade`. | +| `.tale/reference/` | Read-only schema mirror; regenerated by `tale init` and `tale update`. | The reference tree is bytes-identical to the schemas the platform validates against at deploy time. Treat it as canonical: when a field name in a hand-written config disagrees with the reference, the reference wins. @@ -46,7 +46,7 @@ The rules file names three rules each editor enforces while editing: - **Workflows use integration operations.** A workflow step references integration operations declared in `integrations//config.json`. Editing a step against an operation that does not exist will fail validation. - **Naming is enforced.** Agent filenames match `[a-z0-9][a-z0-9_-]*\.json`. Workflow step slugs match `[a-z0-9][a-z0-9_-]*`. Integration directories are lowercase alphanumeric with hyphens or underscores. -When the editor proposes a change, ask it to cite the file in `.tale/reference/` it relied on. If it cannot, regenerate the mirror with `tale upgrade` and try again. +When the editor proposes a change, ask it to cite the file in `.tale/reference/` it relied on. If it cannot, regenerate the mirror with `tale update` and try again. ## Where this fits diff --git a/docs/en/platform/agents/external-agent.md b/docs/en/platform/agents/external-agent.md index a03c471a5a..df9aaabb11 100644 --- a/docs/en/platform/agents/external-agent.md +++ b/docs/en/platform/agents/external-agent.md @@ -29,7 +29,7 @@ How the agent reaches its model is a per-agent choice, set on the agent's **Inst **Managed (platform gateway)** is the default and what every section above describes. The platform mints a short-lived virtual key for the turn, routes the agent through its gateway, enforces the agent's allowed models, meters usage, and applies the org's spend caps. The sandbox never holds a real provider key. -**Bring your own credentials (BYO)** takes the platform out of the request path. No virtual key is minted; the agent authenticates with credentials you store under [Environment & secrets](/platform/member/environment) and reaches the provider directly. The model becomes a raw provider id you type verbatim rather than a catalogue entry, and the agent's native web search and fetch are left enabled rather than disabled. Because the gateway is bypassed, the org's model allowlist, spend caps, and usage metering do not apply to BYO turns — billing and limits move to your own provider account. Switching an agent from managed to BYO clears its saved platform models, since catalogue references mean nothing to a raw passthrough; you re-enter the raw ids. +**Bring your own credentials (BYO)** takes the platform out of the request path. No virtual key is minted; the agent authenticates with credentials you store under [Environment variables & secrets](/platform/member/environment) and reaches the provider directly. The model becomes a raw provider id you type verbatim rather than a catalogue entry, and the agent's native web search and fetch are left enabled rather than disabled. Because the gateway is bypassed, the org's model allowlist, spend caps, and usage metering do not apply to BYO turns — billing and limits move to your own provider account. Switching an agent from managed to BYO clears its saved platform models, since catalogue references mean nothing to a raw passthrough; you re-enter the raw ids. That is also a shift in the trust boundary. In managed mode the sandbox holds only a budget-scoped gateway key; in BYO mode your real provider credential is injected into the sandbox environment — the same posture as the in-sandbox GitHub token — so any code the agent runs in the box can read it. That is by design: it is your box and your credential. Configuring an agent is already a privileged action, so the per-agent toggle is the only control; there is no separate org-level switch. @@ -47,4 +47,4 @@ This accounting is a property of the gateway, so it covers managed turns only. A ## Where this fits -An external agent turns a chat thread into a live session with a coding tool in a sandbox — you drive it in plain language, it works in an isolated workspace, and the session persists for follow-ups until you close the thread. Credentials are the axis that decides how much of that runs under the org's control: a managed agent stays on the platform gateway under the org's caps and metering, while a bring-your-own agent runs on the keys you keep under [Environment & secrets](/platform/member/environment) and answers to your own provider account. The drift candidates here are the agent and model names; pair this page with the running [Providers](/platform/admin/providers) list rather than memorising specific model strings, and with [Integrations](/platform/integrations/overview) for the connected integrations the agent can reach — from GitHub for a real pull-request workflow to a search or data integration that pulls outside facts into the work. +An external agent turns a chat thread into a live session with a coding tool in a sandbox — you drive it in plain language, it works in an isolated workspace, and the session persists for follow-ups until you close the thread. Credentials are the axis that decides how much of that runs under the org's control: a managed agent stays on the platform gateway under the org's caps and metering, while a bring-your-own agent runs on the keys you keep under [Environment variables & secrets](/platform/member/environment) and answers to your own provider account. The drift candidates here are the agent and model names; pair this page with the running [Providers](/platform/admin/providers) list rather than memorising specific model strings, and with [Integrations](/platform/integrations/overview) for the connected integrations the agent can reach — from GitHub for a real pull-request workflow to a search or data integration that pulls outside facts into the work. diff --git a/docs/en/platform/member/environment.md b/docs/en/platform/member/environment.md index b3fa1df81b..2a28b413f8 100644 --- a/docs/en/platform/member/environment.md +++ b/docs/en/platform/member/environment.md @@ -1,15 +1,15 @@ --- -title: Environment & secrets +title: Environment variables & secrets description: Your personal environment variables and secrets, injected into every agent sandbox you run in an organisation — most often the provider credential a bring-your-own agent authenticates with. --- -Environment & secrets is your personal store of variables that Tale injects into every agent sandbox you run in this organisation. When an external agent starts its sandbox, each entry you have saved here is set in the container's environment before the agent runs, so a command the agent issues — or the agent itself — can read it. The headline use is credentials: a [bring-your-own external agent](/platform/agents/external-agent) authenticates with the API key or token you keep here instead of the platform gateway. It is a member-level page that every role can reach, and the entries are scoped to you and to the current organisation, so they never leak to teammates and never follow you into another org. +Environment variables & secrets is your personal store of variables that Tale injects into every agent sandbox you run in this organisation. When an external agent starts its sandbox, each entry you have saved here is set in the container's environment before the agent runs, so a command the agent issues — or the agent itself — can read it. The headline use is credentials: a [bring-your-own external agent](/platform/agents/external-agent) authenticates with the API key or token you keep here instead of the platform gateway. It is a member-level page that every role can reach, and the entries are scoped to you and to the current organisation, so they never leak to teammates and never follow you into another org. This page covers the two kinds of entry, how secrets are protected, the rules a name and value have to satisfy, and where the values end up. ## Variables and secrets -Open **Settings > Environment**. The page is an add form at the top and the list of what you have saved below. Each entry is a **Name** and a **Value**, plus a **Secret** switch that decides how the value is stored and shown. +Open **Settings > Environment**. **Add variable** opens a dialog for a new entry, with the list of what you have saved below. Each entry is a **Name** and a **Value**, plus a **Secret** switch that decides how the value is stored and shown. A plain variable is stored as-is and shown back in full in the list — use it for non-sensitive configuration the agent expects, a region name or an endpoint. A **secret** is encrypted the moment you save it and is write-only from then on: the list shows `••••••••` in place of the value, and there is no way to read it back. Turn the switch on for anything sensitive — an API key, an OAuth token, a password. The trade-off is that you cannot review a secret's value later, so if you are unsure it is right, delete it and add it again rather than hunting for a reveal button that does not exist. @@ -29,4 +29,4 @@ That last step is the boundary worth understanding: the values land inside your ## Where this fits -Environment & secrets is the one member-level page that reaches into the sandbox rather than the chat — it is how your own keys and configuration get to the agents you run, without an Editor or Admin setting them for you. The entry you will add most often is the provider credential for a [bring-your-own external agent](/platform/agents/external-agent); read this page alongside that one to see both halves — where the credential is stored and how an agent is told to use it instead of the platform gateway. For the rest of your personal settings — display name, password, custom instructions — see [Preferences](/platform/member/preferences). +Environment variables & secrets is the one member-level page that reaches into the sandbox rather than the chat — it is how your own keys and configuration get to the agents you run, without an Editor or Admin setting them for you. The entry you will add most often is the provider credential for a [bring-your-own external agent](/platform/agents/external-agent); read this page alongside that one to see both halves — where the credential is stored and how an agent is told to use it instead of the platform gateway. For the rest of your personal settings — display name, password, custom instructions — see [Preferences](/platform/member/preferences). diff --git a/docs/en/platform/member/overview.md b/docs/en/platform/member/overview.md index 104372fb47..19392fa91b 100644 --- a/docs/en/platform/member/overview.md +++ b/docs/en/platform/member/overview.md @@ -16,7 +16,7 @@ The Member surface is intentionally narrow. The four buckets are: - **Conversations** — read the inbox threads assigned to you. Members reply when an agent hands a conversation back; they cannot reassign or close threads other people own. - **Approvals** — read the approval cards routed to you. Click Approve, Reject, or Request changes; leave a comment if the rule asks for one. -The org configuration settings — providers, integrations, agents, governance — are hidden for Members; the work surface is the bulk of what is left. The exception is a small personal settings group every role carries: Account, Personalization, and [Environment & secrets](/platform/member/environment), the keys and variables injected into the sandboxes you run. +The org configuration settings — providers, integrations, agents, governance — are hidden for Members; the work surface is the bulk of what is left. The exception is a small personal settings group every role carries: Account, Personalization, and [Environment variables & secrets](/platform/member/environment), the keys and variables injected into the sandboxes you run. ## Pages in this section diff --git a/docs/en/platform/member/preferences.md b/docs/en/platform/member/preferences.md index 5e79275b20..1362a9c417 100644 --- a/docs/en/platform/member/preferences.md +++ b/docs/en/platform/member/preferences.md @@ -37,4 +37,4 @@ The **Log out** row at the bottom of the profile menu confirms with a dialog bef ## Where this fits -Preferences are the line between you and the rest of the org. The org Admin sets defaults — including whether personalization is on for new members, what the password policy is, which models are allowed — and your preferences override the defaults where Tale lets them. One personal page sits apart from this set: [Environment & secrets](/platform/member/environment) holds variables and credentials scoped to you within a single organisation rather than following you across them — the place to keep the provider key a bring-your-own agent uses. The next read worth queuing is [Member overview](/platform/member/overview) for the map of the rest of the Member surface, or [Install as app](/platform/member/install-as-app) if you want Tale to live in your dock rather than your browser tabs. +Preferences are the line between you and the rest of the org. The org Admin sets defaults — including whether personalization is on for new members, what the password policy is, which models are allowed — and your preferences override the defaults where Tale lets them. One personal page sits apart from this set: [Environment variables & secrets](/platform/member/environment) holds variables and credentials scoped to you within a single organisation rather than following you across them — the place to keep the provider key a bring-your-own agent uses. The next read worth queuing is [Member overview](/platform/member/overview) for the map of the rest of the Member surface, or [Install as app](/platform/member/install-as-app) if you want Tale to live in your dock rather than your browser tabs. diff --git a/docs/en/platform/models.md b/docs/en/platform/models.md index 829f800bd7..61d9ee5684 100644 --- a/docs/en/platform/models.md +++ b/docs/en/platform/models.md @@ -17,21 +17,65 @@ OpenRouter is an OpenAI-compatible endpoint Tale calls over HTTPS with a bearer ## OpenRouter — chat, vision, embeddings -OpenRouter is a multi-model gateway. The shipped config picks `deepseek-v4-flash` as the default chat model, `qwen3-vl-32b-instruct` for vision, and `qwen3-embedding-8b` for embeddings — all picked for the speed-to-quality ratio at the moment of writing. The full ship list: - -- **Anthropic** — Claude Opus 4.6, Claude Sonnet 4.6, Claude Haiku 4.5. -- **OpenAI** — GPT-5.2 Pro, GPT-5.2, GPT-5.2 Instant, GPT-OSS 120B (the open-weight release). -- **Google** — Gemini 3 Pro, Gemini 3 Flash, Gemma 4 31B IT, Gemma 4 26B A4B IT, Nano Banana (Gemini 2.5 Flash Image). -- **DeepSeek** — DeepSeek V4 Pro, DeepSeek V4 Flash. -- **Moonshot AI** — Kimi K2.6, Kimi K2.5. -- **MiniMax** — MiniMax M2.7. -- **NVIDIA** — Nemotron 3 Super 120B. -- **Qwen** — Qwen3.6 Max Preview, Qwen3.6 Plus, Qwen3.6 Flash, Qwen3.6 35B A3B, Qwen3.5 397B A17B, Qwen3 Coder 480B, Qwen3 235B A22B, Qwen3 VL 32B, Qwen3 Embedding 8B. -- **Z.AI** — GLM 5.1, GLM 5 Turbo, GLM 5V Turbo. -- **Mistral** — Mistral Large 3, Mistral Medium 3. -- **Xiaomi** — MiMo V2.5 Pro. -- **Meta** — LLaMA 4 Maverick, LLaMA 4 Scout. -- **Black Forest Labs** — FLUX.2 [max], FLUX.2 [pro], FLUX.2 [flex]. +OpenRouter is a multi-model gateway. The shipped config picks `deepseek-v4-flash` as the default chat model, `qwen3-vl-32b-instruct` for vision, and `qwen3-embedding-8b` for embeddings — all picked for the speed-to-quality ratio at the moment of writing. The full ship list below is every visible model in the shipped `openrouter.json`, regenerated by the weekly catalog job so it never drifts from the config: + + + + + +| Provider | Model | Capabilities | Context | Input ($/M) | Output ($/M) | +| ----------------- | ------------------------------------ | ---------------------------- | ------- | ----------- | ------------ | +| AI21 | Jamba Large 1.7 | chat | 256K | 2.00 | 8.00 | +| Amazon | Nova Premier | chat, vision | 1M | 2.50 | 12.50 | +| Amazon | Nova 2 Lite | chat, vision | 1M | 0.30 | 2.50 | +| Anthropic | Claude Sonnet 4.6 | chat, vision | 1M | 3.00 | 15.00 | +| Anthropic | Claude Haiku 4.5 | chat | 200K | 1.00 | 5.00 | +| Anthropic | Claude Opus 4.8 | chat, vision | 1M | 5.00 | 25.00 | +| Black Forest Labs | FLUX.2 [flex] | image-generation, image-edit | — | — | — | +| Black Forest Labs | FLUX.2 [max] | image-generation, image-edit | — | — | — | +| Black Forest Labs | FLUX.2 [pro] | image-generation, image-edit | — | — | — | +| Cohere | Command A | chat | 256K | 2.50 | 10.00 | +| Cohere | Command R | chat | 128K | 0.15 | 0.60 | +| DeepSeek | DeepSeek V4 Pro | chat | 1M | 0.43 | 0.87 | +| DeepSeek | DeepSeek V4 Flash | chat | 1M | 0.09 | 0.18 | +| Google | Gemini 3 Pro | chat, vision | 1M | 2.00 | 12.00 | +| Google | Gemini 3 Flash | chat, vision | 1M | 0.50 | 3.00 | +| Google | Gemma 4 31B IT | chat, vision | 262K | 0.12 | 0.35 | +| Google | Gemma 4 26B A4B IT | chat, vision | 262K | 0.06 | 0.33 | +| Google | Nano Banana (Gemini 2.5 Flash Image) | image-generation, image-edit | 33K | 0.30 | 2.50 | +| Liquid | LFM2 24B | chat | 128K | 0.03 | 0.12 | +| Meta | LLaMA 4 Maverick | chat | 1M | 0.15 | 0.60 | +| Meta | LLaMA 4 Scout | chat | 10M | 0.10 | 0.30 | +| Microsoft | Phi-4 | chat | 16K | 0.07 | 0.14 | +| MiniMax | MiniMax M3 | chat, vision | 1M | 0.30 | 1.20 | +| Mistral | Mistral Large 3 | chat | 262K | 0.50 | 1.50 | +| Mistral | Mistral Medium 3.5 | chat, vision | 262K | 1.50 | 7.50 | +| Moonshot AI | Kimi K2.6 | chat, vision | 262K | 0.68 | 3.41 | +| Moonshot AI | Kimi K2.7 Code | chat, vision | 262K | 0.61 | 3.07 | +| NVIDIA | Nemotron 3 Ultra | chat | 1M | 0.50 | 2.20 | +| NVIDIA | Nemotron 3 Super | chat | 1M | 0.09 | 0.45 | +| OpenAI | GPT-OSS 120B | chat | 131K | 0.04 | 0.18 | +| OpenAI | GPT-4o mini TTS | text-to-speech | — | — | — | +| OpenAI | GPT-5.3 Chat | chat, vision | 128K | 1.75 | 14.00 | +| OpenAI | GPT-5.5 | chat, vision | 1M | 5.00 | 30.00 | +| OpenAI | GPT-5.5 Pro | chat, vision | 1M | 30.00 | 180.00 | +| OpenAI | Whisper v1 | transcription | — | — | — | +| Perplexity | Sonar Pro | chat, vision | 200K | 3.00 | 15.00 | +| Perplexity | Sonar | chat, vision | 127K | 1.00 | 1.00 | +| Qwen | Qwen3.6 Max Preview | chat | 262K | 1.04 | 6.24 | +| Qwen | Qwen3 Coder 480B | chat | 1M | 0.22 | 1.80 | +| Qwen | Qwen3 VL 32B | chat, vision | 262K | 0.10 | 0.42 | +| Qwen | Qwen3.6 Flash | chat, vision | 1M | 0.19 | 1.13 | +| Qwen | Qwen3 Embedding 8B | embedding | — | 0.01 | 0.00 | +| Qwen | Qwen3.7 Plus | chat, vision | 1M | 0.32 | 1.28 | +| Reka | Reka Flash 3 | chat | 66K | 0.10 | 0.20 | +| Xiaomi | MiMo V2.5 Pro | chat | 1M | 0.43 | 0.87 | +| Z.AI | GLM 5.1 | chat | 203K | 0.98 | 3.08 | +| Z.AI | GLM 5 Turbo | chat | 262K | 1.20 | 4.00 | +| Z.AI | GLM 5V Turbo | chat, vision | 131K | 1.20 | 4.00 | +| xAI | Grok 4.20 | chat, vision | 2M | 1.25 | 2.50 | + + The full and live catalogue lives at [openrouter.ai/models](https://openrouter.ai/models). Any model OpenRouter exposes can be added to your instance by editing the `models` array in `/providers/openrouter.json` under `TALE_CONFIG_DIR` (per-org under the org-first layout). diff --git a/docs/en/self-hosted/configuration/providers.md b/docs/en/self-hosted/configuration/providers.md index a7c7ad4ba6..528222c80d 100644 --- a/docs/en/self-hosted/configuration/providers.md +++ b/docs/en/self-hosted/configuration/providers.md @@ -33,7 +33,7 @@ The reference is the file format on disk and the order operations follow when ad } ``` -The full set of fields lives in [`builtin-configs/providers/`](https://github.com/tale-project/tale/tree/main/builtin-configs/providers). The shipped default is a single `openrouter.json` that covers chat, vision, embeddings, transcription, text-to-speech, and image generation — one key for everything. To call a vendor directly instead of through OpenRouter, add another file (for example an `openai.json` pointed at `https://api.openai.com/v1`); see [Models out of the box](/platform/models) for the full default catalogue. +The full set of fields lives in [`builtin-configs/providers/`](https://github.com/tale-project/tale/tree/main/builtin-configs/providers). The shipped default is a single `openrouter.json` that covers chat, vision, embeddings, transcription, text-to-speech, and image generation — one key for everything — with curated presets across the common providers (Anthropic, OpenAI, Google, xAI, Mistral, Meta, DeepSeek, Qwen, Cohere, Amazon, Perplexity, and more). To call a vendor directly instead of through OpenRouter, add another file (for example an `openai.json` pointed at `https://api.openai.com/v1`); see [Models out of the box](/platform/models) for the full default catalogue. `transcriptionMode` selects how a `transcription` model's request body is shaped: `json-base64` (OpenRouter's `input_audio` envelope) or, when omitted, `multipart` — the OpenAI/Whisper `multipart/form-data` upload that vLLM, LocalAI, and a direct OpenAI key also expect. Set it to match whichever transcription endpoint you point at. diff --git a/docs/en/self-hosted/configuration/retention.md b/docs/en/self-hosted/configuration/retention.md index 0e36299cce..01aaaddd89 100644 --- a/docs/en/self-hosted/configuration/retention.md +++ b/docs/en/self-hosted/configuration/retention.md @@ -37,6 +37,8 @@ Under the org-first layout, retention bounds are **per-org**: edit `retention.js The platform container watches the file; changes propose a bounds update for every existing org. Admins see the proposal in their **Retention policy** screen and apply it themselves. The propose-then-apply step is deliberate: tightening a floor shortens history, which is a destructive action no operator should land silently on every tenant. +The same `retention.json` also holds the admin-chosen retention windows under a `policy` key (e.g. `"policy": { "auditLogEnabled": true, "auditLogRetentionDays": 730 }`). That block is written by **Settings > Governance > Retention policy** in the app, so admins normally never edit it by hand — but keeping bounds and policy in one file means there's a single retention file per org to reason about. + ## The retention sweep A scheduled cron inside `tale-convex` runs the actual deletion. Each category is swept independently — a slow run on one does not block the others. Deletions are audited (every category has its own `*.retention_deleted` event), and restoring an entity inside its grace window is possible from **Trash** before the final sweep. diff --git a/docs/en/self-hosted/install/cli-install.md b/docs/en/self-hosted/install/cli-install.md index 837212b763..3c51ef946a 100644 --- a/docs/en/self-hosted/install/cli-install.md +++ b/docs/en/self-hosted/install/cli-install.md @@ -133,14 +133,16 @@ Commands exit `0` on success, `2` on a usage error, `3` on an unmet precondition - `--stop` — stop running project containers before restoring. - `-y, --yes` — skip the confirmation prompt. -`tale rollback` — roll back to the previous patch version (patch-level only). No arguments. +`tale rollback` — roll back to the previous patch version (patch-level only). Prompts for confirmation before it touches anything. + +- `-y, --yes` — skip the confirmation prompt (required when running non-interactively). ### Maintain -`tale upgrade` (alias `tale update`) — upgrade the CLI to the latest release and sync project files. +`tale update` — move this Tale instance to a new version: update the CLI binary, then sync project files to that version's templates. Run `tale deploy` afterwards to roll the containers. The CLI also self-aligns to the instance version on every command, so this is only needed to deliberately change versions. -- `-v, --version ` — install this exact version (e.g. `0.9.0`) instead of the latest; allows downgrades. -- `-f, --force` — force re-download and overwrite locally modified files. +- `-v, --version ` — update to this exact version (e.g. `0.9.0`) instead of the latest; allows downgrades. +- `-f, --force` — force re-sync and overwrite locally modified project files. - `--dry-run` — show what would change without modifying anything. `tale cleanup` — remove inactive (non-current colour) containers. No arguments. @@ -151,6 +153,12 @@ Commands exit `0` on success, `2` on a usage error, `3` on an unmet precondition - `-a, --all` — also remove the stateful infrastructure containers. - `--dry-run` — preview the reset without making changes. +`tale uninstall` — remove the `tale` CLI binary from this system. It prompts before deleting anything and _offers_ to also remove the per-user config (`~/.tale-daemon`) and tear down a project's Docker resources and files. Without `--purge`, a project and its containers are left intact — run `tale reset --all` inside one to remove those. + +- `-f, --force` — skip the confirmation prompt (removes the binary only; the optional cleanups still need `--purge`). +- `--purge` — also remove `~/.tale-daemon` and, for a project found from the current directory, tear down its Docker resources and delete its files. Irreversible. +- `--dry-run` — show what would be removed without removing anything. + `tale config` — manage CLI configuration. Use the `show` subcommand to print the resolved config. ### Advanced diff --git a/docs/en/self-hosted/operate/backups-and-restore.md b/docs/en/self-hosted/operate/backups-and-restore.md index 3f56af486c..59af5e724c 100644 --- a/docs/en/self-hosted/operate/backups-and-restore.md +++ b/docs/en/self-hosted/operate/backups-and-restore.md @@ -58,8 +58,8 @@ tale restore tale restore 20260611-142530-deploy --stop # Bring the stack back on the version that matches the data -tale upgrade --version 0.9.6 -tale deploy --all +tale update --version 0.9.6 +tale deploy --stop ``` The redeploy of the matching version is part of the restore, not an optional extra: the snapshot captured the data exactly as that platform version left it, and a newer binary would immediately re-run its migrations against it. The restore output prints the exact version recorded in the snapshot's manifest. diff --git a/docs/en/self-hosted/operate/release-notes/format.md b/docs/en/self-hosted/operate/release-notes/format.md index e024125fea..05543e8c17 100644 --- a/docs/en/self-hosted/operate/release-notes/format.md +++ b/docs/en/self-hosted/operate/release-notes/format.md @@ -5,7 +5,7 @@ description: The shape Tale's release notes follow — the semver promise, where Tale ships a release per minor version and patches as bug-fix tags between them. The release notes for every tag follow the same shape so you can scan one in a minute and know whether the upgrade is a five-minute bump or a maintenance window. This page covers the format: the semver promise, what each section guarantees, and where to read deeper when a row points at a migration. -The notes themselves live on the GitHub release page for each tag. The CLI also surfaces them — `tale upgrade --notes` prints the notes for the version it is about to install. +The notes themselves live on the GitHub release page for each tag. The CLI also surfaces them — `tale update --notes` prints the notes for the version it is about to install. ## The semver promise @@ -30,7 +30,7 @@ Each release page is the same ordered list of sections. Empty sections are omitt ## How to scan a release -Read the version line, the highlights, and the breaking-changes section. If breaking changes is empty and the security section does not name a fix that touches your install, the upgrade is the two-command sequence from [Upgrades](/self-hosted/operate/upgrades). If either section has rows, walk them before running `tale deploy`. +Read the version line, the highlights, and the breaking-changes section. If breaking changes is empty and the security section does not name a fix that touches your install, the upgrade is the `tale update` + `tale deploy` sequence from [Upgrades](/self-hosted/operate/upgrades). If either section has rows, walk them before running `tale deploy`. ```text 0.12.0 (minor) — 2026-05-14 @@ -49,7 +49,7 @@ Security See: advisory TAL-2026-007. ``` -The shape above is what `tale upgrade --notes` prints. The web version of the same release adds links on every advisory and migration row. +The shape above is what `tale update --notes` prints. The web version of the same release adds links on every advisory and migration row. ## Where this fits diff --git a/docs/en/self-hosted/operate/upgrades.md b/docs/en/self-hosted/operate/upgrades.md index 59eed76320..a342255fff 100644 --- a/docs/en/self-hosted/operate/upgrades.md +++ b/docs/en/self-hosted/operate/upgrades.md @@ -1,41 +1,59 @@ --- title: Upgrades -description: How `tale upgrade` and `tale deploy` move a Tale instance forward — the rolling restart pattern, what to do before an upgrade, and the version compatibility story. +description: How `tale update` moves a Tale instance forward — automatic CLI/instance version alignment, the rolling restart pattern, what to do before an upgrade, and the version compatibility story. --- -Upgrades on a self-hosted Tale instance run through the `tale` CLI in two steps: `tale upgrade` to move the binary itself to the new version, then `tale deploy` to roll the platform containers to match. The deploy uses a blue-green pattern — the new colour starts alongside the old, healthchecks pass, traffic flips, the old colour drains. Zero downtime is the default; if a patch release misbehaves, `tale rollback` returns to the previous patch in one command, and anything bigger recovers from the pre-upgrade snapshot. +Upgrades on a self-hosted Tale instance run through two commands: `tale update` moves the CLI binary to the new version and syncs your project files to match, then `tale deploy` rolls the platform containers. The deploy uses a blue-green pattern — the new colour starts alongside the old, healthchecks pass, traffic flips, the old colour drains. Zero downtime is the default; if a patch release misbehaves, `tale rollback` returns to the previous patch in one command, and anything bigger recovers from the pre-upgrade snapshot. -The CLI install lives in [Install the tale CLI](/self-hosted/install/cli-install). This page covers what each subcommand does and the order to run them in. +What you no longer do is keep the CLI in sync by hand: the CLI aligns itself to the instance automatically (see below), so the only deliberate step is choosing when to move versions with `tale update`. + +The CLI install lives in [Install the tale CLI](/self-hosted/install/cli-install). This page covers what each command does and how the version model works. + +## The CLI tracks the instance automatically + +The CLI binary is always the same version as the instance it manages. The workspace records that version in `tale.json`; on every command the CLI compares its own version against it and, if they differ, self-updates the binary to match (up or down) before running. When they already match — the overwhelmingly common case — this is a no-op with no network call, so you never notice it. + +That means you rarely run `tale update` except when you deliberately want to move to a new version. A teammate who installed a newer CLI than your instance, or restored an older snapshot, gets the right CLI version automatically on their next command. There is no flag to turn this off — keeping the tool and the instance in lockstep is what makes deploys safe. ## Before you upgrade Two things are worth confirming first: -- Your off-host copy of the `backups` volume is current — see [Backups and restore](/self-hosted/operate/backups-and-restore). `tale deploy` snapshots the data volumes automatically before any step that can migrate data, but the snapshot lives on the same host; the off-host copy is what survives a dead disk. +- Your off-host copy of the `backups` volume is current — see [Backups and restore](/self-hosted/operate/backups-and-restore). `tale update` snapshots the data volumes automatically before any step that can migrate data, but the snapshot lives on the same host; the off-host copy is what survives a dead disk. - The release notes for the target version do not name a breaking change. The notes are linked from the GitHub release page; breaking changes are flagged as such at the top. If the upgrade crosses a major version (1.x → 2.x), read the migration notes end-to-end before starting. Major versions are where schema migrations and config-file format changes land. ## The two commands -`tale upgrade` updates the CLI binary itself. The deployed platform version matches the CLI's version — that coupling is intentional, so the CLI you run cannot deploy a version it does not know about. +`tale update` updates the CLI binary and then syncs your project files to that version's templates. It does **not** touch the running containers — that is `tale deploy`'s job. If the file sync fails, the CLI rolls its own binary back to the version your workspace was on, so the binary and `tale.json` never drift apart. ```bash -# Move the CLI to the latest release -tale upgrade +# Move the CLI + project files to the latest release +tale update -# Then roll the platform to match -tale deploy +# Pin a specific version (allows downgrades — see Rolling back) +tale update --version 0.10.2 + +# Preview the version change and file sync without touching anything +tale update --dry-run ``` -`tale deploy` does the actual rolling restart: it pulls new images, starts the new blue or green colour alongside the running one, waits for healthchecks, flips the proxy, drains and removes the old colour. The default targets the rotatable services (`platform`, `rag`, `crawler`); stateful services (`db`, `proxy`) need `--all` to update in place. +`tale deploy` does the actual rolling restart, and it always deploys the CLI's own version — which, thanks to alignment, is the version your workspace records. It sorts the services into three tiers: + +- **App/compute tier** — `platform`, `sandbox`, `sandbox-egress` — rolls on **every** deploy with zero downtime (blue-green: the new colour starts alongside the old, healthchecks pass, traffic flips, the old colour drains). +- **Backend** — `convex` — rolls on every deploy too, so it never version-skews from `platform`; it recreates in place only when its image actually changed. +- **Stop-gated tier** — `db`, `proxy` — left **running and untouched** by default (recreating Postgres or the proxy is a brief outage you don't want on a routine roll). Pass `--stop` to update them; the deploy warns and names them when it skips. ```bash -# Include the stateful services -tale deploy --all +# After tale update, roll the containers to match (app tier + convex) +tale deploy + +# Also update db/proxy (brief downtime while they recreate) +tale deploy --stop # Roll only specific services -tale deploy --services platform,rag +tale deploy --services platform # Preview without changes tale deploy --dry-run @@ -45,7 +63,7 @@ tale deploy --dry-run ## The blue-green pattern -A running instance is one of the two colours (blue or green) at any given time. `tale deploy` brings up the other colour, waits for it to pass healthchecks, then flips Caddy's upstream to the new colour. The old colour drains its in-flight requests (default 30 s), then exits. +A running instance is one of the two colours (blue or green) at any given time. The deploy phase brings up the other colour, waits for it to pass healthchecks, then flips Caddy's upstream to the new colour. The old colour drains its in-flight requests (default 30 s), then exits. Three guarantees the pattern gives you: @@ -53,16 +71,21 @@ Three guarantees the pattern gives you: - **Patch rollback is one command.** `tale rollback` redeploys the previous patch release on the idle colour and flips traffic back. It refuses minor and major downgrades — those can leave the database ahead of the binary, and their recovery path is a snapshot restore. - **Failed healthchecks block the flip.** If the new colour does not pass within the timeout, the deploy aborts and the old colour continues serving. -The full deploy procedure including the cleanup phase lives in `tale --help`; the operator-facing recipe is `tale deploy && tale status` and visual confirmation in the browser. +The full deploy procedure including the cleanup phase lives in `tale --help`; the operator-facing recipe is `tale update && tale deploy && tale status` and visual confirmation in the browser. ## Rolling back ```bash -# Back to the previous patch version +# Back to the previous patch version (prompts for confirmation) tale rollback + +# Skip the prompt when running non-interactively +tale rollback --yes ``` -`tale rollback` is gated to patch-level steps: it only targets the recorded previous version, and refuses unless that version shares `major.minor` with the running platform. Patch releases never carry migrations, so redeploying the previous patch is always safe. Anything bigger may have migrated data forward — redeploying an older binary on top of migrated data corrupts the instance instead of recovering it. For those, the recovery path is restoring the pre-upgrade snapshot and redeploying the version that matches it; the refusal message prints the exact commands, and the full walk lives in [Backups and restore](/self-hosted/operate/backups-and-restore). +`tale rollback` is gated to patch-level steps: it only targets the recorded previous version, and refuses unless that version shares `major.minor` with the running platform. Patch releases never carry migrations, so redeploying the previous patch is always safe. Anything bigger may have migrated data forward — redeploying an older binary on top of migrated data corrupts the instance instead of recovering it. For those, the recovery path is restoring the pre-upgrade snapshot and moving back to the version that matches it with `tale update --version ` followed by `tale deploy --stop` (so `db`/`proxy` roll back too); the refusal message prints the exact commands, and the full walk lives in [Backups and restore](/self-hosted/operate/backups-and-restore). + +Because the rollback tears down the running containers, the command warns what it is about to do and asks for confirmation before it pulls a single image; pass `--yes` to skip that prompt in scripts or CI. ## Version compatibility @@ -74,6 +97,8 @@ Tale versions are semver. The compatibility rules: Skipping minor versions (going from 0.9 to 0.11) is supported as long as the intermediate migrations are still in the binary; the release notes call it out when this is not the case. +To move _down_ a version deliberately — say a minor release misbehaves and you have already reversed its migrations — pin the target with `tale update --version `. The command warns when the target is older than the running version and reminds you to reverse data migrations first. + ## Where this fits The upgrade flow ties together every other operate page — backups are what makes a failed upgrade recoverable, observability is what tells you the new colour is healthy, hardening is what you re-walk after a major version. If you are setting up the CLI for the first time, [Install the tale CLI](/self-hosted/install/cli-install) covers the workstation-side setup; if you are picking up the pager mid-rollout, [Troubleshooting](/self-hosted/operate/observability/troubleshooting) names the symptoms. diff --git a/docs/fr/cloud/onboarding.md b/docs/fr/cloud/onboarding.md index 0b888ab986..dd4f15d007 100644 --- a/docs/fr/cloud/onboarding.md +++ b/docs/fr/cloud/onboarding.md @@ -41,7 +41,7 @@ Pour une marche plus profonde sur ce qui fait un bon agent, voir [Créer un agen ## Étape 5 — Ouvrir le chat -Ouvre **Chat** dans la sidebar et clique **Nouveau chat**. Choisis l'agent dans le sélecteur, tape une question que le domaine de l'agent couvre, envoie. La réponse arrive en streaming ; si elle atterrit comme tu l'as écrite dans les instructions, l'organisation a fini son onboarding. +Clique sur **Nouveau chat** dans la sidebar. Choisis l'agent dans le sélecteur, tape une question que le domaine de l'agent couvre, envoie. La réponse arrive en streaming ; si elle atterrit comme tu l'as écrite dans les instructions, l'organisation a fini son onboarding. Trois suites utiles à faire maintenant pendant que tout est frais : diff --git a/docs/fr/develop/ai-assisted-development.md b/docs/fr/develop/ai-assisted-development.md index a92aea8268..391c6c7d88 100644 --- a/docs/fr/develop/ai-assisted-development.md +++ b/docs/fr/develop/ai-assisted-development.md @@ -34,7 +34,7 @@ La directive compte parce que tout éditeur sous charge saute les lectures de sc | `integrations//config.json` | Manifeste d'intégration — operations, méthode d'auth, hôtes autorisés. | | `integrations//connector.ts` | Connecteur TypeScript optionnel pour les formes REST que le manifeste ne couvre pas. | | `branding/branding.json` | Branding de l'org — couleurs, logos, expéditeurs courriel. | -| `.tale/reference/` | Miroir de schéma en lecture seule ; régénéré par `tale init` et `tale upgrade`. | +| `.tale/reference/` | Miroir de schéma en lecture seule ; régénéré par `tale init` et `tale update`. | L'arbre de référence est byte-à-byte identique aux schémas contre lesquels la plateforme valide au déploiement. Traite-le comme canonique : quand un nom de champ dans une config écrite à la main désaccorde avec la référence, la référence gagne. @@ -46,7 +46,7 @@ Le fichier de règles nomme trois règles que chaque éditeur applique pendant l - **Les workflows utilisent les operations d'intégration.** Une étape de workflow référence une operation d'intégration déclarée dans `integrations//config.json`. Éditer une étape contre une operation qui n'existe pas fait échouer la validation. - **Le nommage est imposé.** Les noms de fichier d'agent correspondent à `[a-z0-9][a-z0-9_-]*\.json`. Les slugs d'étape de workflow correspondent à `[a-z0-9][a-z0-9_-]*`. Les répertoires d'intégration sont en minuscules alphanumériques avec tirets ou soulignés. -Quand l'éditeur propose un changement, demande-lui de citer le fichier dans `.tale/reference/` sur lequel il s'est appuyé. S'il ne peut pas, régénère le miroir avec `tale upgrade` et réessaie. +Quand l'éditeur propose un changement, demande-lui de citer le fichier dans `.tale/reference/` sur lequel il s'est appuyé. S'il ne peut pas, régénère le miroir avec `tale update` et réessaie. ## Où cela s'inscrit diff --git a/docs/fr/platform/agents/external-agent.md b/docs/fr/platform/agents/external-agent.md index c26fe28490..52aa9be36f 100644 --- a/docs/fr/platform/agents/external-agent.md +++ b/docs/fr/platform/agents/external-agent.md @@ -29,7 +29,7 @@ La façon dont l'agent atteint son modèle est un choix propre à chaque agent, **Géré (passerelle de la plateforme)** est le mode par défaut, celui que décrivent toutes les sections ci-dessus. La plateforme forge une clé virtuelle éphémère pour le tour, achemine l'agent par sa passerelle, applique les modèles autorisés de l'agent, mesure l'utilisation et applique les plafonds de dépense de l'organisation. Le bac à sable ne détient jamais de vraie clé de fournisseur. -**Apporte tes propres identifiants (BYO)** retire la plateforme du chemin de la requête. Aucune clé virtuelle n'est forgée ; l'agent s'authentifie avec les identifiants que tu stockes sous [Environnement et secrets](/fr/platform/member/environment) et atteint directement le fournisseur. Le modèle devient un identifiant de fournisseur brut que tu saisis tel quel plutôt qu'une entrée de catalogue, et la recherche et la récupération web natives de l'agent restent activées au lieu d'être désactivées. Comme la passerelle est contournée, la liste de modèles autorisés, les plafonds de dépense et la mesure d'utilisation de l'organisation ne s'appliquent pas aux tours BYO — la facturation et les limites passent à ton propre compte de fournisseur. Faire passer un agent du mode géré au mode BYO efface ses modèles de plateforme enregistrés, puisque les références de catalogue ne signifient rien pour un passage direct brut ; tu ressaisis les identifiants bruts. +**Apporte tes propres identifiants (BYO)** retire la plateforme du chemin de la requête. Aucune clé virtuelle n'est forgée ; l'agent s'authentifie avec les identifiants que tu stockes sous [Variables d'environnement et secrets](/fr/platform/member/environment) et atteint directement le fournisseur. Le modèle devient un identifiant de fournisseur brut que tu saisis tel quel plutôt qu'une entrée de catalogue, et la recherche et la récupération web natives de l'agent restent activées au lieu d'être désactivées. Comme la passerelle est contournée, la liste de modèles autorisés, les plafonds de dépense et la mesure d'utilisation de l'organisation ne s'appliquent pas aux tours BYO — la facturation et les limites passent à ton propre compte de fournisseur. Faire passer un agent du mode géré au mode BYO efface ses modèles de plateforme enregistrés, puisque les références de catalogue ne signifient rien pour un passage direct brut ; tu ressaisis les identifiants bruts. C'est aussi un déplacement de la frontière de confiance. En mode géré, le bac à sable ne détient qu'une clé de passerelle limitée par un budget ; en mode BYO, ton véritable identifiant de fournisseur est injecté dans l'environnement du bac à sable — la même posture que le jeton GitHub dans le bac à sable — de sorte que tout code que l'agent exécute dans la boîte peut le lire. C'est intentionnel : c'est ta boîte et ton identifiant. Configurer un agent est déjà une action privilégiée, si bien que le commutateur par agent est le seul contrôle ; il n'y a pas de bascule distincte au niveau de l'organisation. @@ -47,4 +47,4 @@ Cette comptabilité est une propriété de la passerelle ; elle ne couvre donc q ## Où cela s'inscrit -Un agent externe transforme un fil de discussion en une session en direct avec un outil de code dans un bac à sable — tu le pilotes en langage clair, il travaille dans un espace de travail isolé, et la session persiste pour les suivis jusqu'à ce que tu fermes le fil. Les identifiants sont l'axe qui décide quelle part de tout cela s'exécute sous le contrôle de l'organisation : un agent géré reste sur la passerelle de la plateforme, sous les plafonds et la mesure de l'organisation, tandis qu'un agent qui apporte ses propres identifiants s'exécute sur les clés que tu conserves sous [Environnement et secrets](/fr/platform/member/environment) et répond à ton propre compte de fournisseur. Les candidats à la dérive ici sont les noms d'agent et de modèle ; associe cette page à la liste des [Fournisseurs](/fr/platform/admin/providers) en cours plutôt que de mémoriser des chaînes de modèle spécifiques, et à [Intégrations](/fr/platform/integrations/overview) pour les intégrations connectées que l'agent peut atteindre — de GitHub pour un véritable flux de pull request à une intégration de recherche ou de données qui amène des faits externes dans le travail. +Un agent externe transforme un fil de discussion en une session en direct avec un outil de code dans un bac à sable — tu le pilotes en langage clair, il travaille dans un espace de travail isolé, et la session persiste pour les suivis jusqu'à ce que tu fermes le fil. Les identifiants sont l'axe qui décide quelle part de tout cela s'exécute sous le contrôle de l'organisation : un agent géré reste sur la passerelle de la plateforme, sous les plafonds et la mesure de l'organisation, tandis qu'un agent qui apporte ses propres identifiants s'exécute sur les clés que tu conserves sous [Variables d'environnement et secrets](/fr/platform/member/environment) et répond à ton propre compte de fournisseur. Les candidats à la dérive ici sont les noms d'agent et de modèle ; associe cette page à la liste des [Fournisseurs](/fr/platform/admin/providers) en cours plutôt que de mémoriser des chaînes de modèle spécifiques, et à [Intégrations](/fr/platform/integrations/overview) pour les intégrations connectées que l'agent peut atteindre — de GitHub pour un véritable flux de pull request à une intégration de recherche ou de données qui amène des faits externes dans le travail. diff --git a/docs/fr/platform/member/environment.md b/docs/fr/platform/member/environment.md index 88f72869de..9bd01bdcea 100644 --- a/docs/fr/platform/member/environment.md +++ b/docs/fr/platform/member/environment.md @@ -1,15 +1,15 @@ --- -title: Environnement et secrets +title: Variables d'environnement et secrets description: Tes variables d'environnement et secrets personnels, injectés dans chaque sandbox d'agent que tu lances dans une organisation — le plus souvent l'identifiant fournisseur avec lequel un agent BYO s'authentifie. --- -Environnement et secrets est ton magasin personnel de variables que Tale injecte dans chaque sandbox d'agent que tu lances dans cette organisation. Quand un agent externe démarre sa sandbox, chaque entrée que tu as enregistrée ici est posée dans l'environnement du conteneur avant que l'agent tourne, pour qu'une commande lancée par l'agent — ou l'agent lui-même — puisse la lire. L'usage phare, ce sont les identifiants : un [agent externe en mode BYO](/fr/platform/agents/external-agent) s'authentifie avec la clé API ou le jeton que tu gardes ici plutôt qu'avec la passerelle de la plateforme. C'est une page de niveau membre que chaque rôle peut atteindre, et les entrées sont cantonnées à toi et à l'organisation actuelle, donc elles ne fuient jamais vers tes coéquipiers et ne te suivent jamais dans une autre org. +Variables d'environnement et secrets est ton magasin personnel de variables que Tale injecte dans chaque sandbox d'agent que tu lances dans cette organisation. Quand un agent externe démarre sa sandbox, chaque entrée que tu as enregistrée ici est posée dans l'environnement du conteneur avant que l'agent tourne, pour qu'une commande lancée par l'agent — ou l'agent lui-même — puisse la lire. L'usage phare, ce sont les identifiants : un [agent externe en mode BYO](/fr/platform/agents/external-agent) s'authentifie avec la clé API ou le jeton que tu gardes ici plutôt qu'avec la passerelle de la plateforme. C'est une page de niveau membre que chaque rôle peut atteindre, et les entrées sont cantonnées à toi et à l'organisation actuelle, donc elles ne fuient jamais vers tes coéquipiers et ne te suivent jamais dans une autre org. Cette page couvre les deux types d'entrée, comment les secrets sont protégés, les règles qu'un nom et une valeur doivent respecter, et où les valeurs finissent. ## Variables et secrets -Ouvre **Paramètres > Environnement**. La page est un formulaire d'ajout en haut et la liste de ce que tu as enregistré en dessous. Chaque entrée est un **Nom** et une **Valeur**, plus une bascule **Secret** qui décide comment la valeur est stockée et affichée. +Ouvre **Paramètres > Environnement**. **Ajouter une variable** ouvre une boîte de dialogue pour une nouvelle entrée, avec la liste de ce que tu as enregistré en dessous. Chaque entrée est un **Nom** et une **Valeur**, plus une bascule **Secret** qui décide comment la valeur est stockée et affichée. Une variable simple est stockée telle quelle et réaffichée en entier dans la liste — utilise-la pour la configuration non sensible que l'agent attend, un nom de région ou un endpoint. Un **secret** est chiffré dès l'instant où tu l'enregistres et devient en écriture seule à partir de là : la liste montre `••••••••` à la place de la valeur, et il n'y a aucun moyen de la relire. Active la bascule pour tout ce qui est sensible — une clé API, un jeton OAuth, un mot de passe. Le compromis, c'est que tu ne peux pas revoir la valeur d'un secret plus tard, donc si tu n'es pas sûr qu'elle soit bonne, supprime-le et ajoute-le à nouveau plutôt que de chercher un bouton d'affichage qui n'existe pas. @@ -29,4 +29,4 @@ Cette dernière étape est la frontière à comprendre : les valeurs atterrissen ## Où cela s'inscrit -Environnement et secrets est l'unique page de niveau membre qui atteint la sandbox plutôt que le chat — c'est par elle que tes propres clés et ta configuration parviennent aux agents que tu lances, sans qu'un Éditeur ou un Admin ne les pose à ta place. L'entrée que tu ajouteras le plus souvent est l'identifiant fournisseur d'un [agent externe en mode BYO](/fr/platform/agents/external-agent) ; lis cette page en parallèle de celle-là pour voir les deux moitiés — où l'identifiant est stocké et comment on dit à un agent de l'utiliser au lieu de la passerelle de la plateforme. Pour le reste de tes réglages personnels — nom d'affichage, mot de passe, instructions personnalisées — vois [Préférences](/fr/platform/member/preferences). +Variables d'environnement et secrets est l'unique page de niveau membre qui atteint la sandbox plutôt que le chat — c'est par elle que tes propres clés et ta configuration parviennent aux agents que tu lances, sans qu'un Éditeur ou un Admin ne les pose à ta place. L'entrée que tu ajouteras le plus souvent est l'identifiant fournisseur d'un [agent externe en mode BYO](/fr/platform/agents/external-agent) ; lis cette page en parallèle de celle-là pour voir les deux moitiés — où l'identifiant est stocké et comment on dit à un agent de l'utiliser au lieu de la passerelle de la plateforme. Pour le reste de tes réglages personnels — nom d'affichage, mot de passe, instructions personnalisées — vois [Préférences](/fr/platform/member/preferences). diff --git a/docs/fr/platform/member/overview.md b/docs/fr/platform/member/overview.md index 49cded6043..d31063676d 100644 --- a/docs/fr/platform/member/overview.md +++ b/docs/fr/platform/member/overview.md @@ -16,7 +16,7 @@ La surface Membre est volontairement étroite. Les quatre seaux sont : - **Conversations** — lire les threads d'inbox qui te sont assignés. Les Membres répondent quand un agent rend une conversation ; ils ne peuvent pas réassigner ou fermer des threads que d'autres possèdent. - **Approbations** — lire les cartes d'approbation routées vers toi. Clique sur Approuver, Rejeter, ou Demander des changements ; laisse un commentaire si la règle le demande. -Les réglages de configuration de l'org — Fournisseurs, Intégrations, Agents, Gouvernance — sont cachés pour les Membres ; la surface travail est l'essentiel de ce qui reste. L'exception est un petit groupe de réglages personnels que porte chaque rôle : Compte, Personnalisation et [Environnement et secrets](/fr/platform/member/environment), les clés et variables injectées dans les sandboxes que tu fais tourner. +Les réglages de configuration de l'org — Fournisseurs, Intégrations, Agents, Gouvernance — sont cachés pour les Membres ; la surface travail est l'essentiel de ce qui reste. L'exception est un petit groupe de réglages personnels que porte chaque rôle : Compte, Personnalisation et [Variables d'environnement et secrets](/fr/platform/member/environment), les clés et variables injectées dans les sandboxes que tu fais tourner. ## Pages dans cette section diff --git a/docs/fr/platform/member/preferences.md b/docs/fr/platform/member/preferences.md index aa2c77ab28..03cb11ba06 100644 --- a/docs/fr/platform/member/preferences.md +++ b/docs/fr/platform/member/preferences.md @@ -37,4 +37,4 @@ La ligne **Se déconnecter** en bas du menu de profil confirme via une boîte de ## Où cela s'inscrit -Les préférences sont la ligne entre toi et le reste de l'org. L'Administrateur de l'org pose les valeurs par défaut — y compris si la personnalisation est active pour les nouveaux membres, quelle est la politique de mot de passe, quels modèles sont autorisés — et tes préférences remplacent les valeurs par défaut là où Tale le permet. Une page personnelle se tient à l'écart de cet ensemble : [Environnement et secrets](/fr/platform/member/environment) porte des variables et des identifiants cantonnés à toi au sein d'une seule organisation plutôt qu'ils ne te suivent d'une org à l'autre — l'endroit où garder la clé de fournisseur qu'utilise un agent BYO. La lecture suivante à mettre en file est [Vue d'ensemble Membre](/fr/platform/member/overview) pour la carte du reste de la surface Membre, ou [Installer en tant qu'app](/fr/platform/member/install-as-app) si tu veux que Tale vive dans ton dock plutôt que dans tes onglets de navigateur. +Les préférences sont la ligne entre toi et le reste de l'org. L'Administrateur de l'org pose les valeurs par défaut — y compris si la personnalisation est active pour les nouveaux membres, quelle est la politique de mot de passe, quels modèles sont autorisés — et tes préférences remplacent les valeurs par défaut là où Tale le permet. Une page personnelle se tient à l'écart de cet ensemble : [Variables d'environnement et secrets](/fr/platform/member/environment) porte des variables et des identifiants cantonnés à toi au sein d'une seule organisation plutôt qu'ils ne te suivent d'une org à l'autre — l'endroit où garder la clé de fournisseur qu'utilise un agent BYO. La lecture suivante à mettre en file est [Vue d'ensemble Membre](/fr/platform/member/overview) pour la carte du reste de la surface Membre, ou [Installer en tant qu'app](/fr/platform/member/install-as-app) si tu veux que Tale vive dans ton dock plutôt que dans tes onglets de navigateur. diff --git a/docs/fr/platform/models.md b/docs/fr/platform/models.md index 9c2fcb4b1f..e53086d393 100644 --- a/docs/fr/platform/models.md +++ b/docs/fr/platform/models.md @@ -17,21 +17,65 @@ OpenRouter est un endpoint compatible OpenAI que Tale appelle en HTTPS avec un b ## OpenRouter — chat, vision, embeddings -OpenRouter est une passerelle multi-modèles. La configuration livrée choisit `deepseek-v4-flash` comme modèle de chat par défaut, `qwen3-vl-32b-instruct` pour la vision et `qwen3-embedding-8b` pour les embeddings — tous choisis pour le rapport vitesse/qualité au moment de l'écriture. La liste complète livrée : - -- **Anthropic** — Claude Opus 4.6, Claude Sonnet 4.6, Claude Haiku 4.5. -- **OpenAI** — GPT-5.2 Pro, GPT-5.2, GPT-5.2 Instant, GPT-OSS 120B (la version open-weight). -- **Google** — Gemini 3 Pro, Gemini 3 Flash, Gemma 4 31B IT, Gemma 4 26B A4B IT, Nano Banana (Gemini 2.5 Flash Image). -- **DeepSeek** — DeepSeek V4 Pro, DeepSeek V4 Flash. -- **Moonshot AI** — Kimi K2.6, Kimi K2.5. -- **MiniMax** — MiniMax M2.7. -- **NVIDIA** — Nemotron 3 Super 120B. -- **Qwen** — Qwen3.6 Max Preview, Qwen3.6 Plus, Qwen3.6 Flash, Qwen3.6 35B A3B, Qwen3.5 397B A17B, Qwen3 Coder 480B, Qwen3 235B A22B, Qwen3 VL 32B, Qwen3 Embedding 8B. -- **Z.AI** — GLM 5.1, GLM 5 Turbo, GLM 5V Turbo. -- **Mistral** — Mistral Large 3, Mistral Medium 3. -- **Xiaomi** — MiMo V2.5 Pro. -- **Meta** — LLaMA 4 Maverick, LLaMA 4 Scout. -- **Black Forest Labs** — FLUX.2 [max], FLUX.2 [pro], FLUX.2 [flex]. +OpenRouter est une passerelle multi-modèles. La configuration livrée choisit `deepseek-v4-flash` comme modèle de chat par défaut, `qwen3-vl-32b-instruct` pour la vision et `qwen3-embedding-8b` pour les embeddings — tous choisis pour le rapport vitesse/qualité au moment de l'écriture. La liste complète ci-dessous reprend chaque modèle visible de la `openrouter.json` livrée, régénérée par le job de catalogue hebdomadaire pour qu'elle ne dérive jamais de la configuration : + + + + + +| Fournisseur | Modèle | Capacités | Contexte | Entrée ($/M) | Sortie ($/M) | +| ----------------- | ------------------------------------ | ---------------------------- | -------- | ------------ | ------------ | +| AI21 | Jamba Large 1.7 | chat | 256K | 2.00 | 8.00 | +| Amazon | Nova Premier | chat, vision | 1M | 2.50 | 12.50 | +| Amazon | Nova 2 Lite | chat, vision | 1M | 0.30 | 2.50 | +| Anthropic | Claude Sonnet 4.6 | chat, vision | 1M | 3.00 | 15.00 | +| Anthropic | Claude Haiku 4.5 | chat | 200K | 1.00 | 5.00 | +| Anthropic | Claude Opus 4.8 | chat, vision | 1M | 5.00 | 25.00 | +| Black Forest Labs | FLUX.2 [flex] | image-generation, image-edit | — | — | — | +| Black Forest Labs | FLUX.2 [max] | image-generation, image-edit | — | — | — | +| Black Forest Labs | FLUX.2 [pro] | image-generation, image-edit | — | — | — | +| Cohere | Command A | chat | 256K | 2.50 | 10.00 | +| Cohere | Command R | chat | 128K | 0.15 | 0.60 | +| DeepSeek | DeepSeek V4 Pro | chat | 1M | 0.43 | 0.87 | +| DeepSeek | DeepSeek V4 Flash | chat | 1M | 0.09 | 0.18 | +| Google | Gemini 3 Pro | chat, vision | 1M | 2.00 | 12.00 | +| Google | Gemini 3 Flash | chat, vision | 1M | 0.50 | 3.00 | +| Google | Gemma 4 31B IT | chat, vision | 262K | 0.12 | 0.35 | +| Google | Gemma 4 26B A4B IT | chat, vision | 262K | 0.06 | 0.33 | +| Google | Nano Banana (Gemini 2.5 Flash Image) | image-generation, image-edit | 33K | 0.30 | 2.50 | +| Liquid | LFM2 24B | chat | 128K | 0.03 | 0.12 | +| Meta | LLaMA 4 Maverick | chat | 1M | 0.15 | 0.60 | +| Meta | LLaMA 4 Scout | chat | 10M | 0.10 | 0.30 | +| Microsoft | Phi-4 | chat | 16K | 0.07 | 0.14 | +| MiniMax | MiniMax M3 | chat, vision | 1M | 0.30 | 1.20 | +| Mistral | Mistral Large 3 | chat | 262K | 0.50 | 1.50 | +| Mistral | Mistral Medium 3.5 | chat, vision | 262K | 1.50 | 7.50 | +| Moonshot AI | Kimi K2.6 | chat, vision | 262K | 0.68 | 3.41 | +| Moonshot AI | Kimi K2.7 Code | chat, vision | 262K | 0.61 | 3.07 | +| NVIDIA | Nemotron 3 Ultra | chat | 1M | 0.50 | 2.20 | +| NVIDIA | Nemotron 3 Super | chat | 1M | 0.09 | 0.45 | +| OpenAI | GPT-OSS 120B | chat | 131K | 0.04 | 0.18 | +| OpenAI | GPT-4o mini TTS | text-to-speech | — | — | — | +| OpenAI | GPT-5.3 Chat | chat, vision | 128K | 1.75 | 14.00 | +| OpenAI | GPT-5.5 | chat, vision | 1M | 5.00 | 30.00 | +| OpenAI | GPT-5.5 Pro | chat, vision | 1M | 30.00 | 180.00 | +| OpenAI | Whisper v1 | transcription | — | — | — | +| Perplexity | Sonar Pro | chat, vision | 200K | 3.00 | 15.00 | +| Perplexity | Sonar | chat, vision | 127K | 1.00 | 1.00 | +| Qwen | Qwen3.6 Max Preview | chat | 262K | 1.04 | 6.24 | +| Qwen | Qwen3 Coder 480B | chat | 1M | 0.22 | 1.80 | +| Qwen | Qwen3 VL 32B | chat, vision | 262K | 0.10 | 0.42 | +| Qwen | Qwen3.6 Flash | chat, vision | 1M | 0.19 | 1.13 | +| Qwen | Qwen3 Embedding 8B | embedding | — | 0.01 | 0.00 | +| Qwen | Qwen3.7 Plus | chat, vision | 1M | 0.32 | 1.28 | +| Reka | Reka Flash 3 | chat | 66K | 0.10 | 0.20 | +| Xiaomi | MiMo V2.5 Pro | chat | 1M | 0.43 | 0.87 | +| Z.AI | GLM 5.1 | chat | 203K | 0.98 | 3.08 | +| Z.AI | GLM 5 Turbo | chat | 262K | 1.20 | 4.00 | +| Z.AI | GLM 5V Turbo | chat, vision | 131K | 1.20 | 4.00 | +| xAI | Grok 4.20 | chat, vision | 2M | 1.25 | 2.50 | + + Le catalogue complet et à jour vit sur [openrouter.ai/models](https://openrouter.ai/models). Tout modèle exposé par OpenRouter peut être ajouté à ton instance en éditant le tableau `models` dans `/providers/openrouter.json` sous `TALE_CONFIG_DIR` (par-org sous le layout org-first). diff --git a/docs/fr/self-hosted/configuration/providers.md b/docs/fr/self-hosted/configuration/providers.md index 35057de084..6f9db01d58 100644 --- a/docs/fr/self-hosted/configuration/providers.md +++ b/docs/fr/self-hosted/configuration/providers.md @@ -33,7 +33,7 @@ La référence est le format de fichier sur disque et l'ordre des opérations à } ``` -L'ensemble complet des champs vit dans [`builtin-configs/providers/`](https://github.com/tale-project/tale/tree/main/builtin-configs/providers). Le défaut livré est un seul `openrouter.json` qui couvre le chat, la vision, les embeddings, la transcription, la synthèse vocale et la génération d'images — une clé pour tout. Pour appeler un fournisseur directement plutôt que via OpenRouter, ajoute un autre fichier (par exemple un `openai.json` pointant vers `https://api.openai.com/v1`) ; voir [Modèles livrés en standard](/fr/platform/models) pour le catalogue complet par défaut. +L'ensemble complet des champs vit dans [`builtin-configs/providers/`](https://github.com/tale-project/tale/tree/main/builtin-configs/providers). Le défaut livré est un seul `openrouter.json` qui couvre le chat, la vision, les embeddings, la transcription, la synthèse vocale et la génération d'images — une clé pour tout — avec des presets curés pour les fournisseurs courants (Anthropic, OpenAI, Google, xAI, Mistral, Meta, DeepSeek, Qwen, Cohere, Amazon, Perplexity et plus). Pour appeler un fournisseur directement plutôt que via OpenRouter, ajoute un autre fichier (par exemple un `openai.json` pointant vers `https://api.openai.com/v1`) ; voir [Modèles livrés en standard](/fr/platform/models) pour le catalogue complet par défaut. `transcriptionMode` sélectionne la forme du corps de requête d'un modèle `transcription` : `json-base64` (l'enveloppe `input_audio` d'OpenRouter) ou, s'il est omis, `multipart` — l'upload `multipart/form-data` OpenAI/Whisper qu'attendent aussi vLLM, LocalAI et une clé OpenAI directe. Définis-le selon l'endpoint de transcription que tu vises. diff --git a/docs/fr/self-hosted/configuration/retention.md b/docs/fr/self-hosted/configuration/retention.md index a1851e1e3f..f3763d6899 100644 --- a/docs/fr/self-hosted/configuration/retention.md +++ b/docs/fr/self-hosted/configuration/retention.md @@ -37,6 +37,8 @@ Sous la disposition org-first, les bornes de rétention sont **par org** : édit Le conteneur plateforme surveille le fichier ; les changements proposent une mise à jour de bornes pour chaque org existante. Les admins voient la proposition dans leur écran **Politique de rétention** et l'appliquent eux-mêmes. L'étape propose-puis-applique est délibérée : resserrer un plancher raccourcit l'historique, ce qui est une action destructive qu'aucun opérateur ne devrait poser silencieusement sur chaque tenant. +Le même `retention.json` contient aussi, sous une clé `policy`, les fenêtres de rétention choisies par l'admin (p. ex. `"policy": { "auditLogEnabled": true, "auditLogRetentionDays": 730 }`). Ce bloc est écrit par **Paramètres > Gouvernance > Politique de rétention** dans l'app, donc les admins ne l'éditent normalement jamais à la main — mais garder les bornes et la politique dans un seul fichier signifie qu'il n'y a qu'un fichier de rétention par org à appréhender. + ## Le sweep de rétention Un cron planifié dans `tale-convex` fait la suppression réelle. Chaque catégorie est sweepée indépendamment — un run lent sur une ne bloque pas les autres. Les suppressions sont auditées (chaque catégorie a son propre événement `*.retention_deleted`), et restaurer une entité dans sa fenêtre de grâce est possible depuis **Corbeille** avant le sweep final. diff --git a/docs/fr/self-hosted/install/cli-install.md b/docs/fr/self-hosted/install/cli-install.md index 6d39aeea3e..d08ed1aa5c 100644 --- a/docs/fr/self-hosted/install/cli-install.md +++ b/docs/fr/self-hosted/install/cli-install.md @@ -133,14 +133,16 @@ Les commandes se terminent avec `0` en cas de succès, `2` pour une erreur d'uti - `--stop` — arrêter les conteneurs du projet avant la restauration. - `-y, --yes` — ignorer l'invite de confirmation. -`tale rollback` — revenir à la version patch précédente (niveau patch uniquement). Aucun argument. +`tale rollback` — revenir à la version patch précédente (niveau patch uniquement). Demande confirmation au préalable. + +- `-y, --yes` — ignorer l'invite de confirmation (requis en mode non-interactif). ### Maintenance -`tale upgrade` (alias `tale update`) — mettre à jour le CLI vers la dernière version et synchroniser les fichiers projet. +`tale update` — bouger cette instance Tale vers une nouvelle version : mettre à jour le binaire CLI, puis synchroniser les fichiers projet sur les templates de cette version. Lance `tale deploy` ensuite pour rouler les conteneurs. La CLI s'aligne aussi d'elle-même sur la version de l'instance à chaque commande, donc ceci n'est nécessaire que pour changer délibérément de version. -- `-v, --version ` — installer exactement cette version (p. ex. `0.9.0`) au lieu de la dernière ; autorise les rétrogradations. -- `-f, --force` — forcer le re-téléchargement et écraser les fichiers modifiés localement. +- `-v, --version ` — mettre à jour vers exactement cette version (p. ex. `0.9.0`) au lieu de la dernière ; autorise les rétrogradations. +- `-f, --force` — forcer la re-synchronisation et écraser les fichiers projet modifiés localement. - `--dry-run` — montrer ce qui changerait sans rien modifier. `tale cleanup` — supprimer les conteneurs inactifs (couleur non courante). Aucun argument. @@ -151,6 +153,12 @@ Les commandes se terminent avec `0` en cas de succès, `2` pour une erreur d'uti - `-a, --all` — supprimer aussi les conteneurs d'infrastructure avec état. - `--dry-run` — prévisualiser la réinitialisation sans rien modifier. +`tale uninstall` — supprimer le binaire CLI `tale` de ce système. Il demande confirmation avant de supprimer quoi que ce soit et _propose_ de retirer aussi la configuration propre à l'utilisateur (`~/.tale-daemon`) et de démanteler les ressources Docker et les fichiers d'un projet. Sans `--purge`, un projet et ses conteneurs restent intacts — lance `tale reset --all` à l'intérieur pour les supprimer. + +- `-f, --force` — ignorer l'invite de confirmation (supprime uniquement le binaire ; les nettoyages optionnels nécessitent toujours `--purge`). +- `--purge` — retirer aussi `~/.tale-daemon` et, pour un projet trouvé depuis le répertoire courant, démanteler ses ressources Docker et supprimer ses fichiers. Irréversible. +- `--dry-run` — montrer ce qui serait supprimé sans rien supprimer. + `tale config` — gérer la configuration du CLI. Utilise le sous-commande `show` pour afficher la configuration résolue. ### Avancé diff --git a/docs/fr/self-hosted/operate/backups-and-restore.md b/docs/fr/self-hosted/operate/backups-and-restore.md index 028ae41886..cb2aac76f3 100644 --- a/docs/fr/self-hosted/operate/backups-and-restore.md +++ b/docs/fr/self-hosted/operate/backups-and-restore.md @@ -58,8 +58,8 @@ tale restore tale restore 20260611-142530-deploy --stop # Remonter le stack sur la version qui correspond aux données -tale upgrade --version 0.9.6 -tale deploy --all +tale update --version 0.9.6 +tale deploy --stop ``` Le redéploiement de la version correspondante fait partie de la restauration, ce n'est pas un extra optionnel : le snapshot a capturé les données exactement comme cette version de la plateforme les a laissées, et un binaire plus récent relancerait immédiatement ses migrations dessus. La sortie de la restauration imprime la version exacte enregistrée dans le manifest du snapshot. diff --git a/docs/fr/self-hosted/operate/release-notes/format.md b/docs/fr/self-hosted/operate/release-notes/format.md index de2d44d4f1..760b61c0fc 100644 --- a/docs/fr/self-hosted/operate/release-notes/format.md +++ b/docs/fr/self-hosted/operate/release-notes/format.md @@ -5,7 +5,7 @@ description: La forme que suivent les notes de version de Tale — la promesse s Tale publie une version par minor et des patches comme tags correctifs entre elles. Les notes de version pour chaque tag suivent la même forme afin que tu puisses en scanner une en une minute et savoir si la montée de version est un bump de cinq minutes ou une fenêtre de maintenance. Cette page couvre le format : la promesse semver, ce que chaque section garantit, et où lire plus en profondeur quand une ligne pointe vers une migration. -Les notes elles-mêmes vivent sur la page de release GitHub de chaque tag. La CLI les fait aussi remonter — `tale upgrade --notes` imprime les notes de la version qu'elle est sur le point d'installer. +Les notes elles-mêmes vivent sur la page de release GitHub de chaque tag. La CLI les fait aussi remonter — `tale update --notes` imprime les notes de la version qu'elle est sur le point d'installer. ## La promesse semver @@ -30,7 +30,7 @@ Chaque page de release est la même liste ordonnée de sections. Les sections vi ## Comment scanner une release -Lis la ligne de version, les highlights et la section des changements breaking. Si la section breaking est vide et que la section sécurité ne nomme pas un fix qui touche ton install, la montée de version est la séquence à deux commandes de [Montées de version](/fr/self-hosted/operate/upgrades). Si l'une ou l'autre des sections a des lignes, parcours-les avant de lancer `tale deploy`. +Lis la ligne de version, les highlights et la section des changements breaking. Si la section breaking est vide et que la section sécurité ne nomme pas un fix qui touche ton install, la montée de version est la séquence `tale update` + `tale deploy` depuis [Montées de version](/fr/self-hosted/operate/upgrades). Si l'une ou l'autre des sections a des lignes, parcours-les avant de lancer `tale deploy`. ```text 0.12.0 (minor) — 14/05/2026 @@ -49,7 +49,7 @@ Sécurité Voir : avis TAL-2026-007. ``` -La forme ci-dessus est ce qu'imprime `tale upgrade --notes`. La version web de la même release ajoute des liens sur chaque ligne d'avis et de migration. +La forme ci-dessus est ce qu'imprime `tale update --notes`. La version web de la même release ajoute des liens sur chaque ligne d'avis et de migration. ## Où cela s'inscrit diff --git a/docs/fr/self-hosted/operate/upgrades.md b/docs/fr/self-hosted/operate/upgrades.md index 58dd05d182..3151203994 100644 --- a/docs/fr/self-hosted/operate/upgrades.md +++ b/docs/fr/self-hosted/operate/upgrades.md @@ -1,43 +1,61 @@ --- title: Montées de version -description: Comment `tale upgrade` et `tale deploy` font avancer une instance Tale — le pattern de redémarrage rolling, quoi faire avant une montée de version et l'histoire de la compatibilité de versions. +description: Comment `tale update` fait avancer une instance Tale — l'alignement automatique de version entre la CLI et l'instance, le pattern de redémarrage rolling, quoi faire avant une montée de version et l'histoire de la compatibilité de versions. --- -Les montées de version sur une instance Tale auto-hébergée passent par la CLI `tale` en deux étapes : `tale upgrade` pour bouger le binaire lui-même à la nouvelle version, puis `tale deploy` pour rouler les conteneurs plateforme pour correspondre. Le déploiement utilise un pattern blue-green — la nouvelle couleur démarre à côté de l'ancienne, les healthchecks passent, le trafic bascule, l'ancienne couleur draine. Zéro downtime est le défaut ; si une release patch se comporte mal, `tale rollback` ramène le patch précédent en une commande, et tout ce qui est plus gros se récupère depuis le snapshot pré-upgrade. +Les montées de version sur une instance Tale auto-hébergée passent par deux commandes : `tale update` bouge le binaire CLI à la nouvelle version et synchronise tes fichiers projet pour correspondre, puis `tale deploy` roule les conteneurs plateforme. Le déploiement utilise un pattern blue-green — la nouvelle couleur démarre à côté de l'ancienne, les healthchecks passent, le trafic bascule, l'ancienne couleur draine. Zéro downtime est le défaut ; si une release patch se comporte mal, `tale rollback` ramène le patch précédent en une commande, et tout ce qui est plus gros se récupère depuis le snapshot pré-upgrade. -L'installation de la CLI vit dans [Installer la CLI tale](/fr/self-hosted/install/cli-install). Cette page couvre ce que fait chaque sous-commande et l'ordre dans lequel les exécuter. +Ce que tu ne fais plus, c'est garder la CLI synchronisée à la main : la CLI s'aligne elle-même sur l'instance automatiquement (voir plus bas), donc le seul pas délibéré est de choisir quand bouger de version avec `tale update`. + +L'installation de la CLI vit dans [Installer la CLI tale](/fr/self-hosted/install/cli-install). Cette page couvre ce que fait chaque commande et comment le modèle de versions fonctionne. + +## La CLI suit l'instance automatiquement + +Le binaire CLI est toujours à la même version que l'instance qu'il gère. Le workspace enregistre cette version dans `tale.json` ; à chaque commande, la CLI compare sa propre version à celle-là et, si elles diffèrent, se met à jour pour correspondre (en montant ou en descendant) avant de tourner. Quand elles correspondent déjà — le cas largement le plus fréquent — c'est un no-op sans appel réseau, donc tu ne le remarques jamais. + +Cela veut dire que tu lances rarement `tale update`, sauf quand tu veux délibérément bouger vers une nouvelle version. Un coéquipier qui a installé une CLI plus récente que ton instance, ou restauré un snapshot plus ancien, obtient la bonne version de CLI automatiquement à sa prochaine commande. Il n'y a aucun flag pour désactiver ça — garder l'outil et l'instance au pas l'un de l'autre est ce qui rend les déploiements sûrs. ## Avant de monter de version Deux choses valent la peine d'être confirmées d'abord : -- Ta copie hors-hôte du volume `backups` est à jour — voir [Backups et restauration](/fr/self-hosted/operate/backups-and-restore). `tale deploy` snapshotte automatiquement les volumes de données avant toute étape qui peut migrer des données, mais le snapshot vit sur le même hôte ; la copie hors-hôte est ce qui survit à un disque mort. +- Ta copie hors-hôte du volume `backups` est à jour — voir [Backups et restauration](/fr/self-hosted/operate/backups-and-restore). `tale update` snapshotte automatiquement les volumes de données avant toute étape qui peut migrer des données, mais le snapshot vit sur le même hôte ; la copie hors-hôte est ce qui survit à un disque mort. - Les notes de version pour la version cible ne nomment pas un changement breaking. Les notes sont liées depuis la page de release GitHub ; les changements breaking sont flaggés comme tels en haut. Si la montée de version traverse une version majeure (1.x → 2.x), lis les notes de migration de bout en bout avant de commencer. Les versions majeures sont où atterrissent les migrations de schéma et les changements de format de fichier de config. ## Les deux commandes -`tale upgrade` met à jour le binaire CLI lui-même. La version plateforme déployée correspond à la version de la CLI — ce couplage est intentionnel, pour que la CLI que tu lances ne puisse pas déployer une version qu'elle ne connaît pas. +`tale update` met à jour le binaire CLI, puis synchronise tes fichiers projet sur les templates de cette version. Il ne **touche pas** aux conteneurs en marche — c'est le boulot de `tale deploy`. Si la synchro des fichiers échoue, la CLI fait reculer son propre binaire à la version sur laquelle ton workspace était, pour que le binaire et `tale.json` ne dérivent jamais l'un de l'autre. ```bash -# Bouge la CLI à la dernière release -tale upgrade +# Bouge la CLI et les fichiers projet à la dernière release +tale update -# Puis roule la plateforme pour correspondre -tale deploy +# Fixe une version précise (autorise les downgrades — voir Rollback) +tale update --version 0.10.2 + +# Aperçu du changement de version et de la synchro des fichiers sans rien toucher +tale update --dry-run ``` -`tale deploy` fait le redémarrage rolling réel : il pull les nouvelles images, démarre la nouvelle couleur blue ou green à côté de celle qui tourne, attend les healthchecks, bascule le proxy, draine et retire l'ancienne couleur. Le défaut cible les services rotables (`platform`, `rag`, `crawler`) ; les services stateful (`db`, `proxy`) ont besoin de `--all` pour être mis à jour en place. +`tale deploy` fait le vrai redémarrage rolling, et il déploie toujours la version propre à la CLI — qui, grâce à l'alignement, est la version qu'enregistre ton workspace. Il trie les services en trois étages : + +- **Étage app/compute** — `platform`, `sandbox`, `sandbox-egress` — roule à **chaque** déploiement, sans downtime (blue-green : la nouvelle couleur démarre à côté de l'ancienne, les healthchecks passent, le trafic bascule, l'ancienne couleur draine). +- **Backend** — `convex` — roule à chaque déploiement lui aussi, pour ne jamais dériver en version d'avec `platform` ; il ne se recrée en place que quand son image a réellement changé. +- **Étage à arrêt requis** — `db`, `proxy` — laissés **en marche et intacts** par défaut (recréer Postgres ou le proxy est une brève coupure que tu ne veux pas sur un roll de routine). Passe `--stop` pour les mettre à jour ; le déploiement prévient et les nomme quand il les saute. ```bash -# Inclus les services stateful -tale deploy --all +# Après tale update, roule les conteneurs pour correspondre (étage app + convex) +tale deploy + +# Mets aussi à jour db/proxy (brève coupure pendant qu'ils se recréent) +tale deploy --stop # Roule seulement des services spécifiques -tale deploy --services platform,rag +tale deploy --services platform -# Aperçu sans changements +# Aperçu sans changement tale deploy --dry-run ``` @@ -45,7 +63,7 @@ tale deploy --dry-run ## Le pattern blue-green -Une instance en marche est l'une des deux couleurs (blue ou green) à un instant donné. `tale deploy` monte l'autre couleur, attend qu'elle passe les healthchecks, puis bascule l'upstream de Caddy sur la nouvelle couleur. L'ancienne couleur draine ses requêtes en vol (défaut 30 s), puis sort. +Une instance en marche est l'une des deux couleurs (blue ou green) à un instant donné. La phase de déploiement monte l'autre couleur, attend qu'elle passe les healthchecks, puis bascule l'upstream de Caddy sur la nouvelle couleur. L'ancienne couleur draine ses requêtes en vol (défaut 30 s), puis sort. Trois garanties que le pattern te donne : @@ -53,16 +71,21 @@ Trois garanties que le pattern te donne : - **Le rollback de patch est une commande.** `tale rollback` redéploie la release patch précédente sur la couleur inactive et rebascule le trafic. Il refuse les downgrades minor et major — ceux-là peuvent laisser la base en avance sur le binaire, et leur chemin de récupération est une restauration de snapshot. - **Les healthchecks échoués bloquent la bascule.** Si la nouvelle couleur ne passe pas dans le timeout, le déploiement abandonne et l'ancienne couleur continue à servir. -La procédure complète de déploiement, y compris la phase de cleanup, vit dans `tale --help` ; la recette côté opérateur est `tale deploy && tale status` et confirmation visuelle dans le navigateur. +La procédure complète de déploiement, y compris la phase de cleanup, vit dans `tale --help` ; la recette côté opérateur est `tale update && tale deploy && tale status` et confirmation visuelle dans le navigateur. ## Rollback ```bash -# Retour à la version patch précédente +# Retour à la version patch précédente (demande confirmation) tale rollback + +# Ignorer l'invite en mode non-interactif +tale rollback --yes ``` -`tale rollback` est limité aux pas de patch : il ne cible que la version précédente enregistrée, et refuse si cette version ne partage pas `major.minor` avec la plateforme qui tourne. Les releases patch ne portent jamais de migrations, donc redéployer le patch précédent est toujours sûr. Tout ce qui est plus gros peut avoir migré les données vers l'avant — déployer un binaire plus vieux sur des données migrées corrompt l'instance au lieu de la sauver. Pour ces cas, le chemin de récupération est de restaurer le snapshot pré-upgrade et de redéployer la version qui correspond ; le message de refus imprime les commandes exactes, et le walk complet vit dans [Backups et restauration](/fr/self-hosted/operate/backups-and-restore). +`tale rollback` est limité aux pas de patch : il ne cible que la version précédente enregistrée, et refuse si cette version ne partage pas `major.minor` avec la plateforme qui tourne. Les releases patch ne portent jamais de migrations, donc redéployer le patch précédent est toujours sûr. Tout ce qui est plus gros peut avoir migré les données vers l'avant — déployer un binaire plus vieux sur des données migrées corrompt l'instance au lieu de la sauver. Pour ces cas, le chemin de récupération est de restaurer le snapshot pré-upgrade et de revenir à la version qui lui correspond avec `tale update --version ` suivi de `tale deploy --stop` (pour que `db`/`proxy` reculent aussi) ; le message de refus imprime les commandes exactes, et le walk complet vit dans [Backups et restauration](/fr/self-hosted/operate/backups-and-restore). + +Comme le rollback démolit les conteneurs en cours d'exécution, la commande prévient de ce qu'elle s'apprête à faire et demande confirmation avant de tirer la moindre image ; passe `--yes` pour ignorer cette invite dans les scripts ou en CI. ## Compatibilité de versions @@ -74,6 +97,8 @@ Les versions Tale sont en semver. Les règles de compatibilité : Sauter des versions mineures (passer de 0.9 à 0.11) est supporté tant que les migrations intermédiaires sont encore dans le binaire ; les notes de version le mentionnent quand ce n'est pas le cas. +Pour descendre _délibérément_ d'une version — disons qu'une release minor se comporte mal et que tu as déjà inversé ses migrations — fixe la cible avec `tale update --version `. La commande prévient quand la cible est plus ancienne que la version qui tourne et te rappelle d'inverser d'abord les migrations de données. + ## Où cela s'inscrit Le flow de montée de version noue chaque autre page d'exploitation — les backups sont ce qui rend une montée de version échouée récupérable, l'observabilité est ce qui te dit que la nouvelle couleur est saine, le durcissement est ce que tu reparcours après une version majeure. Si tu mets en place la CLI pour la première fois, [Installer la CLI tale](/fr/self-hosted/install/cli-install) couvre le setup côté workstation ; si tu prends le pager en plein rollout, [Dépannage](/fr/self-hosted/operate/observability/troubleshooting) nomme les symptômes. diff --git a/packages/shared/src/tux/lines.test.ts b/packages/shared/src/tux/lines.test.ts index 9ae8097d63..aa277d9ab5 100644 --- a/packages/shared/src/tux/lines.test.ts +++ b/packages/shared/src/tux/lines.test.ts @@ -16,6 +16,7 @@ import { questionLine, rule, sourceLine, + stepStartLine, table, warnLine, } from './lines.ts'; @@ -61,20 +62,27 @@ afterEach(() => { }); describe('line emitters (plain ASCII profile)', () => { - it('render bracketed ASCII markers with no escape codes', () => { + it('render ASCII markers (info is markerless) with no escape codes', () => { doneLine('done'); infoLine('info'); questionLine('a question'); warnLine('warn'); errorLine('err'); expect(stdout()).toContain('[ + ] done'); - expect(stdout()).toContain('[ - ] info'); + // info is neutral narration — emitted as plain text, with no marker. + expect(stdout()).toContain('info'); + expect(stdout()).not.toContain('[ - ]'); expect(stdout()).toContain('[ ? ] a question'); expect(stdout()).toContain('[ ! ] warn'); expect(stderr()).toContain('[ x ] err'); expect(stdout() + stderr()).not.toContain('\x1b'); }); + it('stepStartLine keeps the [ - ] marker (a step start pairs with its terminal)', () => { + stepStartLine('Starting X'); + expect(stdout()).toContain('[ - ] Starting X'); + }); + it('routes errors (and error source lines) to stderr, the rest to stdout', () => { doneLine('ok'); errorLine('boom'); diff --git a/packages/shared/src/tux/lines.ts b/packages/shared/src/tux/lines.ts index b42009c705..2474d2c324 100644 --- a/packages/shared/src/tux/lines.ts +++ b/packages/shared/src/tux/lines.ts @@ -36,8 +36,22 @@ export function doneLine(message: string): void { writeLine(`${marker('done')} ${message}`); } -/** Neutral information, rendered gray. Suppressed by `--quiet`. */ +/** Neutral information, rendered gray. Suppressed by `--quiet`. Markerless on + * purpose: neutral narration reads as prose, visually distinct from the marked + * `[ ✓ ]`/`[ ! ]`/`[ x ]` events around it. For a step's in-progress line, use + * {@link stepStartLine} — that one keeps a marker. */ export function infoLine(message: string): void { + if (quiet()) return; + const p = getPalette(); + writeLine(`${p.dim}${message}${p.reset}`); +} + +/** A step's in-progress line for non-interactive `runStep` (append-only mode): + * the dim `[ - ]` pending marker that pairs with the step's + * `[ ✓ ]`/`[ ! ]`/`[ x ]` terminal. Unlike {@link infoLine}, a step start is + * half of a pair — not standalone narration — so it keeps the marker. + * Suppressed by `--quiet`. */ +export function stepStartLine(message: string): void { if (quiet()) return; const p = getPalette(); writeLine(`${p.dim}${getMarkers().info} ${message}${p.reset}`); diff --git a/packages/shared/src/tux/step.ts b/packages/shared/src/tux/step.ts index 9675d8ae98..3d1c17b75c 100644 --- a/packages/shared/src/tux/step.ts +++ b/packages/shared/src/tux/step.ts @@ -19,7 +19,7 @@ import { getReporterLevel, setActiveRegion, } from './context'; -import { doneLine, errorLine, infoLine, warnLine } from './lines'; +import { doneLine, errorLine, stepStartLine, warnLine } from './lines'; /** * Throw from a `runStep` task to end the step as `[ ! ]` warn (degraded but @@ -54,7 +54,7 @@ export async function runStep( // ── Non-interactive: append-only start + result lines, no cursor escapes. ── if (!caps.interactive) { - infoLine(`${active}...`); + stepStartLine(`${active}...`); try { const result = await task(); doneLine(`${done} (${elapsed()})`); diff --git a/packages/ui/src/components/primitives/button.stories.tsx b/packages/ui/src/components/primitives/button.stories.tsx index 96445377df..2303c83308 100644 --- a/packages/ui/src/components/primitives/button.stories.tsx +++ b/packages/ui/src/components/primitives/button.stories.tsx @@ -108,7 +108,7 @@ export const AllSizes: Story = { - diff --git a/packages/ui/src/components/primitives/button.test.tsx b/packages/ui/src/components/primitives/button.test.tsx index 5e5df31c45..fb2e5efeb8 100644 --- a/packages/ui/src/components/primitives/button.test.tsx +++ b/packages/ui/src/components/primitives/button.test.tsx @@ -67,12 +67,43 @@ describe('Button', () => { it.each(['default', 'sm', 'lg', 'icon'] as const)( 'renders %s size', (size) => { - render(); + // `title` names the icon size (the type requires icon buttons to be + // labeled) and is harmless on the others. + render( + , + ); expect(screen.getByRole('button')).toBeInTheDocument(); }, ); }); + describe('title (icon-button label + tooltip)', () => { + it('uses `title` as the accessible name and drops the native title', () => { + render( + , + ); + const button = screen.getByRole('button', { name: 'Zoom in' }); + expect(button).toBeInTheDocument(); + // No duplicate native browser tooltip. + expect(button).not.toHaveAttribute('title'); + }); + + it('lets an explicit aria-label override the title for the name', () => { + render( + , + ); + expect( + screen.getByRole('button', { name: 'real label' }), + ).toBeInTheDocument(); + }); + }); + describe('interactions', () => { it('calls onClick when clicked', async () => { const handleClick = vi.fn(); diff --git a/packages/ui/src/components/primitives/button.tsx b/packages/ui/src/components/primitives/button.tsx index fe304b6fd6..e82aa5bc60 100644 --- a/packages/ui/src/components/primitives/button.tsx +++ b/packages/ui/src/components/primitives/button.tsx @@ -1,4 +1,5 @@ import { Slot } from '@radix-ui/react-slot'; +import * as TooltipPrimitive from '@radix-ui/react-tooltip'; import { Link } from '@tanstack/react-router'; import { cva, type VariantProps } from 'class-variance-authority'; import { Loader2, type LucideIcon } from 'lucide-react'; @@ -6,6 +7,7 @@ import * as React from 'react'; import { cn } from '../../lib/cn'; import { SkeletonBox } from '../feedback/skeleton'; +import { TooltipContent } from '../overlays/tooltip'; export const buttonVariants = cva( 'inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium transition-all duration-150 active:scale-[0.97] active:duration-75 motion-reduce:active:scale-100 motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100 disabled:hover:opacity-50 leading-none ring-offset-background cursor-pointer', @@ -37,10 +39,10 @@ export const buttonVariants = cva( }, ); -export interface ButtonProps +interface ButtonOwnProps extends React.ButtonHTMLAttributes, - VariantProps { + Omit, 'size'> { asChild?: boolean; isLoading?: boolean; /** Icon to display before the button text */ @@ -57,10 +59,50 @@ export interface ButtonProps * and action rows where labels would otherwise overlap on mobile. */ collapseLabel?: boolean; + /** + * One-stop label for icon buttons: a plain-string `title` populates BOTH the + * `aria-label` (accessibility) AND a hover/focus tooltip, so callers set a + * single prop instead of repeating themselves. The native `title` attribute + * is suppressed so there's no duplicate browser tooltip. An explicit + * `aria-label` or `tooltip` overrides the respective half. + */ + title?: string; + /** + * Rich tooltip content (overrides `title` for the visible tooltip only). + * Use when the tooltip needs more than the plain accessible name — e.g. a + * keyboard-shortcut badge. Rendered with the shared Tooltip primitive, and + * NEVER when `asChild` is set (the button is then a Radix `Slot`, usually + * another overlay's trigger, and wrapping a Slot in a tooltip trigger breaks + * that composition). + */ + tooltip?: React.ReactNode; + /** Side the tooltip opens on (default `top`). */ + tooltipSide?: 'top' | 'right' | 'bottom' | 'left'; } -// Plain control — the real button. No skeleton logic of its own. -const ButtonBase = React.forwardRef( +type ButtonSize = NonNullable['size']>; + +/** + * Icon-only buttons (`size="icon"`) MUST be named — there's no visible text. + * Enforced at the type level: a button must carry a `title` (which becomes both + * the label and a tooltip), OR an explicit `aria-label`, OR be a non-icon size. + * Phrasing it as "labeled OR non-icon" (rather than discriminating on `size`) + * keeps dynamic `size={cond ? 'sm' : 'icon'}` buttons valid as long as they're + * labeled — a plain `size`-discriminated union would reject those outright. + */ +export type ButtonProps = ButtonOwnProps & + ( + | ({ size?: ButtonSize | null } & { 'aria-label': string }) + | ({ size?: ButtonSize | null } & { title: string }) + | { size?: Exclude | null } + ); + +// Loose internal props — the wrapper has already resolved title → aria-label +// and stripped the tooltip props, so the base never sees them. +type ButtonBaseProps = ButtonOwnProps & { size?: ButtonSize | null }; + +// Plain control — the real button. No skeleton/tooltip logic of its own. +const ButtonBase = React.forwardRef( ( { className, @@ -137,11 +179,40 @@ ButtonBase.displayName = 'ButtonBase'; * with an overlay at its exact size. */ export const Button = React.forwardRef( - (props, ref) => ( - - - - ), + ({ tooltip, tooltipSide, title, 'aria-label': ariaLabel, ...props }, ref) => { + // `title` shows a tooltip for any button, and ALSO names icon-only buttons + // (they have no visible text). Text buttons keep their accessible name from + // their children — `title` must NOT override it there — so the name only + // falls back to `title` for `size="icon"`. The native `title` attribute is + // dropped either way so the browser doesn't pop a duplicate tooltip. + const accessibleName = + ariaLabel ?? (props.size === 'icon' ? title : undefined); + const tip = tooltip ?? title; + const base = ( + + ); + // The tooltip Trigger must wrap the REAL button (`ButtonBase` forwards its + // ref + props), never the `SkeletonBox` span — a plain span drops the + // Trigger's injected handlers/ref, leaving the tooltip permanently shut. + // So the Trigger goes inside and `SkeletonBox` wraps the whole thing. + // No tooltip when `asChild` — the button is then a Radix `Slot` (typically + // another overlay's trigger), and wrapping a Slot in a tooltip trigger + // breaks that composition. + const withTooltip = + tip == null || props.asChild ? ( + base + ) : ( + + + {base} + + {tip} + + + + ); + return {withTooltip}; + }, ); Button.displayName = 'Button'; diff --git a/packages/ui/src/components/primitives/icon-button.tsx b/packages/ui/src/components/primitives/icon-button.tsx index b705b6885c..6e7c5b63e5 100644 --- a/packages/ui/src/components/primitives/icon-button.tsx +++ b/packages/ui/src/components/primitives/icon-button.tsx @@ -60,12 +60,17 @@ export const IconButton = forwardRef< variant = 'ghost', className, 'aria-label': ariaLabel, + tooltip, asChild, slotChild, ...props }, ref, ) => { + // An icon button always has an accessible name (`aria-label`), so show it + // as a tooltip for free unless the caller supplies richer `tooltip` + // content. Skipped for the `asChild` (Slot) path below. + const resolvedTooltip = tooltip ?? ariaLabel; const iconNode = ( diff --git a/packages/ui/src/i18n/tests/glossary/glossary.json b/packages/ui/src/i18n/tests/glossary/glossary.json index 6ed2d83892..e5311d8bfe 100644 --- a/packages/ui/src/i18n/tests/glossary/glossary.json +++ b/packages/ui/src/i18n/tests/glossary/glossary.json @@ -417,7 +417,7 @@ "en": "Chat with AI", "de": "Chat mit KI", "fr": "Discuter avec l'IA", - "_note": "Matches UI label `navigation.chatWithAI`." + "_note": "Chat-module feature phrasing (e.g. the chat page meta description). The side-nav entry is the action label `navigation.newChat`." }, { "key": "feature_conversations", diff --git a/services/docs/app/content/frontmatter.json b/services/docs/app/content/frontmatter.json index 8ec8277df1..d5adb54037 100644 --- a/services/docs/app/content/frontmatter.json +++ b/services/docs/app/content/frontmatter.json @@ -236,7 +236,7 @@ "slug": "platform/member/environment", "locale": "de", "frontmatter": { - "title": "Umgebung & Geheimnisse", + "title": "Umgebungsvariablen & Geheimnisse", "description": "Deine persönlichen Umgebungsvariablen und Geheimnisse, die Tale in jede Agent-Sandbox einspeist, die du in einer Organisation startest — meist die Provider-Anmeldedaten, mit denen sich ein BYO-Agent authentifiziert." } }, @@ -1041,7 +1041,7 @@ "locale": "de", "frontmatter": { "title": "Upgrades", - "description": "Wie `tale upgrade` und `tale deploy` eine Tale-Instanz vorwärtsbewegen — das Rolling-Restart-Pattern, was vor einem Upgrade zu tun ist und die Versions-Kompatibilitäts-Story." + "description": "Wie `tale update` eine Tale-Instanz vorwärtsbewegt — die automatische CLI-/Instanz-Versions-Angleichung, das Rolling-Restart-Pattern, was vor einem Upgrade zu tun ist und die Versions-Kompatibilitäts-Story." } }, "de:self-hosted/operate/observability/operations": { @@ -1356,7 +1356,7 @@ "slug": "platform/member/environment", "locale": "fr", "frontmatter": { - "title": "Environnement et secrets", + "title": "Variables d'environnement et secrets", "description": "Tes variables d'environnement et secrets personnels, injectés dans chaque sandbox d'agent que tu lances dans une organisation — le plus souvent l'identifiant fournisseur avec lequel un agent BYO s'authentifie." } }, @@ -2161,7 +2161,7 @@ "locale": "fr", "frontmatter": { "title": "Montées de version", - "description": "Comment `tale upgrade` et `tale deploy` font avancer une instance Tale — le pattern de redémarrage rolling, quoi faire avant une montée de version et l'histoire de la compatibilité de versions." + "description": "Comment `tale update` fait avancer une instance Tale — l'alignement automatique de version entre la CLI et l'instance, le pattern de redémarrage rolling, quoi faire avant une montée de version et l'histoire de la compatibilité de versions." } }, "fr:self-hosted/operate/observability/operations": { @@ -2476,7 +2476,7 @@ "slug": "platform/member/environment", "locale": "en", "frontmatter": { - "title": "Environment & secrets", + "title": "Environment variables & secrets", "description": "Your personal environment variables and secrets, injected into every agent sandbox you run in an organisation — most often the provider credential a bring-your-own agent authenticates with." } }, @@ -3281,7 +3281,7 @@ "locale": "en", "frontmatter": { "title": "Upgrades", - "description": "How `tale upgrade` and `tale deploy` move a Tale instance forward — the rolling restart pattern, what to do before an upgrade, and the version compatibility story." + "description": "How `tale update` moves a Tale instance forward — automatic CLI/instance version alignment, the rolling restart pattern, what to do before an upgrade, and the version compatibility story." } }, "en:self-hosted/operate/observability/operations": { diff --git a/services/docs/tests/lib/markdown.ts b/services/docs/tests/lib/markdown.ts index 5850e8b4de..23cd4a5d89 100644 --- a/services/docs/tests/lib/markdown.ts +++ b/services/docs/tests/lib/markdown.ts @@ -105,6 +105,18 @@ function stripFences(text: string): string { return out.join('\n'); } +/** + * Strip every HTML comment (``, single- or multi-line) from `text`, + * replacing each non-newline character with a space so line numbers and line + * count stay stable. HTML comments are invisible structural markers (e.g. the + * `` fences a generator writes between), never prose, + * so prose mechanics must not scan them — the `/g, (m) => m.replace(/[^\n]/g, ' ')); +} + /** * Mask every inline-code span (` `…` `) in a single line, replacing the entire * span (including the backticks) with spaces of equal length. Keeps the line @@ -143,7 +155,7 @@ function maskUrls(line: string): string { export function* iterProseLines( body: string, ): Iterable<{ line: number; text: string }> { - const stripped = stripFences(body); + const stripped = stripHtmlComments(stripFences(body)); const lines = stripped.split('\n'); for (let i = 0; i < lines.length; i++) { const masked = maskUrls(maskInlineCode(lines[i])); diff --git a/services/platform/app/components/flow/flow-canvas.tsx b/services/platform/app/components/flow/flow-canvas.tsx index ad8a4c50e9..a4689c394f 100644 --- a/services/platform/app/components/flow/flow-canvas.tsx +++ b/services/platform/app/components/flow/flow-canvas.tsx @@ -53,8 +53,8 @@ function FlowCornerControls() { + + {Math.round(zoom * 100)}% - - + {/* Fit-to-screen — always enabled (re-fits even at 100%). */} + + + ); - const renderToolbar = () => { - switch (toolbarPosition) { - case 'overlay': - return ( -
- {headerContent ?? } -
- {toolbar} - {onClose && ( - - )} -
-
- ); - case 'bottom': - return null; - case 'inline': - return
{toolbar}
; - default: - return undefined; - } - }; - return ( -
- {renderToolbar()} +
+ {toolbarPosition === 'overlay' && ( +
+ {headerContent ?? } + {onClose && ( + + )} +
+ )} + + {toolbarPosition === 'inline' && ( +
{controls}
+ )}
- {isBottom && ( -
- {toolbar} + {/* Floating bottom-center cluster for the dialog (`overlay`) and document + preview (`bottom`) layouts. `pointer-events-none` on the wrapper keeps + drag-to-pan working everywhere except on the controls themselves. */} + {(toolbarPosition === 'overlay' || toolbarPosition === 'bottom') && ( +
+
{controls}
)}
diff --git a/services/platform/app/components/ui/data-table/data-table-pagination.tsx b/services/platform/app/components/ui/data-table/data-table-pagination.tsx index f914af14b6..73e8aa5b5f 100644 --- a/services/platform/app/components/ui/data-table/data-table-pagination.tsx +++ b/services/platform/app/components/ui/data-table/data-table-pagination.tsx @@ -123,7 +123,7 @@ export function DataTablePagination({ onClick={handlePrevious} disabled={isPrevDisabled} className="p-1.5" - aria-label={t('aria.previousPage')} + title={t('aria.previousPage')} > {isLoading ? ( @@ -151,7 +151,7 @@ export function DataTablePagination({ onClick={handleNext} disabled={isNextDisabled} className="p-1.5" - aria-label={t('aria.nextPage')} + title={t('aria.nextPage')} > {isLoading ? ( diff --git a/services/platform/app/components/ui/editor/editor-actions.test.tsx b/services/platform/app/components/ui/editor/editor-actions.test.tsx new file mode 100644 index 0000000000..e24378a039 --- /dev/null +++ b/services/platform/app/components/ui/editor/editor-actions.test.tsx @@ -0,0 +1,81 @@ +// @vitest-environment jsdom +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { EditorActions } from './editor-actions'; +import type { EditorController } from './types'; + +const toastMock = vi.fn(); + +vi.mock('@/app/hooks/use-toast', () => ({ + toast: (...args: unknown[]) => toastMock(...args), +})); + +vi.mock('@/lib/i18n/client', () => ({ + useT: () => ({ t: (key: string) => key }), +})); + +function makeController( + overrides: Partial = {}, +): EditorController { + return { + isDirty: true, + isSaving: false, + isValid: true, + isLoading: false, + dirtyKeys: new Set(['field']), + save: vi.fn().mockResolvedValue(undefined), + reset: vi.fn(), + ...overrides, + }; +} + +function clickSave() { + fireEvent.click(screen.getByRole('button', { name: 'actions.save' })); +} + +beforeEach(() => { + toastMock.mockReset(); +}); + +describe('EditorActions — suppressServerErrorToast', () => { + it('toasts a server error by default', async () => { + const controller = makeController({ + save: vi.fn().mockRejectedValue(new Error('Server boom')), + }); + render(); + clickSave(); + await waitFor(() => expect(toastMock).toHaveBeenCalledTimes(1)); + expect(toastMock).toHaveBeenCalledWith( + expect.objectContaining({ + description: 'Server boom', + variant: 'destructive', + }), + ); + }); + + it('suppresses the generic server-error toast when set (caller toasts its own)', async () => { + const controller = makeController({ + save: vi.fn().mockRejectedValue(new Error('Server boom')), + }); + render(); + clickSave(); + await waitFor(() => expect(controller.save).toHaveBeenCalled()); + expect(toastMock).not.toHaveBeenCalled(); + }); + + it('still toasts validation failures even when suppressed', async () => { + const controller = makeController({ + save: vi.fn().mockRejectedValue(new Error('VALIDATION_FAILED')), + }); + render(); + clickSave(); + await waitFor(() => expect(toastMock).toHaveBeenCalledTimes(1)); + expect(toastMock).toHaveBeenCalledWith( + expect.objectContaining({ + description: 'editor.fixHighlightedFields', + variant: 'destructive', + }), + ); + }); +}); diff --git a/services/platform/app/components/ui/editor/editor-actions.tsx b/services/platform/app/components/ui/editor/editor-actions.tsx index 69edd99428..1233804152 100644 --- a/services/platform/app/components/ui/editor/editor-actions.tsx +++ b/services/platform/app/components/ui/editor/editor-actions.tsx @@ -36,6 +36,14 @@ interface EditorActionsProps { onEvent?: (event: EditorTelemetryEvent) => void; /** Tag for telemetry (e.g. `'agent'`, `'org_settings'`). */ entityKind?: string; + /** + * Opt out of the generic server-error toast when the controller's own + * `save()` already surfaces a (typically localized) failure message itself — + * otherwise the user sees two destructive toasts for one failure. Validation + * failures are still toasted here, since callers don't handle those. Default + * `false` (EditorActions owns all error toasting). + */ + suppressServerErrorToast?: boolean; className?: string; } @@ -48,6 +56,7 @@ export function EditorActions({ formId, onEvent, entityKind = 'unknown', + suppressServerErrorToast = false, className, }: EditorActionsProps) { const { t } = useT('common'); @@ -89,7 +98,7 @@ export function EditorActions({ description: t('editor.fixHighlightedFields'), variant: 'destructive', }); - } else { + } else if (!suppressServerErrorToast) { toast({ title: t('actions.save'), description: err instanceof Error ? err.message : String(err), @@ -97,7 +106,7 @@ export function EditorActions({ }); } } - }, [controller, entityKind, onEvent, t]); + }, [controller, entityKind, onEvent, suppressServerErrorToast, t]); const handleDiscard = useCallback(() => { if (!controller.isDirty || controller.isSaving) return; diff --git a/services/platform/app/components/ui/feedback/banner.tsx b/services/platform/app/components/ui/feedback/banner.tsx index 2aed9335de..3c8d397488 100644 --- a/services/platform/app/components/ui/feedback/banner.tsx +++ b/services/platform/app/components/ui/feedback/banner.tsx @@ -112,7 +112,7 @@ export function Banner({ size="icon" onClick={onClose} className={bannerCloseVariants({ variant })} - aria-label={t('aria.dismiss')} + title={t('aria.dismiss')} > diff --git a/services/platform/app/components/ui/filters/filter-button.tsx b/services/platform/app/components/ui/filters/filter-button.tsx index 8d1d3d7a32..4e7eb7c985 100644 --- a/services/platform/app/components/ui/filters/filter-button.tsx +++ b/services/platform/app/components/ui/filters/filter-button.tsx @@ -6,10 +6,13 @@ import { ComponentProps } from 'react'; import { useT } from '@/lib/i18n/client'; import { cn } from '@/lib/utils/cn'; -interface FilterButtonProps extends ComponentProps { +// Intersection (not `interface extends`) because Button's props are a +// discriminated union (icon size requires aria-label) — an interface can't +// extend a union, but an intersection distributes over it cleanly. +type FilterButtonProps = ComponentProps & { hasActiveFilters: boolean; isLoading?: boolean; -} +}; export function FilterButton({ hasActiveFilters, diff --git a/services/platform/app/components/ui/navigation/navigation.tsx b/services/platform/app/components/ui/navigation/navigation.tsx index 51169357f9..083c23fdef 100644 --- a/services/platform/app/components/ui/navigation/navigation.tsx +++ b/services/platform/app/components/ui/navigation/navigation.tsx @@ -1,6 +1,7 @@ 'use client'; -import { Link, useLocation } from '@tanstack/react-router'; +import { Link, useLocation, useNavigate } from '@tanstack/react-router'; +import { useEffect } from 'react'; import { useBrandingContext } from '@/app/components/branding/branding-provider'; import { TaleLogo } from '@/app/components/ui/logo/tale-logo'; @@ -8,6 +9,7 @@ import { Tooltip } from '@/app/components/ui/overlays/tooltip'; import { UserButton } from '@/app/components/user-button'; import { NotificationBell } from '@/app/features/notifications/components/notification-bell'; import { useAbility } from '@/app/hooks/use-ability'; +import { useIsMac } from '@/app/hooks/use-is-mac'; import { useNavigationItems, type NavItem, @@ -52,10 +54,23 @@ function NavigationItem({ item }: { item: NavItem }) { const iconActiveStyle = isActive && accentColor ? { color: accentColor } : undefined; + // Rail links are icon-only; the tooltip carries the label and, for items that + // own a global keyboard shortcut, a hint chip so the binding is discoverable. + const tooltipContent = item.shortcut ? ( + <> + {item.label} + + {item.shortcut} + + + ) : ( + item.label + ); + if (item.external) { return ( - + - + { + const onKeyDown = (e: KeyboardEvent) => { + const isMod = isMac ? e.metaKey : e.ctrlKey; + if (isMod && e.altKey && e.code === 'KeyN') { + e.preventDefault(); + e.stopPropagation(); + void navigate({ + to: '/dashboard/$id/chat', + params: { id: organizationId }, + }); + } + }; + window.addEventListener('keydown', onKeyDown, true); + return () => window.removeEventListener('keydown', onKeyDown, true); + }, [isMac, navigate, organizationId]); return ( {loadingStates.prev ? ( @@ -146,7 +146,7 @@ export function Pagination({ disabled={ isEmpty || isNextDisabled || loadingStates.prev || loadingStates.next } - aria-label={t('aria.nextPage')} + title={t('aria.nextPage')} className="p-1.5" > {loadingStates.next ? ( diff --git a/services/platform/app/features/agents/components/agent-knowledge.tsx b/services/platform/app/features/agents/components/agent-knowledge.tsx index 37f7f79421..0645cdb759 100644 --- a/services/platform/app/features/agents/components/agent-knowledge.tsx +++ b/services/platform/app/features/agents/components/agent-knowledge.tsx @@ -106,7 +106,7 @@ function AgentFileRow({ variant="ghost" size="icon" onClick={() => setConfirmOpen(true)} - aria-label={t('agents.knowledge.removeFile')} + title={t('agents.knowledge.removeFile')} > diff --git a/services/platform/app/features/agents/components/agent-navigation.tsx b/services/platform/app/features/agents/components/agent-navigation.tsx index 8aba4281d3..0a768d8dda 100644 --- a/services/platform/app/features/agents/components/agent-navigation.tsx +++ b/services/platform/app/features/agents/components/agent-navigation.tsx @@ -84,7 +84,6 @@ const AGENT_TAB_DIRTY_KEYS = { 'knowledgeTopK', ], delegation: [], - responseTuning: ['responseTuning'], conversationStarters: ['conversationStarters'], webhook: [], // Env/secrets live in the `agentEnv` side-table, not the agent file — so this @@ -218,16 +217,6 @@ export function AgentNavigation({ matchMode: 'exact', dirtyKeys: AGENT_TAB_DIRTY_KEYS.delegation, }, - ...(isChat - ? [ - { - label: t('agents.navigation.responseTuning'), - href: `${basePath}/response-tuning`, - matchMode: 'exact' as const, - dirtyKeys: AGENT_TAB_DIRTY_KEYS.responseTuning, - }, - ] - : []), { label: t('agents.navigation.conversationStarters'), href: `${basePath}/conversation-starters`, diff --git a/services/platform/app/features/agents/components/agent-response-tuning.tsx b/services/platform/app/features/agents/components/agent-response-tuning.tsx deleted file mode 100644 index 19d602c36d..0000000000 --- a/services/platform/app/features/agents/components/agent-response-tuning.tsx +++ /dev/null @@ -1,158 +0,0 @@ -'use client'; - -import { CollapsibleDetails } from '@tale/ui/collapsible-details'; -import { PageSection } from '@tale/ui/page-section'; -import { SectionHeader } from '@tale/ui/section-header'; -import { useCallback } from 'react'; - -import { ContentArea } from '@/app/components/layout/content-area'; -import { RadioGroup } from '@/app/components/ui/forms/radio-group'; -import { useT } from '@/lib/i18n/client'; -import type { ResponseTuningConfig } from '@/lib/shared/schemas/agents'; - -import { useAgentConfig } from '../hooks/use-agent-config-context'; - -interface AgentResponseTuningProps { - organizationId: string; - agentId: string; -} - -// Typed option lists — also used to narrow RadioGroup's `string` callback value -// back to the literal union WITHOUT an unsafe cast (`find` returns the literal). -const EFFORT = ['adaptive', 'low', 'medium', 'high'] as const; -const CREATIVITY = ['adaptive', 'precise', 'balanced', 'creative'] as const; -const STYLE = [ - 'adaptive', - 'concise', - 'detailed', - 'formal', - 'friendly', -] as const; -const VERBOSITY = ['adaptive', 'terse', 'normal', 'verbose'] as const; -const QUALITY = ['lenient', 'balanced', 'strict'] as const; -const BOUND = ['off', 'low', 'medium', 'high'] as const; - -function narrow( - value: string, - allowed: readonly T[], -): T | undefined { - return allowed.find((a) => a === value); -} - -const cap = (v: string) => `${v.charAt(0).toUpperCase()}${v.slice(1)}`; - -export function AgentResponseTuning(_props: AgentResponseTuningProps) { - const { t } = useT('settings'); - const { config, updateConfig } = useAgentConfig(); - const tuning: ResponseTuningConfig = config.responseTuning ?? {}; - - const patch = useCallback( - (next: Partial) => { - // Functional update so each field merges onto the LATEST responseTuning, - // not a render-closure snapshot — two fields committing in the same tick - // can't stomp each other. - updateConfig((prev) => ({ - responseTuning: { ...prev.responseTuning, ...next }, - })); - }, - [updateConfig], - ); - - const k = (suffix: string) => t(`agents.responseTuning.${suffix}`); - const opt = (values: readonly string[], keyOf: (v: string) => string) => - values.map((value) => ({ value, label: k(keyOf(value)) })); - - // The four primary controls each map a value to its i18n label key: the - // `adaptive`/`off` sentinels have their own key, the rest are ``. - const labelKey = (field: string) => (v: string) => - v === 'adaptive' ? 'adaptive' : `${field}${cap(v)}`; - const boundKey = (v: string) => - v === 'off' ? 'boundNone' : `effort${cap(v)}`; - - return ( - - - - {/* The Auto router and adaptive reasoning governor handle these per - message; everything here is an optional override, collapsed by default - so the common (fully-automatic) case stays uncluttered. */} - -
- patch({ effort: narrow(v, EFFORT) })} - options={opt(EFFORT, labelKey('effort'))} - /> - - patch({ creativity: narrow(v, CREATIVITY) })} - options={opt(CREATIVITY, labelKey('creativity'))} - /> - - patch({ style: narrow(v, STYLE) })} - options={opt(STYLE, labelKey('style'))} - /> - - patch({ verbosity: narrow(v, VERBOSITY) })} - options={opt(VERBOSITY, labelKey('verbosity'))} - /> - - - - patch({ qualityProfile: narrow(v, QUALITY) }) - } - options={opt(QUALITY, (v) => `quality${cap(v)}`)} - /> - -
- { - const b = narrow(v, BOUND); - patch({ effortFloor: b === 'off' ? undefined : b }); - }} - options={opt(BOUND, boundKey)} - /> - { - const b = narrow(v, BOUND); - patch({ effortCeiling: b === 'off' ? undefined : b }); - }} - options={opt(BOUND, boundKey)} - /> -
-
-
-
-
- ); -} diff --git a/services/platform/app/features/agents/hooks/use-agent-config-context.test.tsx b/services/platform/app/features/agents/hooks/use-agent-config-context.test.tsx index 8db8810685..1ff1ee9117 100644 --- a/services/platform/app/features/agents/hooks/use-agent-config-context.test.tsx +++ b/services/platform/app/features/agents/hooks/use-agent-config-context.test.tsx @@ -193,12 +193,12 @@ describe('useAgentConfig', () => { it('stays clean when a nested patch only reorders keys (no false dirty)', () => { // Regression: dirty detection was `JSON.stringify(config) !== // JSON.stringify(saved)`, which is key-order-sensitive. Re-applying the - // same nested object with its keys in a different order (as the response - // tuning panel does on every patch) must NOT read as dirty. + // same nested object with its keys in a different order (as a config panel + // does on every patch) must NOT read as dirty. const { result } = renderHook(() => useAgentConfig(), { wrapper: createWrapper({ ...BASE_CONFIG, - responseTuning: { effort: 'high', creativity: 'balanced' }, + routing: { modelSelection: 'auto', cascade: true }, }), }); expect(result.current.isDirty).toBe(false); @@ -206,10 +206,10 @@ describe('useAgentConfig', () => { act(() => { // Same content, keys reversed + an explicit `undefined` leaf. result.current.updateConfig({ - responseTuning: { - creativity: 'balanced', - effort: 'high', - style: undefined, + routing: { + cascade: true, + modelSelection: 'auto', + cascadeDraftModel: undefined, }, }); }); @@ -217,16 +217,16 @@ describe('useAgentConfig', () => { expect(result.current.isDirty).toBe(false); }); - it('becomes dirty on a real nested response-tuning change', () => { + it('becomes dirty on a real nested routing change', () => { const { result } = renderHook(() => useAgentConfig(), { wrapper: createWrapper({ ...BASE_CONFIG, - responseTuning: { effort: 'high' }, + routing: { modelSelection: 'auto' }, }), }); act(() => { - result.current.updateConfig({ responseTuning: { effort: 'low' } }); + result.current.updateConfig({ routing: { modelSelection: 'config' } }); }); expect(result.current.isDirty).toBe(true); diff --git a/services/platform/app/features/agents/organigram/organigram-panel.tsx b/services/platform/app/features/agents/organigram/organigram-panel.tsx index 0384d04ea3..8d1684e444 100644 --- a/services/platform/app/features/agents/organigram/organigram-panel.tsx +++ b/services/platform/app/features/agents/organigram/organigram-panel.tsx @@ -124,7 +124,7 @@ export function OrganigramPanel({ size="icon" variant="ghost" icon={X} - aria-label={t('panel.close')} + title={t('panel.close')} onClick={onClose} />
diff --git a/services/platform/app/features/automations/components/automation-ai-chat-panel.tsx b/services/platform/app/features/automations/components/automation-ai-chat-panel.tsx index 0bc069658d..93c1fe38ef 100644 --- a/services/platform/app/features/automations/components/automation-ai-chat-panel.tsx +++ b/services/platform/app/features/automations/components/automation-ai-chat-panel.tsx @@ -103,7 +103,7 @@ export function AutomationAIChatPanel({ size="icon" className="size-8" onClick={onClose} - aria-label={t('sidePanel.close')} + title={t('sidePanel.close')} > diff --git a/services/platform/app/features/automations/components/automation-sidepanel.tsx b/services/platform/app/features/automations/components/automation-sidepanel.tsx index d397f86224..6e3317af18 100644 --- a/services/platform/app/features/automations/components/automation-sidepanel.tsx +++ b/services/platform/app/features/automations/components/automation-sidepanel.tsx @@ -366,7 +366,7 @@ export function AutomationSidePanel({ size="icon" className="size-8" onClick={onClose} - aria-label={t('sidePanel.close')} + title={t('sidePanel.close')} > diff --git a/services/platform/app/features/automations/components/automation-steps.tsx b/services/platform/app/features/automations/components/automation-steps.tsx index e04e88cd50..1e58778280 100644 --- a/services/platform/app/features/automations/components/automation-steps.tsx +++ b/services/platform/app/features/automations/components/automation-steps.tsx @@ -125,6 +125,7 @@ function AutomationStepsInner({ onOpenAIChat, }: AutomationStepsProps) { const { t } = useT('automations'); + const { t: tCommon } = useT('common'); const hasSteps = steps && steps.length > 0; const [nodes, setNodes, onNodesChange] = useNodesState([]); // oxlint-disable-next-line typescript/no-unnecessary-type-arguments -- without explicit Edge, TS infers never[] @@ -448,6 +449,7 @@ function AutomationStepsInner({ @@ -116,7 +116,7 @@ export const BranchNavigator = memo(function BranchNavigator({ className="size-6" onClick={handleNext} disabled={currentIndex >= totalCount - 1} - aria-label={t('branchNavigator.next')} + title={t('branchNavigator.next')} > diff --git a/services/platform/app/features/chat/components/chat-error-display.tsx b/services/platform/app/features/chat/components/chat-error-display.tsx index 042d759c6d..fc2b852cf2 100644 --- a/services/platform/app/features/chat/components/chat-error-display.tsx +++ b/services/platform/app/features/chat/components/chat-error-display.tsx @@ -33,16 +33,21 @@ export function ChatErrorDisplay({ error, onRetry }: ChatErrorDisplayProps) { {tChat('errorGenerating')}

- {tChat(sanitized.i18nKey)} + {tChat(sanitized.i18nKey, sanitized.params)}

- {error && ( + {sanitized.triedCount != null && ( +

+ {tChat('errorTriedModels', { count: sanitized.triedCount })} +

+ )} + {sanitized.rawMessage && (

- {error} + {sanitized.rawMessage}

)} diff --git a/services/platform/app/features/chat/components/chat-header.tsx b/services/platform/app/features/chat/components/chat-header.tsx index f6c57a6c0b..2e0f7e2ed7 100644 --- a/services/platform/app/features/chat/components/chat-header.tsx +++ b/services/platform/app/features/chat/components/chat-header.tsx @@ -14,8 +14,6 @@ import { Ellipsis, Search, Share, - Plus, - SquarePen, } from 'lucide-react'; import { useEffect, useState, useCallback, useMemo } from 'react'; @@ -24,6 +22,7 @@ import { Sheet } from '@/app/components/ui/overlays/sheet'; import { Tooltip } from '@/app/components/ui/overlays/tooltip'; import { useChatLayout } from '@/app/features/chat/context/chat-layout-context'; import { useFormatDate } from '@/app/hooks/use-format-date'; +import { useIsMac } from '@/app/hooks/use-is-mac'; import { useOptionalTeamFilter } from '@/app/hooks/use-team-filter'; import { useT } from '@/lib/i18n/client'; import { cn } from '@/lib/utils/cn'; @@ -39,12 +38,12 @@ interface ChatHeaderProps { export function ChatHeader({ organizationId, threadId }: ChatHeaderProps) { const navigate = useNavigate(); - const { isHistoryOpen, setIsHistoryOpen, clearChatState } = useChatLayout(); + const { isHistoryOpen, setIsHistoryOpen } = useChatLayout(); const [isSearchOpen, setIsSearchOpen] = useState(false); const [isMobileHistoryOpen, setIsMobileHistoryOpen] = useState(false); const [isShareDialogOpen, setIsShareDialogOpen] = useState(false); const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); - const [isMac, setIsMac] = useState(false); + const isMac = useIsMac(); const { t: tChat } = useT('chat'); const { t: tDialogs } = useT('dialogs'); @@ -86,19 +85,7 @@ export function ChatHeader({ organizationId, threadId }: ChatHeaderProps) { [navigate, organizationId], ); - useEffect(() => { - if (typeof window !== 'undefined') { - const platform = ( - navigator.platform || - navigator.userAgent || - '' - ).toLowerCase(); - setIsMac(platform.includes('mac')); - } - }, []); - const findShortcut = isMac ? '⌘ K' : 'CTRL + K'; - const newChatShortcut = isMac ? '⌥ ⌘ N' : 'ALT + CTRL + N'; const historyShortcut = isMac ? '⌘ H' : 'CTRL + H'; const handleToggleSearch = useCallback(() => { @@ -114,14 +101,6 @@ export function ChatHeader({ organizationId, threadId }: ChatHeaderProps) { } }, [isHistoryOpen, setIsHistoryOpen]); - const handleNewChat = useCallback(() => { - clearChatState(); - void navigate({ - to: '/dashboard/$id/chat', - params: { id: organizationId }, - }); - }, [navigate, organizationId, clearChatState]); - const handleChatSelect = useCallback(() => { setIsMobileHistoryOpen(false); }, []); @@ -135,11 +114,6 @@ export function ChatHeader({ organizationId, threadId }: ChatHeaderProps) { handleToggleSearch(); return; } - if (isMod && e.altKey && e.code === 'KeyN') { - e.preventDefault(); - e.stopPropagation(); - handleNewChat(); - } if (isMod && !e.shiftKey && (e.key === 'h' || e.key === 'H')) { e.preventDefault(); e.stopPropagation(); @@ -148,7 +122,7 @@ export function ChatHeader({ organizationId, threadId }: ChatHeaderProps) { }; window.addEventListener('keydown', onKeyDown, true); return () => window.removeEventListener('keydown', onKeyDown, true); - }, [isMac, handleToggleSearch, handleNewChat, handleToggleHistory]); + }, [isMac, handleToggleSearch, handleToggleHistory]); // The per-thread voice toggle moved to the composer (next to dictation), // so the header dropdown now only carries the export action. @@ -185,7 +159,7 @@ export function ChatHeader({ organizationId, threadId }: ChatHeaderProps) { /> -
+
@@ -205,7 +179,14 @@ export function ChatHeader({ organizationId, threadId }: ChatHeaderProps) { aria-label={ isHistoryOpen ? tChat('hideHistory') : tChat('showHistory') } - className={cn(isHistoryOpen && 'bg-accent text-accent-foreground')} + // Negative margin pulls the icon's glyph to the header's content + // edge so it lines up with page titles (which start at px-4), since + // a ghost icon button carries its own p-2 inset. Same idiom as the + // onboarding wizard's leading Back button. + className={cn( + '-ml-2', + isHistoryOpen && 'bg-accent text-accent-foreground', + )} > - - {tChat('newChat')} - - {newChatShortcut} - - - } - side="bottom" - contentClassName="py-1.5" - > - - - {threadId && ( <>
@@ -296,7 +255,7 @@ export function ChatHeader({ organizationId, threadId }: ChatHeaderProps) { size="icon" variant="ghost" onClick={handleToggleHistory} - aria-label={ + title={ isMobileHistoryOpen ? tChat('hideHistory') : tChat('showHistory') } > @@ -311,7 +270,7 @@ export function ChatHeader({ organizationId, threadId }: ChatHeaderProps) { size="icon" variant="ghost" onClick={handleToggleSearch} - aria-label={tChat('searchChat')} + title={tChat('searchChat')} > @@ -321,7 +280,7 @@ export function ChatHeader({ organizationId, threadId }: ChatHeaderProps) { size="icon" variant="ghost" onClick={() => setIsShareDialogOpen(true)} - aria-label={tChat('share.button')} + title={tChat('share.button')} > @@ -340,14 +299,6 @@ export function ChatHeader({ organizationId, threadId }: ChatHeaderProps) { /> )} -
diff --git a/services/platform/app/features/chat/components/chat-input.tsx b/services/platform/app/features/chat/components/chat-input.tsx index 47dad3dbc4..5b9c00d244 100644 --- a/services/platform/app/features/chat/components/chat-input.tsx +++ b/services/platform/app/features/chat/components/chat-input.tsx @@ -36,13 +36,24 @@ import { type KbMention, type MentionTrigger, } from '../hooks/use-kb-mentions'; -import { captureScreenshot } from '../utils/capture-screenshot'; +import { useVideoUrlIngest } from '../hooks/use-video-url-ingest'; import { normalizeCopiedText } from '../utils/normalize-copied-text'; import { AgentSelector } from './agent-selector'; import { useArenaModeOptional } from './arena/arena-mode-context'; import { ArenaModelSelector } from './arena/arena-model-selector'; import { AttachmentTray } from './chat-input/attachment-tray'; import { toBcp47 } from './chat-input/locale-defaults'; +import { + PasteImageOverlay, + type PasteImageChip, +} from './chat-input/paste-image-overlay'; +import { + buildMarkerToken, + collapseMarkerSpaces, + nextPasteImageId, + pastedImageIdFromName, + tokenSpans, +} from './chat-input/paste-image-tokens'; import { ComposerCapabilityPills } from './composer-capability-pills'; import { ComposerModeMenu } from './composer-mode-menu'; import { @@ -185,6 +196,23 @@ interface ChatInputProps extends Omit< variant?: 'full' | 'assistant'; } +/** + * Media-processing states that block sending, in send-button-tooltip + * precedence order; each maps to its `chat` i18n tooltip key. + */ +type MediaBlockReason = + | 'transcribing' + | 'processingVideo' + | 'failedVideo' + | 'failedAudio'; + +const MEDIA_BLOCK_TOOLTIP_KEY: Record = { + transcribing: 'transcription.inProgressTooltip', + processingVideo: 'videoLink.chip.inProgressTooltip', + failedVideo: 'videoLink.chip.failedSendBlockedTooltip', + failedAudio: 'transcription.failedSendBlockedTooltip', +}; + export function ChatInput({ value = '', onChange, @@ -259,14 +287,27 @@ export function ChatInput({ // path so we don't mutate the textarea while an IME commit is in // flight (round-2 V10 / HIGH #19). const isComposingRef = useRef(false); - // Set as soon as a paste begins ingest; cleared in `.finally`. The - // send-gate ORs this in so a paste-then-Enter race can't bypass the - // chip rendering (chip query won't show the row until the mutation - // round-trip lands, but `ingestVideoUrlsFromText` runs fire-and-forget - // so without this flag the gate has nothing to watch) — round-2 V10 / - // HIGH #23. - const pasteIngestInFlightRef = useRef(false); - const [pasteIngestPending, setPasteIngestPending] = useState(false); + // A pasted/dropped video URL ingests fire-and-forget; `pasteIngestPending` + // lets the send-gate block a paste-then-Enter race until the chip row lands + // (round-2 V10 / HIGH #23). Shared by the paste + drag-drop handlers below. + const { pending: pasteIngestPending, ingest: ingestVideoUrls } = + useVideoUrlIngest(ingestVideoUrlsFromText, organizationId, i18n.language); + + // Single source of truth for the media-processing states that block send. + // `mediaBlockReason` names the active one (precedence order) for the send- + // button tooltip; `mediaBlocksSend` is the OR consumed by the send-gate and + // the disabled state. `pasteIngestPending` (blocks send, no tooltip) and + // `sendBlocked` (carries its own reason string) stay separate. + const mediaBlockReason: MediaBlockReason | null = isTranscribing + ? 'transcribing' + : isProcessingVideo + ? 'processingVideo' + : hasFailedVideoJobs + ? 'failedVideo' + : hasFailedAudioJobs + ? 'failedAudio' + : null; + const mediaBlocksSend = mediaBlockReason !== null; const [previewImage, setPreviewImage] = useState<{ src: string; alt: string; @@ -423,10 +464,7 @@ export function ChatInput({ disabled || isUploading || isIndexing || - isTranscribing || - isProcessingVideo || - hasFailedVideoJobs || - hasFailedAudioJobs || + mediaBlocksSend || pasteIngestPending || sendBlocked ) @@ -449,8 +487,10 @@ export function ChatInput({ attachments.length > 0 ? clearAttachments() : undefined; // Prepend any staged quote as a markdown blockquote so the model sees - // the referenced passage above the user's message, then clear it. - const trimmed = value.trim(); + // the referenced passage above the user's message, then clear it. The + // `[N]` markers ride along (positional reference for the agent); their + // reserve spaces collapse to one so the sent text stays clean. + const trimmed = collapseMarkerSpaces(value).trim(); const messageToSend = quotedText ? `> ${quotedText.replace(/\n/g, '\n> ')}\n\n${trimmed}` : trimmed; @@ -471,28 +511,6 @@ export function ChatInput({ onSendMessage(messageToSend, attachmentsToSend, kbRefsToSend); }; - const screenshotSupported = - typeof navigator !== 'undefined' && - !!navigator.mediaDevices?.getDisplayMedia; - - const handleTakeScreenshot = useCallback(async () => { - try { - const file = await captureScreenshot(); - if (file) await uploadFiles([file]); - } catch (err) { - // The user dismissing the OS picker rejects with NotAllowed/Abort — - // treat as a silent cancel, surface anything else. - if ( - err instanceof DOMException && - (err.name === 'NotAllowedError' || err.name === 'AbortError') - ) { - return; - } - console.error('[screenshot] capture failed', err); - toast({ title: tComposer('screenshotFailed'), variant: 'destructive' }); - } - }, [uploadFiles, tComposer]); - const imageAttachments = useMemo( () => attachments.filter((att) => att.fileType.startsWith('image/')), [attachments], @@ -503,6 +521,106 @@ export function ChatInput({ [attachments], ); + // Chip data per image id, derived purely from the live attachments + uploads + // — the marker is independent of the image (deleting a `[N]` keeps the + // attachment, and vice-versa). An id present here means a `[N]` token refers + // to a real image, which also gates atomic marker deletion below. + const pasteChips = useMemo(() => { + const map = new Map(); + for (const id of uploadingFiles) { + const tokenId = pastedImageIdFromName(id); + if (tokenId !== null) map.set(tokenId, { status: 'uploading' }); + } + for (const att of attachments) { + const tokenId = pastedImageIdFromName(att.fileName); + if (tokenId !== null && att.fileType.startsWith('image/')) { + map.set(tokenId, { status: 'ready', previewUrl: att.previewUrl }); + } + } + return map; + }, [attachments, uploadingFiles]); + + // Clicking an inline `[N]` chip opens the same preview dialog as its tray + // thumbnail (the chip mirrors the tray, it doesn't replace it). + const openPastedImage = useCallback( + (id: number) => { + const att = attachments.find( + (a) => pastedImageIdFromName(a.fileName) === id, + ); + if (att?.previewUrl) { + setPreviewImage({ src: att.previewUrl, alt: att.fileName }); + } + }, + [attachments], + ); + + // Removing an image from the tray also strips its inline `[N]` marker, so a + // removed image never leaves an orphan token behind. (Deleting the marker + // alone keeps the image — see deletePastedTokenAtCaret.) + const handleRemoveAttachment = useCallback( + (fileId: Id<'_storage'>) => { + const att = attachments.find((a) => a.fileId === fileId); + const id = att ? pastedImageIdFromName(att.fileName) : null; + if (id !== null && onChange) { + const span = tokenSpans(value).find((s) => s.id === id); + if (span) { + let end = span.end; + while (value[end] === ' ') end += 1; + onChange(value.slice(0, span.start) + value.slice(end)); + } + } + removeAttachment(fileId); + }, + [attachments, value, onChange, removeAttachment], + ); + + // Insert `[N]` marker(s) (each with reserve spaces for the chip) at the + // caret. Shared by paste and the drag-from-tray drop below. + const insertMarkersAtCaret = (ids: number[]) => { + const textarea = textareaRef.current; + if (!textarea || !onChange || ids.length === 0) return; + const start = textarea.selectionStart ?? textarea.value.length; + const end = textarea.selectionEnd ?? textarea.value.length; + const before = textarea.value.slice(0, start); + const lead = before.length > 0 && !/\s$/.test(before) ? ' ' : ''; + const tokenText = lead + ids.map(buildMarkerToken).join(''); + textarea.setRangeText(tokenText, start, end, 'end'); + onChange(textarea.value); + }; + + // Drag a tray image into the composer to drop its `[N]` marker. The tray + // thumbnail puts its id on the drag in this custom type; we move the caret to + // the drop point (best-effort) and insert the marker there. + const handleMarkerDragOver = (e: React.DragEvent) => { + if (e.dataTransfer.types.includes('application/x-tale-marker-id')) { + e.preventDefault(); + e.stopPropagation(); // suppress the FileUpload drop overlay over the input + } + }; + const handleMarkerDrop = (e: React.DragEvent) => { + const raw = e.dataTransfer.getData('application/x-tale-marker-id'); + if (!raw) return; // not a marker drag — let the file DropZone handle it + const id = Number(raw); + if (!Number.isInteger(id)) return; + e.preventDefault(); + e.stopPropagation(); + const ta = textareaRef.current; + if (ta) { + const doc = document as Document & { + caretPositionFromPoint?: ( + x: number, + y: number, + ) => { offsetNode: Node; offset: number } | null; + }; + const pos = doc.caretPositionFromPoint?.(e.clientX, e.clientY); + if (pos && (pos.offsetNode === ta || ta.contains(pos.offsetNode))) { + ta.setSelectionRange(pos.offset, pos.offset); + } + ta.focus(); + } + insertMarkersAtCaret([id]); + }; + const handleInputChange = (newValue: string) => { onChange?.(newValue); // Typing both opens and closes the `@` picker (caret-move events only @@ -519,6 +637,41 @@ export function ChatInput({ [value, onChange], ); + // Treat a `[N]` marker as a single atomic unit when deleting: a plain + // Backspace at the end of `[1]` would otherwise erase only `]` and leave a + // dangling `[1`. When the caret sits inside/adjacent to a marker, wipe the + // whole token plus its reserve spaces. Deleting a marker only removes the + // marker — the image stays in the tray. Returns true when it handled the key. + const deletePastedTokenAtCaret = ( + e: React.KeyboardEvent, + direction: 'back' | 'forward', + ): boolean => { + if (pasteChips.size === 0) return false; + const ta = textareaRef.current; + if ( + !ta || + ta.selectionStart === null || + ta.selectionStart !== ta.selectionEnd + ) { + return false; + } + const caret = ta.selectionStart; + const span = tokenSpans(ta.value).find( + (s) => + pasteChips.has(s.id) && + (direction === 'back' + ? s.start < caret && caret <= s.end + : s.start <= caret && caret < s.end), + ); + if (!span) return false; + e.preventDefault(); + let end = span.end; + while (ta.value[end] === ' ') end += 1; // consume the reserve spaces + ta.setRangeText('', span.start, end, 'end'); + onChange?.(ta.value); + return true; + }; + const handleKeyDown = (e: React.KeyboardEvent) => { // IME composition guard. macOS Pinyin / Japanese Kotoeri commits a // candidate via Enter; without these checks the textarea swallows @@ -564,6 +717,13 @@ export function ChatInput({ } } + // Atomic marker deletion (runs before the textarea's default edit so a + // Backspace can't shave a `[1]` token down to a dangling `[1`). + if (!isComposing) { + if (e.key === 'Backspace' && deletePastedTokenAtCaret(e, 'back')) return; + if (e.key === 'Delete' && deletePastedTokenAtCaret(e, 'forward')) return; + } + if (e.key !== 'Enter' || e.shiftKey) return; if (isComposing) return; e.preventDefault(); @@ -592,26 +752,40 @@ export function ChatInput({ const items = e.clipboardData?.items; if (!items) return; + // Name each pasted image `[N].`, where N is one past the highest `[N]` + // already in the text — so numbering restarts at 1 once a send clears the + // composer and never collides with a token the user typed. const imageFiles: File[] = []; + const newImageIds: number[] = []; + let nextId = nextPasteImageId(textareaRef.current?.value ?? value); for (let i = 0; i < items.length; i++) { const item = items[i]; if (item.type.startsWith('image/')) { const file = item.getAsFile(); if (file) { const extension = item.type.split('/')[1] || 'png'; - const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); - const renamedFile = new File( - [file], - `pasted-image-${timestamp}.${extension}`, - { type: file.type }, + const id = nextId++; + newImageIds.push(id); + imageFiles.push( + new File([file], `[${id}].${extension}`, { type: file.type }), ); - imageFiles.push(renamedFile); } } } if (imageFiles.length > 0) { + // A pasted image is represented by its `[N]` marker, inserted + // programmatically below. Clipboards frequently ship a text/alt or URL + // fallback ALONGSIDE the image bytes, so prevent the native text paste + // (and skip the text-normalize path) — otherwise that fallback inserts + // on top of the marker and the message double-ups. + e.preventDefault(); + // Drop a `[N]` reference marker at the caret so the image has a position + // in the message; the overlay paints a thumbnail badge over it and the + // agent sees `[N]` in the prose next to the `[N].ext` attachment. + insertMarkersAtCaret(newImageIds); void uploadFiles(imageFiles); + return; } // Video-link detection. Read both text/plain and text/html (rich- @@ -629,23 +803,7 @@ export function ChatInput({ // double, some older email clients ship single). html.match(/href=["']([^"']+)["']/g)?.join(' ') || ''; - if (text) { - // Set the in-flight ref BEFORE awaiting the ingest so the send- - // gate sees the pending state on the very next render. Cleared - // in `.finally`. Without this, a user who pastes then hits - // Enter immediately would ship the message before the mutation - // round-trip lands and the chip query reflects the new row. - pasteIngestInFlightRef.current = true; - setPasteIngestPending(true); - void ingestVideoUrlsFromText( - text, - organizationId, - i18n.language, - ).finally(() => { - pasteIngestInFlightRef.current = false; - setPasteIngestPending(false); - }); - } + ingestVideoUrls(text); } // Normalize the pasted text: collapse the blank-line stacks that copying @@ -683,27 +841,7 @@ export function ChatInput({ { - // Mirror the paste-handler gate so a drag-and-drop URL - // followed by an immediate Enter doesn't beat the chip - // into existence — without this, the send-gate doesn't - // know an ingest is in-flight and the agent receives - // the raw URL instead of the transcript. - pasteIngestInFlightRef.current = true; - setPasteIngestPending(true); - void ingestVideoUrlsFromText( - text, - organizationId, - i18n.language, - ).finally(() => { - pasteIngestInFlightRef.current = false; - setPasteIngestPending(false); - }); - } - : undefined - } + onTextDrop={ingestVideoUrlsFromText ? ingestVideoUrls : undefined} clickable={false} disabled={attachDisabled || fileUploadDisabled} > @@ -787,7 +925,7 @@ export function ChatInput({ transcriptionStatuses={transcriptionStatuses} indexingStatuses={indexingStatuses} retryAudioTranscription={retryAudioTranscription} - removeAttachment={removeAttachment} + removeAttachment={handleRemoveAttachment} onPreviewImage={setPreviewImage} onPreviewTranscript={setPreviewTranscript} /> @@ -823,6 +961,8 @@ export function ChatInput({ onFocus={onComposerActivate} onKeyDown={handleKeyDown} onPaste={handlePaste} + onDragOver={handleMarkerDragOver} + onDrop={handleMarkerDrop} // Caret moves (clicks, arrow keys) only update/close an open // mention picker — typing is what opens it (handleInputChange). onSelect={() => updateMentionTrigger(true)} @@ -851,6 +991,12 @@ export function ChatInput({ : undefined } /> + {value.length === 0 && !inputDisabled && ( fileInputRef.current?.click()} - onTakeScreenshot={ - screenshotSupported && !fileUploadDisabled - ? () => void handleTakeScreenshot() - : undefined - } fileUploadDisabled={fileUploadDisabled} disabled={attachDisabled} /> @@ -976,24 +1117,15 @@ export function ChatInput({ inputDisabled || isUploading || isIndexing || - isTranscribing || - isProcessingVideo || - hasFailedVideoJobs || - hasFailedAudioJobs || + mediaBlocksSend || pasteIngestPending || sendBlocked; const tooltipContent = - isTranscribing && !isLoading - ? tChat('transcription.inProgressTooltip') - : isProcessingVideo && !isLoading - ? tChat('videoLink.chip.inProgressTooltip') - : hasFailedVideoJobs && !isLoading - ? tChat('videoLink.chip.failedSendBlockedTooltip') - : hasFailedAudioJobs && !isLoading - ? tChat('transcription.failedSendBlockedTooltip') - : sendBlocked && sendBlockedReason && !isLoading - ? sendBlockedReason - : ''; + !isLoading && mediaBlockReason + ? tChat(MEDIA_BLOCK_TOOLTIP_KEY[mediaBlockReason]) + : sendBlocked && sendBlockedReason && !isLoading + ? sendBlockedReason + : ''; // Native `disabled` swallows pointer events on // Chromium/WebKit, so the Tooltip trigger never fires // when the button is in exactly the states the tooltip diff --git a/services/platform/app/features/chat/components/chat-input/attachment-tray.tsx b/services/platform/app/features/chat/components/chat-input/attachment-tray.tsx index 314ad738f1..7d92856be5 100644 --- a/services/platform/app/features/chat/components/chat-input/attachment-tray.tsx +++ b/services/platform/app/features/chat/components/chat-input/attachment-tray.tsx @@ -17,6 +17,7 @@ import { type IndexingStatusInfo, type TranscriptionStatusInfo, } from './attachment-status-label'; +import { pastedImageIdFromName } from './paste-image-tokens'; interface TranscriptPreview { fileName: string; @@ -102,47 +103,72 @@ export function AttachmentTray({
))} - {imageAttachments.map((attachment) => ( -
- + {pasteIndex !== null && ( + + {pasteIndex}. + )} - - -
- ))} + +
+ ); + })} {fileAttachments.map((attachment) => { const audioInfo = isAudioOrVideo(attachment.fileType) diff --git a/services/platform/app/features/chat/components/chat-input/paste-image-overlay.browser.test.tsx b/services/platform/app/features/chat/components/chat-input/paste-image-overlay.browser.test.tsx new file mode 100644 index 0000000000..94da71598b --- /dev/null +++ b/services/platform/app/features/chat/components/chat-input/paste-image-overlay.browser.test.tsx @@ -0,0 +1,120 @@ +import { waitFor } from '@testing-library/react'; +import { type CSSProperties, useRef } from 'react'; +import { describe, expect, it, vi } from 'vitest'; + +import { render } from '@/tests/utils/render'; + +import { PasteImageOverlay, type PasteImageChip } from './paste-image-overlay'; + +/** + * REAL Chromium (project `browser`) test. The overlay positions its chips by + * mirroring the textarea text into an off-screen `
` and reading each + * token span's `offsetLeft/Top/Width/Height` — geometry jsdom fakes as 0, so + * this needs real layout, hence the browser tier. Verifies the end-to-end + * pipeline: a `[N]` token in the textarea gets a thumbnail/spinner chip painted + * over it at a real position, and tokens without chip data are left alone. + */ + +// Mirrors the composer textarea's box: zero padding/border so the overlay's +// "assumes zero padding" measurement holds. +const TEXTAREA_STYLE: CSSProperties = { + minHeight: 72, + width: 320, + padding: 0, + border: 0, + margin: 0, + font: '16px/1.5 sans-serif', + resize: 'none', + boxSizing: 'border-box', +}; + +// 1×1 transparent PNG — a valid src so the "ready" chip renders an image. +const PIXEL = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M8AAAMBAQDJ/pLvAAAAAElFTkSuQmCC'; + +function Harness({ + value, + chips, + onOpen = vi.fn(), +}: { + value: string; + chips: Map; + onOpen?: (id: number) => void; +}) { + const ref = useRef(null); + return ( +
+